View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 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         __log.debug("PURGING");
263         BasicDBObject invalidQuery = new BasicDBObject();
264 
265         invalidQuery.put(MongoSessionManager.__ACCESSED, new BasicDBObject("$lt",System.currentTimeMillis() - _purgeInvalidAge));
266         invalidQuery.put(MongoSessionManager.__VALID, false);
267         
268         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
269 
270         for (DBObject session : oldSessions)
271         {
272             String id = (String)session.get("id");
273             
274             __log.debug("MongoSessionIdManager:purging invalid session {}", id);
275             
276             _sessions.remove(session);
277         }
278 
279         if (_purgeValidAge != 0)
280         {
281             BasicDBObject validQuery = new BasicDBObject();
282 
283             validQuery.put(MongoSessionManager.__ACCESSED,new BasicDBObject("$lt",System.currentTimeMillis() - _purgeValidAge));
284             validQuery.put(MongoSessionManager.__VALID, true);
285 
286             oldSessions = _sessions.find(validQuery,new BasicDBObject(MongoSessionManager.__ID,1));
287 
288             for (DBObject session : oldSessions)
289             {
290                 String id = (String)session.get(MongoSessionManager.__ID);
291 
292                 __log.debug("MongoSessionIdManager:purging valid session {}", id);
293 
294                 _sessions.remove(session);
295             }
296         }
297 
298     }
299     
300     /* ------------------------------------------------------------ */
301     /**
302      * Purge is a process that cleans the mongodb cluster of old sessions that are no
303      * longer valid.
304      * 
305      */
306     protected void purgeFully()
307     {
308         BasicDBObject invalidQuery = new BasicDBObject();
309         invalidQuery.put(MongoSessionManager.__VALID, false);
310         
311         DBCursor oldSessions = _sessions.find(invalidQuery, new BasicDBObject(MongoSessionManager.__ID, 1));
312         
313         for (DBObject session : oldSessions)
314         {
315             String id = (String)session.get(MongoSessionManager.__ID);
316             
317             __log.debug("MongoSessionIdManager:purging invalid session {}", id);
318             
319             _sessions.remove(session);
320         }
321 
322     }
323     
324     
325     /* ------------------------------------------------------------ */
326     public DBCollection getSessions()
327     {
328         return _sessions;
329     }
330     
331     
332     /* ------------------------------------------------------------ */
333     public boolean isPurgeEnabled()
334     {
335         return _purge;
336     }
337     
338     /* ------------------------------------------------------------ */
339     public void setPurge(boolean purge)
340     {
341         this._purge = purge;
342     }
343 
344 
345     /* ------------------------------------------------------------ */
346     /** 
347      * The period in seconds between scavenge checks.
348      * 
349      * @param scavengePeriod
350      */
351     public void setScavengePeriod(long scavengePeriod)
352     {
353         if (scavengePeriod <= 0)
354             _scavengePeriod = __defaultScavengePeriod;
355         else
356             _scavengePeriod = TimeUnit.SECONDS.toMillis(scavengePeriod);
357     }
358 
359     /* ------------------------------------------------------------ */
360     public void setPurgeDelay(long purgeDelay)
361     {
362         if ( isRunning() )
363         {
364             throw new IllegalStateException();
365         }
366         
367         this._purgeDelay = purgeDelay;
368     }
369  
370     /* ------------------------------------------------------------ */
371     public long getPurgeInvalidAge()
372     {
373         return _purgeInvalidAge;
374     }
375 
376     /* ------------------------------------------------------------ */
377     /**
378      * sets how old a session is to be persisted past the point it is
379      * no longer valid
380      */
381     public void setPurgeInvalidAge(long purgeValidAge)
382     {
383         this._purgeInvalidAge = purgeValidAge;
384     } 
385     
386     /* ------------------------------------------------------------ */
387     public long getPurgeValidAge()
388     {
389         return _purgeValidAge;
390     }
391 
392     /* ------------------------------------------------------------ */
393     /**
394      * sets how old a session is to be persist past the point it is 
395      * considered no longer viable and should be removed
396      * 
397      * NOTE: set this value to 0 to disable purging of valid sessions
398      */
399     public void setPurgeValidAge(long purgeValidAge)
400     {
401         this._purgeValidAge = purgeValidAge;
402     } 
403 
404     /* ------------------------------------------------------------ */
405     @Override
406     protected void doStart() throws Exception
407     {
408         __log.debug("MongoSessionIdManager:starting");
409 
410 
411         synchronized (this)
412         {
413             //try and use a common scheduler, fallback to own
414             _scheduler =_server.getBean(Scheduler.class);
415             if (_scheduler == null)
416             {
417                 _scheduler = new ScheduledExecutorScheduler();
418                 _ownScheduler = true;
419                 _scheduler.start();
420             }   
421             else if (!_scheduler.isStarted())
422                 throw new IllegalStateException("Shared scheduler not started");
423             
424 
425             //setup the scavenger thread
426             if (_scavengePeriod > 0)
427             {
428                 if (_scavengerTask != null)
429                 {
430                     _scavengerTask.cancel();
431                     _scavengerTask = null;
432                 }
433 
434                 _scavengerTask = _scheduler.schedule(new Scavenger(), _scavengePeriod, TimeUnit.MILLISECONDS);
435             }
436             else if (__log.isDebugEnabled())
437                 __log.debug("Scavenger disabled");
438 
439 
440             //if purging is enabled, setup the purge thread
441             if ( _purge )
442             { 
443                 if (_purgerTask != null)
444                 {
445                     _purgerTask.cancel();
446                     _purgerTask = null;
447                 }
448                 _purgerTask = _scheduler.schedule(new Purger(), _purgeDelay, TimeUnit.MILLISECONDS);
449             }
450             else if (__log.isDebugEnabled())
451                 __log.debug("Purger disabled");
452         }
453     }
454 
455     /* ------------------------------------------------------------ */
456     @Override
457     protected void doStop() throws Exception
458     {
459         synchronized (this)
460         {
461             if (_scavengerTask != null)
462             {
463                 _scavengerTask.cancel();
464                 _scavengerTask = null;
465             }
466  
467             if (_purgerTask != null)
468             {
469                 _purgerTask.cancel();
470                 _purgerTask = null;
471             }
472             
473             if (_ownScheduler && _scheduler != null)
474             {
475                 _scheduler.stop();
476                 _scheduler = null;
477             }
478         }
479         super.doStop();
480     }
481 
482     /* ------------------------------------------------------------ */
483     /**
484      * Searches database to find if the session id known to mongo, and is it valid
485      */
486     @Override
487     public boolean idInUse(String sessionId)
488     {        
489         /*
490          * optimize this query to only return the valid variable
491          */
492         DBObject o = _sessions.findOne(new BasicDBObject("id",sessionId), __valid_true);
493         
494         if ( o != null )
495         {                    
496             Boolean valid = (Boolean)o.get(MongoSessionManager.__VALID);
497             if ( valid == null )
498             {
499                 return false;
500             }            
501             
502             return valid;
503         }
504         
505         return false;
506     }
507 
508     /* ------------------------------------------------------------ */
509     @Override
510     public void addSession(HttpSession session)
511     {
512         if (session == null)
513         {
514             return;
515         }
516         
517         /*
518          * already a part of the index in mongo...
519          */
520         
521         __log.debug("MongoSessionIdManager:addSession {}", session.getId());
522         
523         synchronized (_sessionsIds)
524         {
525             _sessionsIds.add(session.getId());
526         }
527         
528     }
529 
530     /* ------------------------------------------------------------ */
531     @Override
532     public void removeSession(HttpSession session)
533     {
534         if (session == null)
535         {
536             return;
537         }
538         
539         synchronized (_sessionsIds)
540         {
541             _sessionsIds.remove(session.getId());
542         }
543     }
544 
545     /* ------------------------------------------------------------ */
546     /** Remove the session id from the list of in-use sessions.
547      * Inform all other known contexts that sessions with the same id should be
548      * invalidated.
549      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
550      */
551     @Override
552     public void invalidateAll(String sessionId)
553     {
554         synchronized (_sessionsIds)
555         {
556             _sessionsIds.remove(sessionId);
557                 
558             //tell all contexts that may have a session object with this id to
559             //get rid of them
560             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
561             for (int i=0; contexts!=null && i<contexts.length; i++)
562             {
563                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
564                 if (sessionHandler != null) 
565                 {
566                     SessionManager manager = sessionHandler.getSessionManager();
567 
568                     if (manager != null && manager instanceof MongoSessionManager)
569                     {
570                         ((MongoSessionManager)manager).invalidateSession(sessionId);
571                     }
572                 }
573             }
574         }      
575     } 
576 
577     /* ------------------------------------------------------------ */
578     /**
579      * Expire this session for all contexts that are sharing the session 
580      * id.
581      * @param sessionId
582      */
583     public void expireAll (String sessionId)
584     {
585         synchronized (_sessionsIds)
586         {
587             _sessionsIds.remove(sessionId);
588             
589             
590             //tell all contexts that may have a session object with this id to
591             //get rid of them
592             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
593             for (int i=0; contexts!=null && i<contexts.length; i++)
594             {
595                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
596                 if (sessionHandler != null) 
597                 {
598                     SessionManager manager = sessionHandler.getSessionManager();
599 
600                     if (manager != null && manager instanceof MongoSessionManager)
601                     {
602                         ((MongoSessionManager)manager).expire(sessionId);
603                     }
604                 }
605             }
606         }      
607     }
608     
609     /* ------------------------------------------------------------ */
610     @Override
611     public void renewSessionId(String oldClusterId, String oldNodeId, HttpServletRequest request)
612     {
613         //generate a new id
614         String newClusterId = newSessionId(request.hashCode());
615 
616         synchronized (_sessionsIds)
617         {
618             _sessionsIds.remove(oldClusterId);//remove the old one from the list
619             _sessionsIds.add(newClusterId); //add in the new session id to the list
620 
621             //tell all contexts to update the id 
622             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
623             for (int i=0; contexts!=null && i<contexts.length; i++)
624             {
625                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
626                 if (sessionHandler != null) 
627                 {
628                     SessionManager manager = sessionHandler.getSessionManager();
629 
630                     if (manager != null && manager instanceof MongoSessionManager)
631                     {
632                         ((MongoSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
633                     }
634                 }
635             }
636         }
637     }
638 
639 }