blob: 9f95eb97593b70bead88ea968318f877bc145445 [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.net.UnknownHostException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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.ConcurrentHashSet;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
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 JDBCSessionIdManager.
* <p>
* 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
* <p>
* This class has a timer that runs a periodic scavenger thread to query
* for all id's known to this node whose precalculated expiry time has passed.
* <p>
* 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 static long __defaultScavengePeriod = 30 * 60 * 1000; // every 30 minutes
final DBCollection _sessions;
protected Server _server;
protected Scheduler _scheduler;
protected boolean _ownScheduler;
protected Scheduler.Task _scavengerTask;
protected Scheduler.Task _purgerTask;
private long _scavengePeriod = __defaultScavengePeriod;
/**
* 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
*/
protected final Set<String> _sessionsIds = new ConcurrentHashSet<>();
/**
* The maximum number of items to return from a purge query.
*/
private int _purgeLimit = 0;
private int _scavengeBlockSize;
/**
* Scavenger
*
*/
protected class Scavenger implements Runnable
{
@Override
public void run()
{
try
{
scavenge();
idle();
}
finally
{
if (_scheduler != null && _scheduler.isRunning())
_scavengerTask = _scheduler.schedule(this, _scavengePeriod, TimeUnit.MILLISECONDS);
}
}
}
/**
* Purger
*
*/
protected class Purger implements Runnable
{
@Override
public void run()
{
try
{
purge();
}
finally
{
if (_scheduler != null && _scheduler.isRunning())
_purgerTask = _scheduler.schedule(this, _purgeDelay, TimeUnit.MILLISECONDS);
}
}
}
/* ------------------------------------------------------------ */
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;
DBObject idKey = BasicDBObjectBuilder.start().add("id", 1).get();
_sessions.createIndex(idKey,
BasicDBObjectBuilder.start()
.add("name", "id_1")
.add("ns", _sessions.getFullName())
.add("sparse", false)
.add("unique", true)
.get());
DBObject versionKey = BasicDBObjectBuilder.start().add("id", 1).add("version", 1).get();
_sessions.createIndex(versionKey, BasicDBObjectBuilder.start()
.add("name", "id_1_version_1")
.add("ns", _sessions.getFullName())
.add("sparse", false)
.add("unique", true)
.get());
// index our accessed and valid fields so that purges are faster, note that the "valid" field is first
// so that we can take advantage of index prefixes
// http://docs.mongodb.org/manual/core/index-compound/#compound-index-prefix
DBObject validKey = BasicDBObjectBuilder.start().add(MongoSessionManager.__VALID, 1).add(MongoSessionManager.__ACCESSED, 1).get();
_sessions.createIndex(validKey,
BasicDBObjectBuilder.start()
.add("name", MongoSessionManager.__VALID+"_1_"+MongoSessionManager.__ACCESSED+"_1")
.add("ns", _sessions.getFullName())
.add("sparse", false)
.add("background", true).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()
{
long now = System.currentTimeMillis();
__log.debug("SessionIdManager:scavenge:at {}", now);
/*
* run a query returning results that:
* - are in the known list of sessionIds
* - the expiry time has passed
*
* we limit the query to return just the __ID so we are not sucking back full sessions
*
* break scavenge query into blocks for faster mongo queries
*/
Set<String> block = new HashSet<String>();
Iterator<String> itor = _sessionsIds.iterator();
while (itor.hasNext())
{
block.add(itor.next());
if ((_scavengeBlockSize > 0) && (block.size() == _scavengeBlockSize))
{
//got a block
scavengeBlock (now, block);
//reset for next run
block.clear();
}
}
//non evenly divisble block size, or doing it all at once
if (!block.isEmpty())
scavengeBlock(now, block);
}
/* ------------------------------------------------------------ */
/**
* Check a block of session ids for expiry and thus scavenge.
*
* @param atTime purge at time
* @param ids set of session ids
*/
protected void scavengeBlock (long atTime, Set<String> ids)
{
if (ids == null)
return;
BasicDBObject query = new BasicDBObject();
query.put(MongoSessionManager.__ID,new BasicDBObject("$in", ids ));
query.put(MongoSessionManager.__EXPIRY, new BasicDBObject("$gt", 0).append("$lt", atTime));
DBCursor checkSessions = _sessions.find(query, new BasicDBObject(MongoSessionManager.__ID, 1));
for ( DBObject session : checkSessions )
{
__log.debug("SessionIdManager:scavenge: expiring session {}", (String)session.get(MongoSessionManager.__ID));
expireAll((String)session.get(MongoSessionManager.__ID));
}
}
/* ------------------------------------------------------------ */
/**
* ScavengeFully will expire all sessions. In most circumstances
* you should never need to call this method.
*
* <b>USE WITH CAUTION</b>
*/
protected void scavengeFully()
{
__log.debug("SessionIdManager:scavengeFully");
DBCursor checkSessions = _sessions.find();
for (DBObject session : checkSessions)
{
expireAll((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 than 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()
{
__log.debug("PURGING");
BasicDBObject invalidQuery = new BasicDBObject();
invalidQuery.put(MongoSessionManager.__VALID, false);
invalidQuery.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _purgeInvalidAge));
DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
if (_purgeLimit > 0)
{
oldSessions.limit(_purgeLimit);
}
for (DBObject session : oldSessions)
{
String id = (String)session.get("id");
__log.debug("MongoSessionIdManager:purging invalid session {}", id);
_sessions.remove(session);
}
if (_purgeValidAge != 0)
{
BasicDBObject validQuery = new BasicDBObject();
validQuery.put(MongoSessionManager.__VALID, true);
validQuery.put(MongoSessionManager.__ACCESSED,new BasicDBObject("$lt",System.currentTimeMillis() - _purgeValidAge));
oldSessions = _sessions.find(validQuery,new BasicDBObject(MongoSessionManager.__ID,1));
if (_purgeLimit > 0)
{
oldSessions.limit(_purgeLimit);
}
for (DBObject session : oldSessions)
{
String id = (String)session.get(MongoSessionManager.__ID);
__log.debug("MongoSessionIdManager:purging valid session {}", 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 session {}", id);
_sessions.remove(session);
}
}
/* ------------------------------------------------------------ */
public DBCollection getSessions()
{
return _sessions;
}
/* ------------------------------------------------------------ */
public boolean isPurgeEnabled()
{
return _purge;
}
/* ------------------------------------------------------------ */
public void setPurge(boolean purge)
{
this._purge = purge;
}
/* ------------------------------------------------------------ */
/**
* The period in seconds between scavenge checks.
*
* @param scavengePeriod the scavenge period in seconds
*/
public void setScavengePeriod(long scavengePeriod)
{
if (scavengePeriod <= 0)
_scavengePeriod = __defaultScavengePeriod;
else
_scavengePeriod = TimeUnit.SECONDS.toMillis(scavengePeriod);
}
/* ------------------------------------------------------------ */
/** When scavenging, the max number of session ids in the query.
*
* @param size the scavenge block size
*/
public void setScavengeBlockSize (int size)
{
_scavengeBlockSize = size;
}
/* ------------------------------------------------------------ */
public int getScavengeBlockSize ()
{
return _scavengeBlockSize;
}
/* ------------------------------------------------------------ */
/**
* The maximum number of items to return from a purge query. If &lt;= 0 there is no limit. Defaults to 0
*
* @param purgeLimit the purge limit
*/
public void setPurgeLimit(int purgeLimit)
{
_purgeLimit = purgeLimit;
}
/* ------------------------------------------------------------ */
public int getPurgeLimit()
{
return _purgeLimit;
}
/* ------------------------------------------------------------ */
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
* @param purgeValidAge the purge valid age
*/
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
* @param purgeValidAge the purge valid age
*/
public void setPurgeValidAge(long purgeValidAge)
{
this._purgeValidAge = purgeValidAge;
}
/* ------------------------------------------------------------ */
@Override
protected void doStart() throws Exception
{
__log.debug("MongoSessionIdManager:starting");
synchronized (this)
{
//try and use a common scheduler, fallback to own
_scheduler =_server.getBean(Scheduler.class);
if (_scheduler == null)
{
_scheduler = new ScheduledExecutorScheduler();
_ownScheduler = true;
_scheduler.start();
}
else if (!_scheduler.isStarted())
throw new IllegalStateException("Shared scheduler not started");
//setup the scavenger thread
if (_scavengePeriod > 0)
{
if (_scavengerTask != null)
{
_scavengerTask.cancel();
_scavengerTask = null;
}
_scavengerTask = _scheduler.schedule(new Scavenger(), _scavengePeriod, TimeUnit.MILLISECONDS);
}
else if (__log.isDebugEnabled())
__log.debug("Scavenger disabled");
//if purging is enabled, setup the purge thread
if ( _purge )
{
if (_purgerTask != null)
{
_purgerTask.cancel();
_purgerTask = null;
}
_purgerTask = _scheduler.schedule(new Purger(), _purgeDelay, TimeUnit.MILLISECONDS);
}
else if (__log.isDebugEnabled())
__log.debug("Purger disabled");
}
}
/* ------------------------------------------------------------ */
@Override
protected void doStop() throws Exception
{
synchronized (this)
{
if (_scavengerTask != null)
{
_scavengerTask.cancel();
_scavengerTask = null;
}
if (_purgerTask != null)
{
_purgerTask.cancel();
_purgerTask = null;
}
if (_ownScheduler && _scheduler != null)
{
_scheduler.stop();
_scheduler = null;
}
}
super.doStop();
}
/* ------------------------------------------------------------ */
/**
* Searches database to find if the session id known to mongo, and is it valid
*/
@Override
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;
}
/* ------------------------------------------------------------ */
@Override
public void addSession(HttpSession session)
{
if (session == null)
{
return;
}
/*
* already a part of the index in mongo...
*/
__log.debug("MongoSessionIdManager:addSession {}", session.getId());
_sessionsIds.add(session.getId());
}
/* ------------------------------------------------------------ */
@Override
public void removeSession(HttpSession session)
{
if (session == null)
{
return;
}
_sessionsIds.remove(session.getId());
}
/* ------------------------------------------------------------ */
/** Remove the session id from the list of in-use sessions.
* Inform all other known contexts that sessions with the same id should be
* invalidated.
* @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
*/
@Override
public void invalidateAll(String sessionId)
{
_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 = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
if (sessionHandler != null)
{
SessionManager manager = sessionHandler.getSessionManager();
if (manager != null && manager instanceof MongoSessionManager)
{
((MongoSessionManager)manager).invalidateSession(sessionId);
}
}
}
}
/* ------------------------------------------------------------ */
/**
* Expire this session for all contexts that are sharing the session
* id.
* @param sessionId the session id
*/
public void expireAll (String sessionId)
{
_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 = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
if (sessionHandler != null)
{
SessionManager manager = sessionHandler.getSessionManager();
if (manager != null && manager instanceof MongoSessionManager)
{
((MongoSessionManager)manager).expire(sessionId);
}
}
}
}
public void idle ()
{
//tell all contexts to passivate out idle sessions
Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
for (int i=0; contexts!=null && i<contexts.length; i++)
{
SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
if (sessionHandler != null)
{
SessionManager manager = sessionHandler.getSessionManager();
if (manager != null && manager instanceof MongoSessionManager)
{
((MongoSessionManager)manager).idle();
}
}
}
}
/* ------------------------------------------------------------ */
@Override
public void renewSessionId(String oldClusterId, String oldNodeId, HttpServletRequest request)
{
//generate a new id
String newClusterId = newSessionId(request.hashCode());
_sessionsIds.remove(oldClusterId);//remove the old one from the list
_sessionsIds.add(newClusterId); //add in the new session id to the list
//tell all contexts to update the id
Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
for (int i=0; contexts!=null && i<contexts.length; i++)
{
SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
if (sessionHandler != null)
{
SessionManager manager = sessionHandler.getSessionManager();
if (manager != null && manager instanceof MongoSessionManager)
{
((MongoSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
}
}
}
}
}