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