View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.concurrent.TimeUnit;
27  
28  import javax.servlet.http.HttpServletRequest;
29  import javax.servlet.http.HttpSession;
30  
31  import org.eclipse.jetty.server.Handler;
32  import org.eclipse.jetty.server.Server;
33  import org.eclipse.jetty.server.SessionManager;
34  import org.eclipse.jetty.server.handler.ContextHandler;
35  import org.eclipse.jetty.server.session.AbstractSessionIdManager;
36  import org.eclipse.jetty.server.session.SessionHandler;
37  import org.eclipse.jetty.util.log.Log;
38  import org.eclipse.jetty.util.log.Logger;
39  import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
40  import org.eclipse.jetty.util.thread.Scheduler;
41  
42  import com.mongodb.BasicDBObject;
43  import com.mongodb.BasicDBObjectBuilder;
44  import com.mongodb.DBCollection;
45  import com.mongodb.DBCursor;
46  import com.mongodb.DBObject;
47  import com.mongodb.Mongo;
48  import com.mongodb.MongoException;
49  
50  /**
51   * Based partially on the JDBCSessionIdManager.
52   *
53   * Theory is that we really only need the session id manager for the local 
54   * instance so we have something to scavenge on, namely the list of known ids
55   * 
56   * This class has a timer that runs a periodic scavenger thread to query
57   *  for all id's known to this node whose precalculated expiry time has passed.
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      final static long __defaultScavengePeriod = 30 * 60 * 1000; // every 30 minutes
73     
74      
75      final DBCollection _sessions;
76      protected Server _server;
77      private Scheduler _scheduler;
78      private boolean _ownScheduler;
79      private Scheduler.Task _scavengerTask;
80      private Scheduler.Task _purgerTask;
81   
82  
83      
84      private long _scavengePeriod = __defaultScavengePeriod;
85      
86  
87      /** 
88       * purge process is enabled by default
89       */
90      private boolean _purge = true;
91  
92      /**
93       * purge process would run daily by default
94       */
95      private long _purgeDelay = 24 * 60 * 60 * 1000; // every day
96      
97      /**
98       * how long do you want to persist sessions that are no longer
99       * valid before removing them completely
100      */
101     private long _purgeInvalidAge = 24 * 60 * 60 * 1000; // default 1 day
102 
103     /**
104      * how long do you want to leave sessions that are still valid before
105      * assuming they are dead and removing them
106      */
107     private long _purgeValidAge = 7 * 24 * 60 * 60 * 1000; // default 1 week
108 
109     
110     /**
111      * the collection of session ids known to this manager
112      * 
113      * TODO consider if this ought to be concurrent or not
114      */
115     protected final Set<String> _sessionsIds = new HashSet<String>();
116     
117     
118     /**
119      * Scavenger
120      *
121      */
122     protected class Scavenger implements Runnable
123     {
124         @Override
125         public void run()
126         {
127             try
128             {
129                 scavenge();
130             }
131             finally
132             {
133                 if (_scheduler != null && _scheduler.isRunning())
134                     _scavengerTask = _scheduler.schedule(this, _scavengePeriod, TimeUnit.MILLISECONDS);
135             }
136         } 
137     }
138     
139     
140     /**
141      * Purger
142      *
143      */
144     protected class Purger implements Runnable
145     {
146         @Override
147         public void run()
148         {
149             try
150             {
151                 purge();
152             }
153             finally
154             {
155                 if (_scheduler != null && _scheduler.isRunning())
156                     _purgerTask = _scheduler.schedule(this, _purgeDelay, TimeUnit.MILLISECONDS);
157             }
158         }
159     }
160     
161     
162     
163 
164     /* ------------------------------------------------------------ */
165     public MongoSessionIdManager(Server server) throws UnknownHostException, MongoException
166     {
167         this(server, new Mongo().getDB("HttpSessions").getCollection("sessions"));
168     }
169 
170     /* ------------------------------------------------------------ */
171     public MongoSessionIdManager(Server server, DBCollection sessions)
172     {
173         super(new Random());
174         
175         _server = server;
176         _sessions = sessions;
177 
178         _sessions.ensureIndex(
179                 BasicDBObjectBuilder.start().add("id",1).get(),
180                 BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
181         _sessions.ensureIndex(
182                 BasicDBObjectBuilder.start().add("id",1).add("version",1).get(),
183                 BasicDBObjectBuilder.start().add("unique",true).add("sparse",false).get());
184 
185     }
186  
187     /* ------------------------------------------------------------ */
188     /**
189      * Scavenge is a process that periodically checks the tracked session
190      * ids of this given instance of the session id manager to see if they 
191      * are past the point of expiration.
192      */
193     protected void scavenge()
194     {
195         long now = System.currentTimeMillis();
196         __log.debug("SessionIdManager:scavenge:at {}", now);        
197         synchronized (_sessionsIds)
198         {         
199             /*
200              * run a query returning results that:
201              *  - are in the known list of sessionIds
202              *  - the expiry time has passed
203              *  
204              *  we limit the query to return just the __ID so we are not sucking back full sessions
205              */
206             BasicDBObject query = new BasicDBObject();     
207             query.put(MongoSessionManager.__ID,new BasicDBObject("$in", _sessionsIds ));
208             query.put(MongoSessionManager.__EXPIRY, new BasicDBObject("$gt", 0));
209             query.put(MongoSessionManager.__EXPIRY, new BasicDBObject("$lt", now));
210         
211             
212             DBCursor checkSessions = _sessions.find(query, new BasicDBObject(MongoSessionManager.__ID, 1));
213                         
214             for ( DBObject session : checkSessions )
215             {             
216                 __log.debug("SessionIdManager:scavenge: expiring session {}", (String)session.get(MongoSessionManager.__ID));
217                 expireAll((String)session.get(MongoSessionManager.__ID));
218             }
219         }      
220     }
221     
222     /* ------------------------------------------------------------ */
223     /**
224      * ScavengeFully will expire all sessions. In most circumstances
225      * you should never need to call this method.
226      * 
227      * <b>USE WITH CAUTION</b>
228      */
229     protected void scavengeFully()
230     {        
231         __log.debug("SessionIdManager:scavengeFully");
232 
233         DBCursor checkSessions = _sessions.find();
234 
235         for (DBObject session : checkSessions)
236         {
237             expireAll((String)session.get(MongoSessionManager.__ID));
238         }
239 
240     }
241 
242     /* ------------------------------------------------------------ */
243     /**
244      * Purge is a process that cleans the mongodb cluster of old sessions that are no
245      * longer valid.
246      * 
247      * There are two checks being done here:
248      * 
249      *  - if the accessed time is older than the current time minus the purge invalid age
250      *    and it is no longer valid then remove that session
251      *  - if the accessed time is older then the current time minus the purge valid age
252      *    then we consider this a lost record and remove it
253      *    
254      *  NOTE: if your system supports long lived sessions then the purge valid age should be
255      *  set to zero so the check is skipped.
256      *  
257      *  The second check was added to catch sessions that were being managed on machines 
258      *  that might have crashed without marking their sessions as 'valid=false'
259      */
260     protected void purge()
261     {
262         BasicDBObject invalidQuery = new BasicDBObject();
263 
264         invalidQuery.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _purgeInvalidAge));
265         invalidQuery.put(MongoSessionManager.__VALID, false);
266         
267         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
268 
269         for (DBObject session : oldSessions)
270         {
271             String id = (String)session.get("id");
272             
273             __log.debug("MongoSessionIdManager:purging invalid session {}", id);
274             
275             _sessions.remove(session);
276         }
277 
278         if (_purgeValidAge != 0)
279         {
280             BasicDBObject validQuery = new BasicDBObject();
281 
282             validQuery.put(MongoSessionManager.__ACCESSED,new BasicDBObject("$lt",System.currentTimeMillis() - _purgeValidAge));
283             validQuery.put(MongoSessionManager.__VALID, true);
284 
285             oldSessions = _sessions.find(validQuery,new BasicDBObject(MongoSessionManager.__ID,1));
286 
287             for (DBObject session : oldSessions)
288             {
289                 String id = (String)session.get(MongoSessionManager.__ID);
290 
291                 __log.debug("MongoSessionIdManager:purging valid session {}", id);
292 
293                 _sessions.remove(session);
294             }
295         }
296 
297     }
298     
299     /* ------------------------------------------------------------ */
300     /**
301      * Purge is a process that cleans the mongodb cluster of old sessions that are no
302      * longer valid.
303      * 
304      */
305     protected void purgeFully()
306     {
307         BasicDBObject invalidQuery = new BasicDBObject();
308         invalidQuery.put(MongoSessionManager.__VALID, false);
309         
310         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
311         
312         for (DBObject session : oldSessions)
313         {
314             String id = (String)session.get(MongoSessionManager.__ID);
315             
316             __log.debug("MongoSessionIdManager:purging invalid session {}", id);
317             
318             _sessions.remove(session);
319         }
320 
321     }
322     
323     
324     /* ------------------------------------------------------------ */
325     public DBCollection getSessions()
326     {
327         return _sessions;
328     }
329     
330     
331     /* ------------------------------------------------------------ */
332     public boolean isPurgeEnabled()
333     {
334         return _purge;
335     }
336     
337     /* ------------------------------------------------------------ */
338     public void setPurge(boolean purge)
339     {
340         this._purge = purge;
341     }
342 
343 
344     /* ------------------------------------------------------------ */
345     /** 
346      * The period in seconds between scavenge checks.
347      * 
348      * @param scavengePeriod
349      */
350     public void setScavengePeriod(long scavengePeriod)
351     {
352         if (scavengePeriod <= 0)
353             _scavengePeriod = __defaultScavengePeriod;
354         else
355             _scavengePeriod = TimeUnit.SECONDS.toMillis(scavengePeriod);
356     }
357 
358     /* ------------------------------------------------------------ */
359     public void setPurgeDelay(long purgeDelay)
360     {
361         if ( isRunning() )
362         {
363             throw new IllegalStateException();
364         }
365         
366         this._purgeDelay = purgeDelay;
367     }
368  
369     /* ------------------------------------------------------------ */
370     public long getPurgeInvalidAge()
371     {
372         return _purgeInvalidAge;
373     }
374 
375     /* ------------------------------------------------------------ */
376     /**
377      * sets how old a session is to be persisted past the point it is
378      * no longer valid
379      */
380     public void setPurgeInvalidAge(long purgeValidAge)
381     {
382         this._purgeInvalidAge = purgeValidAge;
383     } 
384     
385     /* ------------------------------------------------------------ */
386     public long getPurgeValidAge()
387     {
388         return _purgeValidAge;
389     }
390 
391     /* ------------------------------------------------------------ */
392     /**
393      * sets how old a session is to be persist past the point it is 
394      * considered no longer viable and should be removed
395      * 
396      * NOTE: set this value to 0 to disable purging of valid sessions
397      */
398     public void setPurgeValidAge(long purgeValidAge)
399     {
400         this._purgeValidAge = purgeValidAge;
401     } 
402 
403     /* ------------------------------------------------------------ */
404     @Override
405     protected void doStart() throws Exception
406     {
407         __log.debug("MongoSessionIdManager:starting");
408 
409 
410         synchronized (this)
411         {
412             //try and use a common scheduler, fallback to own
413             _scheduler =_server.getBean(Scheduler.class);
414             if (_scheduler == null)
415             {
416                 _scheduler = new ScheduledExecutorScheduler();
417                 _ownScheduler = true;
418                 _scheduler.start();
419             }   
420             else if (!_scheduler.isStarted())
421                 throw new IllegalStateException("Shared scheduler not started");
422             
423 
424             //setup the scavenger thread
425             if (_scavengePeriod > 0)
426             {
427                 if (_scavengerTask != null)
428                 {
429                     _scavengerTask.cancel();
430                     _scavengerTask = null;
431                 }
432 
433                 _scavengerTask = _scheduler.schedule(new Scavenger(), _scavengePeriod, TimeUnit.MILLISECONDS);
434             }
435 
436 
437             //if purging is enabled, setup the purge thread
438             if ( _purge )
439             { 
440                 if (_purgerTask != null)
441                 {
442                     _purgerTask.cancel();
443                     _purgerTask = null;
444                 }
445                 _purgerTask = _scheduler.schedule(new Purger(), _purgeDelay, TimeUnit.MILLISECONDS);
446             }
447         }
448     }
449 
450     /* ------------------------------------------------------------ */
451     @Override
452     protected void doStop() throws Exception
453     {
454         synchronized (this)
455         {
456             if (_scavengerTask != null)
457             {
458                 _scavengerTask.cancel();
459                 _scavengerTask = null;
460             }
461  
462             if (_purgerTask != null)
463             {
464                 _purgerTask.cancel();
465                 _purgerTask = null;
466             }
467             
468             if (_ownScheduler && _scheduler != null)
469             {
470                 _scheduler.stop();
471                 _scheduler = null;
472             }
473         }
474         super.doStop();
475     }
476 
477     /* ------------------------------------------------------------ */
478     /**
479      * Searches database to find if the session id known to mongo, and is it valid
480      */
481     @Override
482     public boolean idInUse(String sessionId)
483     {        
484         /*
485          * optimize this query to only return the valid variable
486          */
487         DBObject o = _sessions.findOne(new BasicDBObject("id",sessionId), __valid_true);
488         
489         if ( o != null )
490         {                    
491             Boolean valid = (Boolean)o.get(MongoSessionManager.__VALID);
492             if ( valid == null )
493             {
494                 return false;
495             }            
496             
497             return valid;
498         }
499         
500         return false;
501     }
502 
503     /* ------------------------------------------------------------ */
504     @Override
505     public void addSession(HttpSession session)
506     {
507         if (session == null)
508         {
509             return;
510         }
511         
512         /*
513          * already a part of the index in mongo...
514          */
515         
516         __log.debug("MongoSessionIdManager:addSession {}", session.getId());
517         
518         synchronized (_sessionsIds)
519         {
520             _sessionsIds.add(session.getId());
521         }
522         
523     }
524 
525     /* ------------------------------------------------------------ */
526     @Override
527     public void removeSession(HttpSession session)
528     {
529         if (session == null)
530         {
531             return;
532         }
533         
534         synchronized (_sessionsIds)
535         {
536             _sessionsIds.remove(session.getId());
537         }
538     }
539 
540     /* ------------------------------------------------------------ */
541     /** Remove the session id from the list of in-use sessions.
542      * Inform all other known contexts that sessions with the same id should be
543      * invalidated.
544      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
545      */
546     @Override
547     public void invalidateAll(String sessionId)
548     {
549         synchronized (_sessionsIds)
550         {
551             _sessionsIds.remove(sessionId);
552                 
553             //tell all contexts that may have a session object with this id to
554             //get rid of them
555             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
556             for (int i=0; contexts!=null && i<contexts.length; i++)
557             {
558                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
559                 if (sessionHandler != null) 
560                 {
561                     SessionManager manager = sessionHandler.getSessionManager();
562 
563                     if (manager != null && manager instanceof MongoSessionManager)
564                     {
565                         ((MongoSessionManager)manager).invalidateSession(sessionId);
566                     }
567                 }
568             }
569         }      
570     } 
571 
572     /* ------------------------------------------------------------ */
573     /**
574      * Expire this session for all contexts that are sharing the session 
575      * id.
576      * @param sessionId
577      */
578     public void expireAll (String sessionId)
579     {
580         synchronized (_sessionsIds)
581         {
582             _sessionsIds.remove(sessionId);
583             
584             
585             //tell all contexts that may have a session object with this id to
586             //get rid of them
587             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
588             for (int i=0; contexts!=null && i<contexts.length; i++)
589             {
590                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
591                 if (sessionHandler != null) 
592                 {
593                     SessionManager manager = sessionHandler.getSessionManager();
594 
595                     if (manager != null && manager instanceof MongoSessionManager)
596                     {
597                         ((MongoSessionManager)manager).expire(sessionId);
598                     }
599                 }
600             }
601         }      
602     }
603     
604     /* ------------------------------------------------------------ */
605     @Override
606     public void renewSessionId(String oldClusterId, String oldNodeId, HttpServletRequest request)
607     {
608         //generate a new id
609         String newClusterId = newSessionId(request.hashCode());
610 
611         synchronized (_sessionsIds)
612         {
613             _sessionsIds.remove(oldClusterId);//remove the old one from the list
614             _sessionsIds.add(newClusterId); //add in the new session id to the list
615 
616             //tell all contexts to update the id 
617             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
618             for (int i=0; contexts!=null && i<contexts.length; i++)
619             {
620                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
621                 if (sessionHandler != null) 
622                 {
623                     SessionManager manager = sessionHandler.getSessionManager();
624 
625                     if (manager != null && manager instanceof MongoSessionManager)
626                     {
627                         ((MongoSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
628                     }
629                 }
630             }
631         }
632     }
633 
634 }