Java tutorial
// // ======================================================================== // Copyright (c) 1995-2015 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; private Scheduler _scheduler; private boolean _ownScheduler; private Scheduler.Task _scavengerTask; private 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(); } 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; _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()); // 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 _sessions.ensureIndex( BasicDBObjectBuilder.start().add(MongoSessionManager.__VALID, 1) .add(MongoSessionManager.__ACCESSED, 1).get(), BasicDBObjectBuilder.start().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)); query.put(MongoSessionManager.__EXPIRY, new BasicDBObject("$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 <= 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); } } } } /* ------------------------------------------------------------ */ @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)); } } } } }