View Javadoc

1   // ========================================================================
2   // Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses.
12  // ========================================================================
13  
14  package org.eclipse.jetty.server.session;
15  
16  import java.io.DataInputStream;
17  import java.io.File;
18  import java.io.FileInputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.ObjectInputStream;
22  import java.util.ArrayList;
23  import java.util.Iterator;
24  import java.util.Map;
25  import java.util.Timer;
26  import java.util.TimerTask;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.ConcurrentMap;
29  
30  import javax.servlet.ServletContext;
31  import javax.servlet.http.HttpServletRequest;
32  
33  import org.eclipse.jetty.server.handler.ContextHandler;
34  import org.eclipse.jetty.util.log.Logger;
35  
36  
37  /* ------------------------------------------------------------ */
38  /** An in-memory implementation of SessionManager.
39   * <p>
40   * This manager supports saving sessions to disk, either periodically or at shutdown.
41   * Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions.
42   * <p>
43   * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance
44   * set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler.
45   *
46   */
47  public class HashSessionManager extends AbstractSessionManager
48  {
49      final static Logger __log = SessionHandler.LOG;
50  
51      protected final ConcurrentMap<String,HashedSession> _sessions=new ConcurrentHashMap<String,HashedSession>();
52      private static int __id;
53      private Timer _timer;
54      private boolean _timerStop=false;
55      private TimerTask _task;
56      long _scavengePeriodMs=30000;
57      long _savePeriodMs=0; //don't do period saves by default
58      long _idleSavePeriodMs = 0; // don't idle save sessions by default.
59      private TimerTask _saveTask;
60      File _storeDir;
61      private boolean _lazyLoad=false;
62      private volatile boolean _sessionsLoaded=false;
63      private boolean _deleteUnrestorableSessions=false;
64      
65  
66  
67  
68      /* ------------------------------------------------------------ */
69      public HashSessionManager()
70      {
71          super();
72      }
73  
74      /* ------------------------------------------------------------ */
75      /* (non-Javadoc)
76       * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart()
77       */
78      @Override
79      public void doStart() throws Exception
80      {
81          super.doStart();
82  
83          _timerStop=false;
84          ServletContext context = ContextHandler.getCurrentContext();
85          if (context!=null)
86              _timer=(Timer)context.getAttribute("org.eclipse.jetty.server.session.timer");
87          if (_timer==null)
88          {
89              _timerStop=true;
90              _timer=new Timer("HashSessionScavenger-"+__id++, true);
91          }
92  
93          setScavengePeriod(getScavengePeriod());
94  
95          if (_storeDir!=null)
96          {
97              if (!_storeDir.exists())
98                  _storeDir.mkdirs();
99  
100             if (!_lazyLoad)
101                 restoreSessions();
102         }
103 
104         setSavePeriod(getSavePeriod());
105     }
106 
107     /* ------------------------------------------------------------ */
108     /* (non-Javadoc)
109      * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop()
110      */
111     @Override
112     public void doStop() throws Exception
113     {
114         // stop the scavengers
115         synchronized(this)
116         {
117             if (_saveTask!=null)
118                 _saveTask.cancel();
119             _saveTask=null;
120             if (_task!=null)
121                 _task.cancel();
122             _task=null;
123             if (_timer!=null && _timerStop)
124                 _timer.cancel();
125             _timer=null;
126         }
127 
128         // This will callback invalidate sessions - where we decide if we will save
129         super.doStop();
130 
131         _sessions.clear();
132 
133     }
134 
135     /* ------------------------------------------------------------ */
136     /**
137      * @return the period in seconds at which a check is made for sessions to be invalidated.
138      */
139     public int getScavengePeriod()
140     {
141         return (int)(_scavengePeriodMs/1000);
142     }
143 
144 
145     /* ------------------------------------------------------------ */
146     @Override
147     public int getSessions()
148     {
149         int sessions=super.getSessions();
150         if (__log.isDebugEnabled())
151         {
152             if (_sessions.size()!=sessions)
153                 __log.warn("sessions: "+_sessions.size()+"!="+sessions);
154         }
155         return sessions;
156     }
157 
158     /* ------------------------------------------------------------ */
159     /**
160      * @return seconds Idle period after which a session is saved
161      */
162     public int getIdleSavePeriod()
163     {
164       if (_idleSavePeriodMs <= 0)
165         return 0;
166 
167       return (int)(_idleSavePeriodMs / 1000);
168     }
169 
170     /* ------------------------------------------------------------ */
171     /**
172      * Configures the period in seconds after which a session is deemed idle and saved
173      * to save on session memory.
174      *
175      * The session is persisted, the values attribute map is cleared and the session set to idled.
176      *
177      * @param seconds Idle period after which a session is saved
178      */
179     public void setIdleSavePeriod(int seconds)
180     {
181       _idleSavePeriodMs = seconds * 1000L;
182     }
183 
184     /* ------------------------------------------------------------ */
185     @Override
186     public void setMaxInactiveInterval(int seconds)
187     {
188         super.setMaxInactiveInterval(seconds);
189         if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000L)
190             setScavengePeriod((_dftMaxIdleSecs+9)/10);
191     }
192 
193     /* ------------------------------------------------------------ */
194     /**
195      * @param seconds the period is seconds at which sessions are periodically saved to disk
196      */
197     public void setSavePeriod (int seconds)
198     {
199         long period = (seconds * 1000L);
200         if (period < 0)
201             period=0;
202         _savePeriodMs=period;
203 
204         if (_timer!=null)
205         {
206             synchronized (this)
207             {
208                 if (_saveTask!=null)
209                     _saveTask.cancel();
210                 if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured
211                 {
212                     _saveTask = new TimerTask()
213                     {
214                         @Override
215                         public void run()
216                         {
217                             try
218                             {
219                                 saveSessions(true);
220                             }
221                             catch (Exception e)
222                             {
223                                 __log.warn(e);
224                             }
225                         }
226                     };
227                     _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs);
228                 }
229             }
230         }
231     }
232 
233     /* ------------------------------------------------------------ */
234     /**
235      * @return the period in seconds at which sessions are periodically saved to disk
236      */
237     public int getSavePeriod ()
238     {
239         if (_savePeriodMs<=0)
240             return 0;
241 
242         return (int)(_savePeriodMs/1000);
243     }
244 
245     /* ------------------------------------------------------------ */
246     /**
247      * @param seconds the period in seconds at which a check is made for sessions to be invalidated.
248      */
249     public void setScavengePeriod(int seconds)
250     {
251         if (seconds==0)
252             seconds=60;
253 
254         long old_period=_scavengePeriodMs;
255         long period=seconds*1000L;
256         if (period>60000)
257             period=60000;
258         if (period<1000)
259             period=1000;
260 
261         _scavengePeriodMs=period;
262         if (_timer!=null && (period!=old_period || _task==null))
263         {
264             synchronized (this)
265             {
266                 if (_task!=null)
267                     _task.cancel();
268                 _task = new TimerTask()
269                 {
270                     @Override
271                     public void run()
272                     {
273                         scavenge();
274                     }
275                 };
276                 _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs);
277             }
278         }
279     }
280 
281     /* -------------------------------------------------------------- */
282     /**
283      * Find sessions that have timed out and invalidate them. This runs in the
284      * SessionScavenger thread.
285      */
286     protected void scavenge()
287     {
288         //don't attempt to scavenge if we are shutting down
289         if (isStopping() || isStopped())
290             return;
291 
292         Thread thread=Thread.currentThread();
293         ClassLoader old_loader=thread.getContextClassLoader();
294         try
295         {
296             if (_loader!=null)
297                 thread.setContextClassLoader(_loader);
298 
299             // For each session
300             long now=System.currentTimeMillis();
301             for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();)
302             {
303                 HashedSession session=i.next();
304                 long idleTime=session.getMaxInactiveInterval()*1000L;
305                 if (idleTime>0&&session.getAccessed()+idleTime<now)
306                 {
307                     // Found a stale session, add it to the list
308                     session.timeout();
309                 }
310                 else if (_idleSavePeriodMs>0&&session.getAccessed()+_idleSavePeriodMs<now)
311                 {
312                     session.idle();
313                 }
314             }
315         }
316         catch (Throwable t)
317         {
318             __log.warn("Problem scavenging sessions", t);
319         }
320         finally
321         {
322             thread.setContextClassLoader(old_loader);
323         }
324     }
325 
326     /* ------------------------------------------------------------ */
327     @Override
328     protected void addSession(AbstractSession session)
329     {
330         if (isRunning())
331             _sessions.put(session.getClusterId(),(HashedSession)session);
332     }
333 
334     /* ------------------------------------------------------------ */
335     @Override
336     public AbstractSession getSession(String idInCluster)
337     {
338         if ( _lazyLoad && !_sessionsLoaded)
339         {
340             try
341             {
342                 restoreSessions();
343             }
344             catch(Exception e)
345             {
346                 __log.warn(e);
347             }
348         }
349 
350         Map<String,HashedSession> sessions=_sessions;
351         if (sessions==null)
352             return null;
353 
354         HashedSession session = sessions.get(idInCluster);
355 
356         if (session == null && _lazyLoad)
357             session=restoreSession(idInCluster);
358         if (session == null)
359             return null;
360 
361         if (_idleSavePeriodMs!=0)
362             session.deIdle();
363 
364         return session;
365     }
366 
367     /* ------------------------------------------------------------ */
368     @Override
369     protected void invalidateSessions() throws Exception
370     {
371         // Invalidate all sessions to cause unbind events
372         ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values());
373         int loop=100;
374         while (sessions.size()>0 && loop-->0)
375         {
376             // If we are called from doStop
377             if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite())
378             {
379                 // Then we only save and remove the session - it is not invalidated.
380                 for (HashedSession session : sessions)
381                 {
382                     session.save(false);
383                     removeSession(session,false);
384                 }
385             }
386             else
387             {
388                 for (HashedSession session : sessions)
389                     session.invalidate();
390             }
391 
392             // check that no new sessions were created while we were iterating
393             sessions=new ArrayList<HashedSession>(_sessions.values());
394         }
395     }
396 
397     /* ------------------------------------------------------------ */
398     @Override
399     protected AbstractSession newSession(HttpServletRequest request)
400     {
401         return new HashedSession(this, request);
402     }
403 
404     /* ------------------------------------------------------------ */
405     protected AbstractSession newSession(long created, long accessed, String clusterId)
406     {
407         return new HashedSession(this, created,accessed, clusterId);
408     }
409 
410     /* ------------------------------------------------------------ */
411     @Override
412     protected boolean removeSession(String clusterId)
413     {
414         return _sessions.remove(clusterId)!=null;
415     }
416 
417     /* ------------------------------------------------------------ */
418     public void setStoreDirectory (File dir)
419     {
420         _storeDir=dir;
421     }
422 
423     /* ------------------------------------------------------------ */
424     public File getStoreDirectory ()
425     {
426         return _storeDir;
427     }
428 
429     /* ------------------------------------------------------------ */
430     public void setLazyLoad(boolean lazyLoad)
431     {
432         _lazyLoad = lazyLoad;
433     }
434 
435     /* ------------------------------------------------------------ */
436     public boolean isLazyLoad()
437     {
438         return _lazyLoad;
439     }
440     
441     /* ------------------------------------------------------------ */
442     public boolean isDeleteUnrestorableSessions()
443     {
444         return _deleteUnrestorableSessions;
445     }
446     
447     /* ------------------------------------------------------------ */
448     public void setDeleteUnrestorableSessions(boolean deleteUnrestorableSessions)
449     {
450         _deleteUnrestorableSessions = deleteUnrestorableSessions;
451     }
452 
453     /* ------------------------------------------------------------ */
454     public void restoreSessions () throws Exception
455     {
456         _sessionsLoaded = true;
457 
458         if (_storeDir==null || !_storeDir.exists())
459         {
460             return;
461         }
462 
463         if (!_storeDir.canRead())
464         {
465             __log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath());
466             return;
467         }
468 
469         String[] files = _storeDir.list();
470         for (int i=0;files!=null&&i<files.length;i++)
471         {
472             restoreSession(files[i]);
473         }
474     }
475 
476     /* ------------------------------------------------------------ */
477     protected synchronized HashedSession restoreSession(String idInCuster)
478     {
479         File file = new File(_storeDir,idInCuster);
480         try
481         {
482             if (file.exists())
483             {
484                 FileInputStream in = new FileInputStream(file);
485                 HashedSession session = restoreSession(in, null);
486                 in.close();
487                 addSession(session, false);
488                 session.didActivate();
489                 file.delete();
490                 return session;
491             }
492         }
493         catch (Exception e)
494         {
495            
496             if (isDeleteUnrestorableSessions())
497             {
498                 if (file.exists())
499                 {
500                     file.delete();
501                     __log.warn("Deleting file for unrestorable session "+idInCuster, e);
502                 }
503             }
504             else
505                 __log.warn("Problem restoring session "+idInCuster, e);
506                 
507         }
508         return null;
509     }
510 
511     /* ------------------------------------------------------------ */
512     public void saveSessions(boolean reactivate) throws Exception
513     {
514         if (_storeDir==null || !_storeDir.exists())
515         {
516             return;
517         }
518 
519         if (!_storeDir.canWrite())
520         {
521             __log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable");
522             return;
523         }
524 
525         for (HashedSession session : _sessions.values())
526             session.save(true);
527     }
528 
529     /* ------------------------------------------------------------ */
530     public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception
531     {
532         /*
533          * Take care of this class's fields first by calling
534          * defaultReadObject
535          */
536         DataInputStream in = new DataInputStream(is);
537         String clusterId = in.readUTF();
538         in.readUTF(); // nodeId
539         long created = in.readLong();
540         long accessed = in.readLong();
541         int requests = in.readInt();
542 
543         if (session == null)
544             session = (HashedSession)newSession(created, accessed, clusterId);
545         session.setRequests(requests);
546         int size = in.readInt();
547         if (size>0)
548         {
549             ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
550             for (int i=0; i<size;i++)
551             {
552                 String key = ois.readUTF();
553                 Object value = ois.readObject();
554                 session.setAttribute(key,value);
555             }
556             ois.close();
557         }
558         else
559             in.close();
560         return session;
561     }
562 
563 
564     /* ------------------------------------------------------------ */
565     /* ------------------------------------------------------------ */
566     protected class ClassLoadingObjectInputStream extends ObjectInputStream
567     {
568         /* ------------------------------------------------------------ */
569         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
570         {
571             super(in);
572         }
573 
574         /* ------------------------------------------------------------ */
575         public ClassLoadingObjectInputStream () throws IOException
576         {
577             super();
578         }
579 
580         /* ------------------------------------------------------------ */
581         @Override
582         public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
583         {
584             try
585             {
586                 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
587             }
588             catch (ClassNotFoundException e)
589             {
590                 return super.resolveClass(cl);
591             }
592         }
593     }
594 }