View Javadoc

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