View Javadoc

1   package org.eclipse.jetty.nosql.mongodb;
2   //========================================================================
3   //Copyright (c) 2011 Intalio, Inc.
4   //------------------------------------------------------------------------
5   //All rights reserved. This program and the accompanying materials
6   //are made available under the terms of the Eclipse Public License v1.0
7   //and Apache License v2.0 which accompanies this distribution.
8   //The Eclipse Public License is available at
9   //http://www.eclipse.org/legal/epl-v10.html
10  //The Apache License v2.0 is available at
11  //http://www.opensource.org/licenses/apache2.0.php
12  //You may elect to redistribute this code under either of these licenses.
13  //========================================================================
14  
15  
16  import java.net.UnknownHostException;
17  import java.util.HashSet;
18  import java.util.Random;
19  import java.util.Set;
20  import java.util.Timer;
21  import java.util.TimerTask;
22  
23  import javax.servlet.http.HttpServletRequest;
24  import javax.servlet.http.HttpSession;
25  
26  import org.eclipse.jetty.server.Handler;
27  import org.eclipse.jetty.server.Server;
28  import org.eclipse.jetty.server.SessionManager;
29  import org.eclipse.jetty.server.handler.ContextHandler;
30  import org.eclipse.jetty.server.session.AbstractSessionIdManager;
31  import org.eclipse.jetty.server.session.SessionHandler;
32  import org.eclipse.jetty.util.log.Log;
33  import org.eclipse.jetty.util.log.Logger;
34  
35  import com.mongodb.BasicDBObject;
36  import com.mongodb.BasicDBObjectBuilder;
37  import com.mongodb.DBCollection;
38  import com.mongodb.DBCursor;
39  import com.mongodb.DBObject;
40  import com.mongodb.Mongo;
41  import com.mongodb.MongoException;
42  
43  /**
44   * Based partially on the jdbc session id manager...
45   *
46   * Theory is that we really only need the session id manager for the local 
47   * instance so we have something to scavenge on, namely the list of known ids
48   * 
49   * this class has a timer that runs at the scavenge delay that runs a query
50   *  for all id's known to this node and that have and old accessed value greater
51   *  then the scavengeDelay.
52   *  
53   * these found sessions are then run through the invalidateAll(id) method that 
54   * is a bit hinky but is supposed to notify all handlers this id is now DOA and 
55   * ought to be cleaned up.  this ought to result in a save operation on the session
56   * that will change the valid field to false (this conjecture is unvalidated atm)
57   */
58  public class MongoSessionIdManager extends AbstractSessionIdManager
59  {
60      private final static Logger __log = Log.getLogger("org.eclipse.jetty.server.session");
61  
62      final static DBObject __version_1 = new BasicDBObject(MongoSessionManager.__VERSION,1);
63      final static DBObject __valid_false = new BasicDBObject(MongoSessionManager.__VALID,false);
64  
65      final DBCollection _sessions;
66      protected Server _server;
67      private Timer _scavengeTimer;
68      private Timer _purgeTimer;
69      private TimerTask _scavengerTask;
70      private TimerTask _purgeTask;
71  
72      
73      
74      private long _scavengeDelay = 30 * 60 * 1000; // every 30 minutes
75      private long _scavengePeriod = 10 * 6 * 1000; // wait at least 10 minutes
76      
77  
78      /** 
79       * purge process is enabled by default
80       */
81      private boolean _purge = true;
82  
83      /**
84       * purge process would run daily by default
85       */
86      private long _purgeDelay = 24 * 60 * 60 * 1000; // every day
87      
88      /**
89       * how long do you want to persist sessions that are no longer
90       * valid before removing them completely
91       */
92      private long _purgeInvalidAge = 24 * 60 * 60 * 1000; // default 1 day
93  
94      /**
95       * how long do you want to leave sessions that are still valid before
96       * assuming they are dead and removing them
97       */
98      private long _purgeValidAge = 7 * 24 * 60 * 60 * 1000; // default 1 week
99  
100     
101     /**
102      * the collection of session ids known to this manager
103      * 
104      * TODO consider if this ought to be concurrent or not
105      */
106     protected final Set<String> _sessionsIds = new HashSet<String>();
107     
108 
109     /* ------------------------------------------------------------ */
110     public MongoSessionIdManager(Server server) throws UnknownHostException, MongoException
111     {
112         this(server, new Mongo().getDB("HttpSessions").getCollection("sessions"));
113     }
114 
115     /* ------------------------------------------------------------ */
116     public MongoSessionIdManager(Server server, DBCollection sessions)
117     {
118         super(new Random());
119         
120         _server = server;
121         _sessions = sessions;
122 
123         _sessions.ensureIndex(
124                 BasicDBObjectBuilder.start().add("id",1).get(),
125                 BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
126         _sessions.ensureIndex(
127                 BasicDBObjectBuilder.start().add("id",1).add("version",1).get(),
128                 BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
129 
130     }
131  
132     /* ------------------------------------------------------------ */
133     /**
134      * Scavenge is a process that periodically checks the tracked session
135      * ids of this given instance of the session id manager to see if they 
136      * are past the point of expiration.
137      */
138     protected void scavenge()
139     {
140         __log.debug("SessionIdManager:scavenge:called with delay" + _scavengeDelay);
141                 
142         synchronized (_sessionsIds)
143         {         
144             /*
145              * run a query returning results that:
146              *  - are in the known list of sessionIds
147              *  - have an accessed time less then current time - the scavenger period
148              *  
149              *  we limit the query to return just the __ID so we are not sucking back full sessions
150              */
151             BasicDBObject query = new BasicDBObject();     
152             query.put(MongoSessionManager.__ID,new BasicDBObject("$in", _sessionsIds ));
153             query.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _scavengeDelay));
154             
155             DBCursor checkSessions = _sessions.find(query, new BasicDBObject(MongoSessionManager.__ID, 1));
156                         
157             for ( DBObject session : checkSessions )
158             {             	                    
159                 invalidateAll((String)session.get(MongoSessionManager.__ID));
160             }
161         } 
162         
163     }
164     
165     /* ------------------------------------------------------------ */
166     /**
167      * ScavengeFully is a process that periodically checks the tracked session
168      * ids of this given instance of the session id manager to see if they 
169      * are past the point of expiration.
170      * 
171      * NOTE: this is potentially devastating and may lead to serious session
172      * coherence issues, not to be used in a running cluster
173      */
174     protected void scavengeFully()
175     {        
176         __log.debug("SessionIdManager:scavengeFully");
177 
178         DBCursor checkSessions = _sessions.find();
179 
180         for (DBObject session : checkSessions)
181         {
182             invalidateAll((String)session.get(MongoSessionManager.__ID));
183         }
184 
185     }
186 
187     /* ------------------------------------------------------------ */
188     /**
189      * Purge is a process that cleans the mongodb cluster of old sessions that are no
190      * longer valid.
191      * 
192      * There are two checks being done here:
193      * 
194      *  - if the accessed time is older then the current time minus the purge invalid age
195      *    and it is no longer valid then remove that session
196      *  - if the accessed time is older then the current time minus the purge valid age
197      *    then we consider this a lost record and remove it
198      *    
199      *  NOTE: if your system supports long lived sessions then the purge valid age should be
200      *  set to zero so the check is skipped.
201      *  
202      *  The second check was added to catch sessions that were being managed on machines 
203      *  that might have crashed without marking their sessions as 'valid=false'
204      */
205     protected void purge()
206     {
207         BasicDBObject invalidQuery = new BasicDBObject();
208 
209         invalidQuery.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _purgeInvalidAge));
210         invalidQuery.put(MongoSessionManager.__VALID, __valid_false);
211         
212         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
213 
214         for (DBObject session : oldSessions)
215         {
216             String id = (String)session.get("id");
217             
218             __log.debug("MongoSessionIdManager:purging invalid " + id);
219             
220             _sessions.remove(session);
221         }
222 
223         if (_purgeValidAge != 0)
224         {
225             BasicDBObject validQuery = new BasicDBObject();
226 
227             validQuery.put(MongoSessionManager.__ACCESSED,new BasicDBObject("$lt",System.currentTimeMillis() - _purgeValidAge));
228             validQuery.put(MongoSessionManager.__VALID, __valid_false);
229 
230             oldSessions = _sessions.find(invalidQuery,new BasicDBObject(MongoSessionManager.__ID,1));
231 
232             for (DBObject session : oldSessions)
233             {
234                 String id = (String)session.get(MongoSessionManager.__ID);
235 
236                 __log.debug("MongoSessionIdManager:purging valid " + id);
237 
238                 _sessions.remove(session);
239             }
240         }
241 
242     }
243     
244     /* ------------------------------------------------------------ */
245     /**
246      * Purge is a process that cleans the mongodb cluster of old sessions that are no
247      * longer valid.
248      * 
249      */
250     protected void purgeFully()
251     {
252         BasicDBObject invalidQuery = new BasicDBObject();
253 
254         invalidQuery.put(MongoSessionManager.__VALID, false);
255         
256         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
257         
258         for (DBObject session : oldSessions)
259         {
260             String id = (String)session.get(MongoSessionManager.__ID);
261             
262             __log.debug("MongoSessionIdManager:purging invalid " + id);
263             
264             _sessions.remove(session);
265         }
266 
267     }
268     
269     
270     /* ------------------------------------------------------------ */
271     public DBCollection getSessions()
272     {
273         return _sessions;
274     }
275     
276     
277     /* ------------------------------------------------------------ */
278     public boolean isPurgeEnabled()
279     {
280         return _purge;
281     }
282     
283     /* ------------------------------------------------------------ */
284     public void setPurge(boolean purge)
285     {
286         this._purge = purge;
287     }
288 
289     /* ------------------------------------------------------------ */
290     /**
291      * sets the scavengeDelay
292      */
293     public void setScavengeDelay(long scavengeDelay)
294     {
295         this._scavengeDelay = scavengeDelay;  
296     }
297 
298 
299     /* ------------------------------------------------------------ */
300     public void setScavengePeriod(long scavengePeriod)
301     {
302         this._scavengePeriod = scavengePeriod;
303     }
304     
305     /* ------------------------------------------------------------ */
306     public void setPurgeDelay(long purgeDelay)
307     {
308         if ( isRunning() )
309         {
310             throw new IllegalStateException();
311         }
312         
313         this._purgeDelay = purgeDelay;
314     }
315  
316     /* ------------------------------------------------------------ */
317     public long getPurgeInvalidAge()
318     {
319         return _purgeInvalidAge;
320     }
321 
322     /* ------------------------------------------------------------ */
323     /**
324      * sets how old a session is to be persisted past the point it is
325      * no longer valid
326      */
327     public void setPurgeInvalidAge(long purgeValidAge)
328     {
329         this._purgeInvalidAge = purgeValidAge;
330     } 
331     
332     /* ------------------------------------------------------------ */
333     public long getPurgeValidAge()
334     {
335         return _purgeValidAge;
336     }
337 
338     /* ------------------------------------------------------------ */
339     /**
340      * sets how old a session is to be persist past the point it is 
341      * considered no longer viable and should be removed
342      * 
343      * NOTE: set this value to 0 to disable purging of valid sessions
344      */
345     public void setPurgeValidAge(long purgeValidAge)
346     {
347         this._purgeValidAge = purgeValidAge;
348     } 
349 
350     /* ------------------------------------------------------------ */
351     @Override
352     protected void doStart() throws Exception
353     {
354         __log.debug("MongoSessionIdManager:starting");
355      
356         /*
357          * setup the scavenger thread
358          */
359         if (_scavengeDelay > 0)
360         {
361             _scavengeTimer = new Timer("MongoSessionIdScavenger",true);
362 
363             synchronized (this)
364             {
365                 if (_scavengerTask != null)
366                 {
367                     _scavengerTask.cancel();
368                 }
369                 
370                 _scavengerTask = new TimerTask()
371                 {
372                     @Override
373                     public void run()
374                     {
375                         scavenge();
376                     }
377                 };
378                 
379                 _scavengeTimer.schedule(_scavengerTask,_scavengeDelay,_scavengePeriod);
380             }
381         }
382         
383         /*
384          * if purging is enabled, setup the purge thread
385          */
386         if ( _purge )
387         {
388             _purgeTimer = new Timer("MongoSessionPurger", true);
389             
390             synchronized (this)
391             {
392                 if (_purgeTask != null)
393                 {
394                     _purgeTask.cancel();
395                 }
396                 _purgeTask = new TimerTask()
397                 {
398                     @Override
399                     public void run()
400                     {
401                         purge();
402                     }
403                 };
404                 _purgeTimer.schedule(_purgeTask,_purgeDelay);
405             }
406         }
407     }
408     
409     /* ------------------------------------------------------------ */
410     @Override
411     protected void doStop() throws Exception
412     {
413         if (_scavengeTimer != null)
414         {
415             _scavengeTimer.cancel();
416             _scavengeTimer = null;
417         }
418         
419         if (_purgeTimer != null)
420         {
421             _purgeTimer.cancel();
422             _purgeTimer = null;
423         }
424         
425         super.doStop();
426     }
427 
428     /* ------------------------------------------------------------ */
429     /**
430      * is the session id known to mongo, and is it valid
431      */
432     @Override
433     public boolean idInUse(String sessionId)
434     {        
435         /*
436          * optimize this query to only return the valid variable
437          */
438         DBObject o = _sessions.findOne(new BasicDBObject("id",sessionId), __valid_false);
439         
440         if ( o != null )
441         {
442             Boolean valid = (Boolean)o.get(MongoSessionManager.__VALID);
443             
444             if ( valid == null )
445             {
446                 return false;
447             }
448             
449             return valid;
450         }
451         
452         return false;
453     }
454 
455     /* ------------------------------------------------------------ */
456     @Override
457     public void addSession(HttpSession session)
458     {
459         if (session == null)
460         {
461             return;
462         }
463         
464         /*
465          * already a part of the index in mongo...
466          */
467         
468         __log.debug("MongoSessionIdManager:addSession:" + session.getId());
469         
470         synchronized (_sessionsIds)
471         {
472             _sessionsIds.add(session.getId());
473         }
474         
475     }
476 
477     /* ------------------------------------------------------------ */
478     @Override
479     public void removeSession(HttpSession session)
480     {
481         if (session == null)
482         {
483             return;
484         }
485         
486         synchronized (_sessionsIds)
487         {
488             _sessionsIds.remove(session.getId());
489         }
490     }
491 
492     /* ------------------------------------------------------------ */
493     @Override
494     public void invalidateAll(String sessionId)
495     {
496         synchronized (_sessionsIds)
497         {
498             _sessionsIds.remove(sessionId);
499             
500             
501             //tell all contexts that may have a session object with this id to
502             //get rid of them
503             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
504             for (int i=0; contexts!=null && i<contexts.length; i++)
505             {
506                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
507                 if (sessionHandler != null) 
508                 {
509                     SessionManager manager = sessionHandler.getSessionManager();
510 
511                     if (manager != null && manager instanceof MongoSessionManager)
512                     {
513                         ((MongoSessionManager)manager).invalidateSession(sessionId);
514                     }
515                 }
516             }
517         }      
518     }
519 
520     /* ------------------------------------------------------------ */
521     // TODO not sure if this is correct
522     @Override
523     public String getClusterId(String nodeId)
524     {
525         int dot=nodeId.lastIndexOf('.');
526         return (dot>0)?nodeId.substring(0,dot):nodeId;
527     }
528 
529     /* ------------------------------------------------------------ */
530     // TODO not sure if this is correct
531     @Override
532     public String getNodeId(String clusterId, HttpServletRequest request)
533     {
534         if (_workerName!=null)
535             return clusterId+'.'+_workerName;
536 
537         return clusterId;
538     }
539 
540 }