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