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