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