blob: e700789a7eee685a31587a028b14d921ad031e40 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.nosql.mongodb;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jetty.nosql.NoSqlSession;
import org.eclipse.jetty.nosql.NoSqlSessionManager;
import org.eclipse.jetty.server.SessionIdManager;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;
/**
* MongoSessionManager
*
* Clustered session manager using MongoDB as the shared DB instance.
* The document model is an outer object that contains the elements:
* <ul>
* <li>"id" : session_id </li>
* <li>"created" : create_time </li>
* <li>"accessed": last_access_time </li>
* <li>"maxIdle" : max_idle_time setting as session was created </li>
* <li>"expiry" : time at which session should expire </li>
* <li>"valid" : session_valid </li>
* <li>"context" : a nested object containing 1 nested object per context for which the session id is in use
* </ul>
* Each of the nested objects inside the "context" element contains:
* <ul>
* <li>unique_context_name : nested object containing name:value pairs of the session attributes for that context</li>
* </ul>
* <p>
* One of the name:value attribute pairs will always be the special attribute "__metadata__". The value
* is an object representing a version counter which is incremented every time the attributes change.
* </p>
* <p>
* For example:
* <code>
* { "_id" : ObjectId("52845534a40b66410f228f23"),
* "accessed" : NumberLong("1384818548903"),
* "maxIdle" : 1,
* "context" : { "::/contextA" : { "A" : "A",
* "__metadata__" : { "version" : NumberLong(2) }
* },
* "::/contextB" : { "B" : "B",
* "__metadata__" : { "version" : NumberLong(1) }
* }
* },
* "created" : NumberLong("1384818548903"),
* "expiry" : NumberLong("1384818549903"),
* "id" : "w01ijx2vnalgv1sqrpjwuirprp7",
* "valid" : true
* }
* </code>
* </p>
* <p>
* In MongoDB, the nesting level is indicated by "." separators for the key name. Thus to
* interact with a session attribute, the key is composed of:
* "context".unique_context_name.attribute_name
* Eg "context"."::/contextA"."A"
* </p>
*/
@ManagedObject("Mongo Session Manager")
public class MongoSessionManager extends NoSqlSessionManager
{
private static final Logger LOG = Log.getLogger(MongoSessionManager.class);
private final static Logger __log = Log.getLogger("org.eclipse.jetty.server.session");
/*
* strings used as keys or parts of keys in mongo
*/
/**
* Special attribute for a session that is context-specific
*/
private final static String __METADATA = "__metadata__";
/**
* Session id
*/
public final static String __ID = "id";
/**
* Time of session creation
*/
private final static String __CREATED = "created";
/**
* Whether or not session is valid
*/
public final static String __VALID = "valid";
/**
* Time at which session was invalidated
*/
public final static String __INVALIDATED = "invalidated";
/**
* Last access time of session
*/
public final static String __ACCESSED = "accessed";
/**
* Time this session will expire, based on last access time and maxIdle
*/
public final static String __EXPIRY = "expiry";
/**
* The max idle time of a session (smallest value across all contexts which has a session with the same id)
*/
public final static String __MAX_IDLE = "maxIdle";
/**
* Name of nested document field containing 1 sub document per context for which the session id is in use
*/
private final static String __CONTEXT = "context";
/**
* Special attribute per session per context, incremented each time attributes are modified
*/
public final static String __VERSION = __METADATA + ".version";
/**
* the context id is only set when this class has been started
*/
private String _contextId = null;
/**
* Access to MongoDB
*/
private DBCollection _dbSessions;
/**
* Utility value of 1 for a session version for this context
*/
private DBObject _version_1;
/* ------------------------------------------------------------ */
public MongoSessionManager() throws UnknownHostException, MongoException
{
}
/*------------------------------------------------------------ */
@Override
public void doStart() throws Exception
{
super.doStart();
String[] hosts = getContextHandler().getVirtualHosts();
if (hosts == null || hosts.length == 0)
hosts = new String[]
{ "::" }; // IPv6 equiv of 0.0.0.0
String contextPath = getContext().getContextPath();
if (contextPath == null || "".equals(contextPath))
{
contextPath = "*";
}
_contextId = createContextId(hosts,contextPath);
_version_1 = new BasicDBObject(getContextAttributeKey(__VERSION),1);
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.server.session.AbstractSessionManager#setSessionIdManager(org.eclipse.jetty.server.SessionIdManager)
*/
@Override
public void setSessionIdManager(SessionIdManager metaManager)
{
MongoSessionIdManager msim = (MongoSessionIdManager)metaManager;
_dbSessions=msim.getSessions();
super.setSessionIdManager(metaManager);
}
/* ------------------------------------------------------------ */
@Override
protected synchronized Object save(NoSqlSession session, Object version, boolean activateAfterSave)
{
try
{
__log.debug("MongoSessionManager:save session {}", session.getClusterId());
session.willPassivate();
// Form query for upsert
BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
// Form updates
BasicDBObject update = new BasicDBObject();
boolean upsert = false;
BasicDBObject sets = new BasicDBObject();
BasicDBObject unsets = new BasicDBObject();
// handle valid or invalid
if (session.isValid())
{
long expiry = (session.getMaxInactiveInterval() > 0?(session.getAccessed()+(1000L*getMaxInactiveInterval())):0);
__log.debug("MongoSessionManager: calculated expiry {} for session {}", expiry, session.getId());
// handle new or existing
if (version == null)
{
// New session
upsert = true;
version = new Long(1);
sets.put(__CREATED,session.getCreationTime());
sets.put(__VALID,true);
sets.put(getContextAttributeKey(__VERSION),version);
sets.put(__MAX_IDLE, getMaxInactiveInterval());
sets.put(__EXPIRY, expiry);
}
else
{
version = new Long(((Number)version).longValue() + 1);
update.put("$inc",_version_1);
//if max idle time and/or expiry is smaller for this context, then choose that for the whole session doc
BasicDBObject fields = new BasicDBObject();
fields.append(__MAX_IDLE, true);
fields.append(__EXPIRY, true);
DBObject o = _dbSessions.findOne(new BasicDBObject("id",session.getClusterId()), fields);
if (o != null)
{
Integer currentMaxIdle = (Integer)o.get(__MAX_IDLE);
Long currentExpiry = (Long)o.get(__EXPIRY);
if (currentMaxIdle != null && getMaxInactiveInterval() > 0 && getMaxInactiveInterval() < currentMaxIdle)
sets.put(__MAX_IDLE, getMaxInactiveInterval());
if (currentExpiry != null && expiry > 0 && expiry != currentExpiry)
sets.put(__EXPIRY, expiry);
}
}
sets.put(__ACCESSED,session.getAccessed());
Set<String> names = session.takeDirty();
if (isSaveAllAttributes() || upsert)
{
names.addAll(session.getNames()); // note dirty may include removed names
}
for (String name : names)
{
Object value = session.getAttribute(name);
if (value == null)
unsets.put(getContextKey() + "." + encodeName(name),1);
else
sets.put(getContextKey() + "." + encodeName(name),encodeName(value));
}
}
else
{
sets.put(__VALID,false);
sets.put(__INVALIDATED, System.currentTimeMillis());
unsets.put(getContextKey(),1);
}
// Do the upsert
if (!sets.isEmpty())
update.put("$set",sets);
if (!unsets.isEmpty())
update.put("$unset",unsets);
_dbSessions.update(key,update,upsert,false,WriteConcern.SAFE);
if (__log.isDebugEnabled())
__log.debug("MongoSessionManager:save:db.sessions.update( {}, {} )", key, update);
if (activateAfterSave)
session.didActivate();
return version;
}
catch (Exception e)
{
LOG.warn(e);
}
return null;
}
/*------------------------------------------------------------ */
@Override
protected Object refresh(NoSqlSession session, Object version)
{
__log.debug("MongoSessionManager:refresh session {}", session.getId());
// check if our in memory version is the same as what is on the disk
if (version != null)
{
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID,session.getClusterId()),_version_1);
if (o != null)
{
Object saved = getNestedValue(o, getContextAttributeKey(__VERSION));
if (saved != null && saved.equals(version))
{
__log.debug("MongoSessionManager:refresh not needed session {}", session.getId());
return version;
}
version = saved;
}
}
// If we are here, we have to load the object
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID,session.getClusterId()));
// If it doesn't exist, invalidate
if (o == null)
{
__log.debug("MongoSessionManager:refresh:marking session {} invalid, no object", session.getClusterId());
session.invalidate();
return null;
}
// If it has been flagged invalid, invalidate
Boolean valid = (Boolean)o.get(__VALID);
if (valid == null || !valid)
{
__log.debug("MongoSessionManager:refresh:marking session {} invalid, valid flag {}", session.getClusterId(), valid);
session.invalidate();
return null;
}
// We need to update the attributes. We will model this as a passivate,
// followed by bindings and then activation.
session.willPassivate();
try
{
DBObject attrs = (DBObject)getNestedValue(o,getContextKey());
//if disk version now has no attributes, get rid of them
if (attrs == null || attrs.keySet().size() == 0)
{
session.clearAttributes();
}
else
{
//iterate over the names of the attributes on the disk version, updating the value
for (String name : attrs.keySet())
{
//skip special metadata field which is not one of the session attributes
if (__METADATA.equals(name))
continue;
String attr = decodeName(name);
Object value = decodeValue(attrs.get(name));
//session does not already contain this attribute, so bind it
if (session.getAttribute(attr) == null)
{
session.doPutOrRemove(attr,value);
session.bindValue(attr,value);
}
else //session already contains this attribute, update its value
{
session.doPutOrRemove(attr,value);
}
}
// cleanup, remove values from session, that don't exist in data anymore:
for (String str : session.getNames())
{
if (!attrs.keySet().contains(encodeName(str)))
{
session.doPutOrRemove(str,null);
session.unbindValue(str,session.getAttribute(str));
}
}
}
/*
* We are refreshing so we should update the last accessed time.
*/
BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
BasicDBObject sets = new BasicDBObject();
// Form updates
BasicDBObject update = new BasicDBObject();
sets.put(__ACCESSED,System.currentTimeMillis());
// Do the upsert
if (!sets.isEmpty())
{
update.put("$set",sets);
}
_dbSessions.update(key,update,false,false,WriteConcern.SAFE);
session.didActivate();
return version;
}
catch (Exception e)
{
LOG.warn(e);
}
return null;
}
/*------------------------------------------------------------ */
@Override
protected synchronized NoSqlSession loadSession(String clusterId)
{
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID,clusterId));
__log.debug("MongoSessionManager:id={} loaded={}", clusterId, o);
if (o == null)
return null;
Boolean valid = (Boolean)o.get(__VALID);
__log.debug("MongoSessionManager:id={} valid={}", clusterId, valid);
if (valid == null || !valid)
return null;
try
{
Object version = o.get(getContextAttributeKey(__VERSION));
Long created = (Long)o.get(__CREATED);
Long accessed = (Long)o.get(__ACCESSED);
NoSqlSession session = null;
// get the session for the context
DBObject attrs = (DBObject)getNestedValue(o,getContextKey());
__log.debug("MongoSessionManager:attrs {}", attrs);
if (attrs != null)
{
__log.debug("MongoSessionManager: session {} present for context {}", clusterId, getContextKey());
//only load a session if it exists for this context
session = new NoSqlSession(this,created,accessed,clusterId,version);
for (String name : attrs.keySet())
{
//skip special metadata attribute which is not one of the actual session attributes
if ( __METADATA.equals(name) )
continue;
String attr = decodeName(name);
Object value = decodeValue(attrs.get(name));
session.doPutOrRemove(attr,value);
session.bindValue(attr,value);
}
session.didActivate();
}
else
__log.debug("MongoSessionManager: session {} not present for context {}",clusterId, getContextKey());
return session;
}
catch (Exception e)
{
LOG.warn(e);
}
return null;
}
/*------------------------------------------------------------ */
/**
* Remove the per-context sub document for this session id.
* @see org.eclipse.jetty.nosql.NoSqlSessionManager#remove(org.eclipse.jetty.nosql.NoSqlSession)
*/
@Override
protected boolean remove(NoSqlSession session)
{
__log.debug("MongoSessionManager:remove:session {} for context {}",session.getClusterId(), getContextKey());
/*
* Check if the session exists and if it does remove the context
* associated with this session
*/
BasicDBObject key = new BasicDBObject(__ID,session.getClusterId());
DBObject o = _dbSessions.findOne(key,_version_1);
if (o != null)
{
BasicDBObject remove = new BasicDBObject();
BasicDBObject unsets = new BasicDBObject();
unsets.put(getContextKey(),1);
remove.put("$unset",unsets);
_dbSessions.update(key,remove,false,false,WriteConcern.SAFE);
return true;
}
else
{
return false;
}
}
/**
* @see org.eclipse.jetty.nosql.NoSqlSessionManager#expire(java.lang.String)
*/
@Override
protected void expire (String idInCluster)
{
__log.debug("MongoSessionManager:expire session {} ", idInCluster);
//Expire the session for this context
super.expire(idInCluster);
//If the outer session document has not already been marked invalid, do so.
DBObject validKey = new BasicDBObject(__VALID, true);
DBObject o = _dbSessions.findOne(new BasicDBObject(__ID,idInCluster), validKey);
if (o != null && (Boolean)o.get(__VALID))
{
BasicDBObject update = new BasicDBObject();
BasicDBObject sets = new BasicDBObject();
sets.put(__VALID,false);
sets.put(__INVALIDATED, System.currentTimeMillis());
update.put("$set",sets);
BasicDBObject key = new BasicDBObject(__ID,idInCluster);
_dbSessions.update(key,update,false,false,WriteConcern.SAFE);
}
}
/*------------------------------------------------------------ */
/**
* Change the session id. Note that this will change the session id for all contexts for which the session id is in use.
* @see org.eclipse.jetty.nosql.NoSqlSessionManager#update(org.eclipse.jetty.nosql.NoSqlSession, java.lang.String, java.lang.String)
*/
@Override
protected void update(NoSqlSession session, String newClusterId, String newNodeId) throws Exception
{
BasicDBObject key = new BasicDBObject(__ID, session.getClusterId());
BasicDBObject sets = new BasicDBObject();
BasicDBObject update = new BasicDBObject(__ID, newClusterId);
sets.put("$set", update);
_dbSessions.update(key, sets, false, false,WriteConcern.SAFE);
}
/*------------------------------------------------------------ */
protected String encodeName(String name)
{
return name.replace("%","%25").replace(".","%2E");
}
/*------------------------------------------------------------ */
protected String decodeName(String name)
{
return name.replace("%2E",".").replace("%25","%");
}
/*------------------------------------------------------------ */
protected Object encodeName(Object value) throws IOException
{
if (value instanceof Number || value instanceof String || value instanceof Boolean || value instanceof Date)
{
return value;
}
else if (value.getClass().equals(HashMap.class))
{
BasicDBObject o = new BasicDBObject();
for (Map.Entry<?, ?> entry : ((Map<?, ?>)value).entrySet())
{
if (!(entry.getKey() instanceof String))
{
o = null;
break;
}
o.append(encodeName(entry.getKey().toString()),encodeName(entry.getValue()));
}
if (o != null)
return o;
}
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.reset();
out.writeUnshared(value);
out.flush();
return bout.toByteArray();
}
/*------------------------------------------------------------ */
protected Object decodeValue(final Object valueToDecode) throws IOException, ClassNotFoundException
{
if (valueToDecode == null || valueToDecode instanceof Number || valueToDecode instanceof String || valueToDecode instanceof Boolean || valueToDecode instanceof Date)
{
return valueToDecode;
}
else if (valueToDecode instanceof byte[])
{
final byte[] decodeObject = (byte[])valueToDecode;
final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject);
final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais);
return objectInputStream.readUnshared();
}
else if (valueToDecode instanceof DBObject)
{
Map<String, Object> map = new HashMap<String, Object>();
for (String name : ((DBObject)valueToDecode).keySet())
{
String attr = decodeName(name);
map.put(attr,decodeValue(((DBObject)valueToDecode).get(name)));
}
return map;
}
else
{
throw new IllegalStateException(valueToDecode.getClass().toString());
}
}
/*------------------------------------------------------------ */
private String getContextKey()
{
return __CONTEXT + "." + _contextId;
}
/*------------------------------------------------------------ */
/** Get a dot separated key for
* @param key
* @return
*/
private String getContextAttributeKey(String attr)
{
return getContextKey()+ "." + attr;
}
/*------------------------------------------------------------ */
@ManagedOperation(value="purge invalid sessions in the session store based on normal criteria", impact="ACTION")
public void purge()
{
((MongoSessionIdManager)_sessionIdManager).purge();
}
/*------------------------------------------------------------ */
@ManagedOperation(value="full purge of invalid sessions in the session store", impact="ACTION")
public void purgeFully()
{
((MongoSessionIdManager)_sessionIdManager).purgeFully();
}
/*------------------------------------------------------------ */
@ManagedOperation(value="scavenge sessions known to this manager", impact="ACTION")
public void scavenge()
{
((MongoSessionIdManager)_sessionIdManager).scavenge();
}
/*------------------------------------------------------------ */
@ManagedOperation(value="scanvenge all sessions", impact="ACTION")
public void scavengeFully()
{
((MongoSessionIdManager)_sessionIdManager).scavengeFully();
}
/*------------------------------------------------------------ */
/**
* returns the total number of session objects in the session store
*
* the count() operation itself is optimized to perform on the server side
* and avoid loading to client side.
*/
@ManagedAttribute("total number of known sessions in the store")
public long getSessionStoreCount()
{
return _dbSessions.find().count();
}
/*------------------------------------------------------------ */
/**
* MongoDB keys are . delimited for nesting so .'s are protected characters
*
* @param virtualHosts
* @param contextPath
* @return
*/
private String createContextId(String[] virtualHosts, String contextPath)
{
String contextId = virtualHosts[0] + contextPath;
contextId.replace('/', '_');
contextId.replace('.','_');
contextId.replace('\\','_');
return contextId;
}
/*------------------------------------------------------------ */
/**
* Dig through a given dbObject for the nested value
*/
private Object getNestedValue(DBObject dbObject, String nestedKey)
{
String[] keyChain = nestedKey.split("\\.");
DBObject temp = dbObject;
for (int i = 0; i < keyChain.length - 1; ++i)
{
temp = (DBObject)temp.get(keyChain[i]);
if ( temp == null )
{
return null;
}
}
return temp.get(keyChain[keyChain.length - 1]);
}
/*------------------------------------------------------------ */
/**
* ClassLoadingObjectInputStream
*
*
*/
protected class ClassLoadingObjectInputStream extends ObjectInputStream
{
public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
{
super(in);
}
public ClassLoadingObjectInputStream () throws IOException
{
super();
}
@Override
public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
{
try
{
return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
}
catch (ClassNotFoundException e)
{
return super.resolveClass(cl);
}
}
}
}