package org.eclipse.jetty.nosql.mongodb;
//========================================================================
//Copyright (c) 2011 Intalio, Inc.
//------------------------------------------------------------------------
//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.
//========================================================================
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SessionManager;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.session.AbstractSessionIdManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
/**
* Based partially on the jdbc session id manager...
*
* Theory is that we really only need the session id manager for the local
* instance so we have something to scavenge on, namely the list of known ids
*
* this class has a timer that runs at the scavenge delay that runs a query
* for all id's known to this node and that have and old accessed value greater
* then the scavengeDelay.
*
* these found sessions are then run through the invalidateAll(id) method that
* is a bit hinky but is supposed to notify all handlers this id is now DOA and
* ought to be cleaned up. this ought to result in a save operation on the session
* that will change the valid field to false (this conjecture is unvalidated atm)
*/
public class MongoSessionIdManager extends AbstractSessionIdManager
{
private final static Logger __log = Log.getLogger("org.eclipse.jetty.server.session");
final static DBObject __version_1 = new BasicDBObject(MongoSessionManager.__VERSION,1);
final static DBObject __valid_false = new BasicDBObject(MongoSessionManager.__VALID,false);
final static DBObject __valid_true = new BasicDBObject(MongoSessionManager.__VALID,true);
final DBCollection _sessions;
protected Server _server;
private Timer _scavengeTimer;
private Timer _purgeTimer;
private TimerTask _scavengerTask;
private TimerTask _purgeTask;
private long _scavengeDelay = 30 * 60 * 1000; // every 30 minutes
private long _scavengePeriod = 10 * 6 * 1000; // wait at least 10 minutes
/**
* purge process is enabled by default
*/
private boolean _purge = true;
/**
* purge process would run daily by default
*/
private long _purgeDelay = 24 * 60 * 60 * 1000; // every day
/**
* how long do you want to persist sessions that are no longer
* valid before removing them completely
*/
private long _purgeInvalidAge = 24 * 60 * 60 * 1000; // default 1 day
/**
* how long do you want to leave sessions that are still valid before
* assuming they are dead and removing them
*/
private long _purgeValidAge = 7 * 24 * 60 * 60 * 1000; // default 1 week
/**
* the collection of session ids known to this manager
*
* TODO consider if this ought to be concurrent or not
*/
protected final Set<String> _sessionsIds = new HashSet<String>();
/* ------------------------------------------------------------ */
public MongoSessionIdManager(Server server) throws UnknownHostException, MongoException
{
this(server, new Mongo().getDB("HttpSessions").getCollection("sessions"));
}
/* ------------------------------------------------------------ */
public MongoSessionIdManager(Server server, DBCollection sessions)
{
super(new Random());
_server = server;
_sessions = sessions;
_sessions.ensureIndex(
BasicDBObjectBuilder.start().add("id",1).get(),
BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
_sessions.ensureIndex(
BasicDBObjectBuilder.start().add("id",1).add("version",1).get(),
BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
}
/* ------------------------------------------------------------ */
/**
* Scavenge is a process that periodically checks the tracked session
* ids of this given instance of the session id manager to see if they
* are past the point of expiration.
*/
protected void scavenge()
{
__log.debug("SessionIdManager:scavenge:called with delay" + _scavengeDelay);
synchronized (_sessionsIds)
{
/*
* run a query returning results that:
* - are in the known list of sessionIds
* - have an accessed time less then current time - the scavenger period
*
* we limit the query to return just the __ID so we are not sucking back full sessions
*/
BasicDBObject query = new BasicDBObject();
query.put(MongoSessionManager.__ID,new BasicDBObject("$in", _sessionsIds ));
query.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _scavengeDelay));
DBCursor checkSessions = _sessions.find(query, new BasicDBObject(MongoSessionManager.__ID, 1));
for ( DBObject session : checkSessions )
{
__log.debug("SessionIdManager:scavenge: invalidating " + (String)session.get(MongoSessionManager.__ID));
invalidateAll((String)session.get(MongoSessionManager.__ID));
}
}
}
/* ------------------------------------------------------------ */
/**
* ScavengeFully is a process that periodically checks the tracked session
* ids of this given instance of the session id manager to see if they
* are past the point of expiration.
*
* NOTE: this is potentially devastating and may lead to serious session
* coherence issues, not to be used in a running cluster
*/
protected void scavengeFully()
{
__log.debug("SessionIdManager:scavengeFully");
DBCursor checkSessions = _sessions.find();
for (DBObject session : checkSessions)
{
invalidateAll((String)session.get(MongoSessionManager.__ID));
}
}
/* ------------------------------------------------------------ */
/**
* Purge is a process that cleans the mongodb cluster of old sessions that are no
* longer valid.
*
* There are two checks being done here:
*
* - if the accessed time is older then the current time minus the purge invalid age
* and it is no longer valid then remove that session
* - if the accessed time is older then the current time minus the purge valid age
* then we consider this a lost record and remove it
*
* NOTE: if your system supports long lived sessions then the purge valid age should be
* set to zero so the check is skipped.
*
* The second check was added to catch sessions that were being managed on machines
* that might have crashed without marking their sessions as 'valid=false'
*/
protected void purge()
{
BasicDBObject invalidQuery = new BasicDBObject();
invalidQuery.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _purgeInvalidAge));
invalidQuery.put(MongoSessionManager.__VALID, __valid_false);
DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
for (DBObject session : oldSessions)
{
String id = (String)session.get("id");
__log.debug("MongoSessionIdManager:purging invalid " + id);
_sessions.remove(session);
}
if (_purgeValidAge != 0)
{
BasicDBObject validQuery = new BasicDBObject();
validQuery.put(MongoSessionManager.__ACCESSED,new BasicDBObject("$lt",System.currentTimeMillis() - _purgeValidAge));
validQuery.put(MongoSessionManager.__VALID, __valid_false);
oldSessions = _sessions.find(invalidQuery,new BasicDBObject(MongoSessionManager.__ID,1));
for (DBObject session : oldSessions)
{
String id = (String)session.get(MongoSessionManager.__ID);
__log.debug("MongoSessionIdManager:purging valid " + id);
_sessions.remove(session);
}
}
}
/* ------------------------------------------------------------ */
/**
* Purge is a process that cleans the mongodb cluster of old sessions that are no
* longer valid.
*
*/
protected void purgeFully()
{
BasicDBObject invalidQuery = new BasicDBObject();
invalidQuery.put(MongoSessionManager.__VALID, false);
DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
for (DBObject session : oldSessions)
{
String id = (String)session.get(MongoSessionManager.__ID);
__log.debug("MongoSessionIdManager:purging invalid " + id);
_sessions.remove(session);
}
}
/* ------------------------------------------------------------ */
public DBCollection getSessions()
{
return _sessions;
}
/* ------------------------------------------------------------ */
public boolean isPurgeEnabled()
{
return _purge;
}
/* ------------------------------------------------------------ */
public void setPurge(boolean purge)
{
this._purge = purge;
}
/* ------------------------------------------------------------ */
/**
* sets the scavengeDelay
*/
public void setScavengeDelay(long scavengeDelay)
{
this._scavengeDelay = scavengeDelay;
}
/* ------------------------------------------------------------ */
public void setScavengePeriod(long scavengePeriod)
{
this._scavengePeriod = scavengePeriod;
}
/* ------------------------------------------------------------ */
public void setPurgeDelay(long purgeDelay)
{
if ( isRunning() )
{
throw new IllegalStateException();
}
this._purgeDelay = purgeDelay;
}
/* ------------------------------------------------------------ */
public long getPurgeInvalidAge()
{
return _purgeInvalidAge;
}
/* ------------------------------------------------------------ */
/**
* sets how old a session is to be persisted past the point it is
* no longer valid
*/
public void setPurgeInvalidAge(long purgeValidAge)
{
this._purgeInvalidAge = purgeValidAge;
}
/* ------------------------------------------------------------ */
public long getPurgeValidAge()
{
return _purgeValidAge;
}
/* ------------------------------------------------------------ */
/**
* sets how old a session is to be persist past the point it is
* considered no longer viable and should be removed
*
* NOTE: set this value to 0 to disable purging of valid sessions
*/
public void setPurgeValidAge(long purgeValidAge)
{
this._purgeValidAge = purgeValidAge;
}
/* ------------------------------------------------------------ */
@Override
protected void doStart() throws Exception
{
__log.debug("MongoSessionIdManager:starting");
/*
* setup the scavenger thread
*/
if (_scavengeDelay > 0)
{
_scavengeTimer = new Timer("MongoSessionIdScavenger",true);
synchronized (this)
{
if (_scavengerTask != null)
{
_scavengerTask.cancel();
}
_scavengerTask = new TimerTask()
{
@Override
public void run()
{
scavenge();
}
};
_scavengeTimer.schedule(_scavengerTask,_scavengeDelay,_scavengePeriod);
}
}
/*
* if purging is enabled, setup the purge thread
*/
if ( _purge )
{
_purgeTimer = new Timer("MongoSessionPurger", true);
synchronized (this)
{
if (_purgeTask != null)
{
_purgeTask.cancel();
}
_purgeTask = new TimerTask()
{
@Override
public void run()
{
purge();
}
};
_purgeTimer.schedule(_purgeTask,_purgeDelay);
}
}
}
/* ------------------------------------------------------------ */
@Override
protected void doStop() throws Exception
{
if (_scavengeTimer != null)
{
_scavengeTimer.cancel();
_scavengeTimer = null;
}
if (_purgeTimer != null)
{
_purgeTimer.cancel();
_purgeTimer = null;
}
super.doStop();
}
/* ------------------------------------------------------------ */
/**
* is the session id known to mongo, and is it valid
*/
public boolean idInUse(String sessionId)
{
/*
* optimize this query to only return the valid variable
*/
DBObject o = _sessions.findOne(new BasicDBObject("id",sessionId), __valid_true);
if ( o != null )
{
Boolean valid = (Boolean)o.get(MongoSessionManager.__VALID);
if ( valid == null )
{
return false;
}
return valid;
}
return false;
}
/* ------------------------------------------------------------ */
public void addSession(HttpSession session)
{
if (session == null)
{
return;
}
/*
* already a part of the index in mongo...
*/
__log.debug("MongoSessionIdManager:addSession:" + session.getId());
synchronized (_sessionsIds)
{
_sessionsIds.add(session.getId());
}
}
/* ------------------------------------------------------------ */
public void removeSession(HttpSession session)
{
if (session == null)
{
return;
}
synchronized (_sessionsIds)
{
_sessionsIds.remove(session.getId());
}
}
/* ------------------------------------------------------------ */
public void invalidateAll(String sessionId)
{
synchronized (_sessionsIds)
{
_sessionsIds.remove(sessionId);
//tell all contexts that may have a session object with this id to
//get rid of them
Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
for (int i=0; contexts!=null && i<contexts.length; i++)
{
SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
if (sessionHandler != null)
{
SessionManager manager = sessionHandler.getSessionManager();
if (manager != null && manager instanceof MongoSessionManager)
{
((MongoSessionManager)manager).invalidateSession(sessionId);
}
}
}
}
}
/* ------------------------------------------------------------ */
// TODO not sure if this is correct
public String getClusterId(String nodeId)
{
int dot=nodeId.lastIndexOf('.');
return (dot>0)?nodeId.substring(0,dot):nodeId;
}
/* ------------------------------------------------------------ */
// TODO not sure if this is correct
public String getNodeId(String clusterId, HttpServletRequest request)
{
if (_workerName!=null)
return clusterId+'.'+_workerName;
return clusterId;
}
}