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.DataOutputStream;
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.FileNotFoundException;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.ObjectInputStream;
25  import java.io.ObjectOutputStream;
26  import java.io.OutputStream;
27  import java.util.ArrayList;
28  import java.util.Collections;
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.Log;
42  
43  
44  /* ------------------------------------------------------------ */
45  /** An in-memory implementation of SessionManager.
46   * <p>
47   * This manager supports saving sessions to disk, either periodically or at shutdown.
48   * Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions.
49   * <p>
50   * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance
51   * set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler.
52   * 
53   */
54  public class HashSessionManager extends AbstractSessionManager
55  {
56      protected final ConcurrentMap<String,HashedSession> _sessions=new ConcurrentHashMap<String,HashedSession>();
57      private static int __id;
58      private Timer _timer;
59      private boolean _timerStop=false;
60      private TimerTask _task;
61      private int _scavengePeriodMs=30000;
62      private int _savePeriodMs=0; //don't do period saves by default
63      private int _idleSavePeriodMs = 0; // don't idle save sessions by default.
64      private TimerTask _saveTask;
65      private File _storeDir;
66      private boolean _lazyLoad=false;
67      private volatile boolean _sessionsLoaded=false;
68      
69      /* ------------------------------------------------------------ */
70      public HashSessionManager()
71      {
72          super();
73      }
74  
75      /* ------------------------------------------------------------ */
76      /* (non-Javadoc)
77       * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart()
78       */
79      @Override
80      public void doStart() throws Exception
81      {
82          super.doStart();
83  
84          _timerStop=false;
85          ServletContext context = ContextHandler.getCurrentContext();
86          if (context!=null)
87              _timer=(Timer)context.getAttribute("org.eclipse.jetty.server.session.timer");
88          if (_timer==null)
89          {
90              _timerStop=true;
91              _timer=new Timer("HashSessionScavenger-"+__id++, true);
92          }
93          
94          setScavengePeriod(getScavengePeriod());
95  
96          if (_storeDir!=null)
97          {
98              if (!_storeDir.exists())
99                  _storeDir.mkdirs();
100 
101             if (!_lazyLoad)
102                 restoreSessions();
103         }
104  
105         setSavePeriod(getSavePeriod());
106     }
107 
108     /* ------------------------------------------------------------ */
109     /* (non-Javadoc)
110      * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop()
111      */
112     @Override
113     public void doStop() throws Exception
114     {     
115         // stop the scavengers
116         synchronized(this)
117         {
118             if (_saveTask!=null)
119                 _saveTask.cancel();
120             _saveTask=null;
121             if (_task!=null)
122                 _task.cancel();
123             _task=null;
124             if (_timer!=null && _timerStop)
125                 _timer.cancel();
126             _timer=null;
127         }
128         
129         // This will callback invalidate sessions - where we decide if we will save 
130         super.doStop();
131  
132         _sessions.clear();
133 
134     }
135 
136     /* ------------------------------------------------------------ */
137     /** 
138      * @return the period in seconds at which a check is made for sessions to be invalidated.
139      */
140     public int getScavengePeriod()
141     {
142         return _scavengePeriodMs/1000;
143     }
144 
145     
146     /* ------------------------------------------------------------ */
147     @Override
148     public Map getSessionMap()
149     {
150         return Collections.unmodifiableMap(_sessions);
151     }
152 
153 
154     /* ------------------------------------------------------------ */
155     @Override
156     public int getSessions()
157     {
158         int sessions=super.getSessions();
159         if (Log.isDebugEnabled())
160         {
161             if (_sessions.size()!=sessions)
162                 Log.warn("sessions: "+_sessions.size()+"!="+sessions);
163         }
164         return sessions;
165     }
166 
167     /* ------------------------------------------------------------ */
168     /**
169      * @return seconds Idle period after which a session is saved 
170      */
171     public int getIdleSavePeriod()
172     {
173       if (_idleSavePeriodMs <= 0)
174         return 0;
175 
176       return _idleSavePeriodMs / 1000;
177     }
178     
179     /* ------------------------------------------------------------ */
180     /**
181      * Configures the period in seconds after which a session is deemed idle and saved 
182      * to save on session memory.  
183      * 
184      * The session is persisted, the values attribute map is cleared and the session set to idled. 
185      * 
186      * @param seconds Idle period after which a session is saved 
187      */
188     public void setIdleSavePeriod(int seconds)
189     {
190       _idleSavePeriodMs = seconds * 1000;
191     }
192 
193     /* ------------------------------------------------------------ */
194     @Override
195     public void setMaxInactiveInterval(int seconds)
196     {
197         super.setMaxInactiveInterval(seconds);
198         if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000)
199             setScavengePeriod((_dftMaxIdleSecs+9)/10);
200     }
201 
202     /* ------------------------------------------------------------ */
203     /**
204      * @param seconds the period is seconds at which sessions are periodically saved to disk
205      */
206     public void setSavePeriod (int seconds)
207     {
208         int period = (seconds * 1000);
209         if (period < 0)
210             period=0;
211         _savePeriodMs=period;
212         
213         if (_timer!=null)
214         {
215             synchronized (this)
216             {
217                 if (_saveTask!=null)
218                     _saveTask.cancel();
219                 if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured
220                 {
221                     _saveTask = new TimerTask()
222                     {
223                         @Override
224                         public void run()
225                         {
226                             try
227                             {
228                                 saveSessions(true);
229                             }
230                             catch (Exception e)
231                             {
232                                 Log.warn(e);
233                             }
234                         }   
235                     };
236                     _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs);
237                 }
238             }
239         }
240     }
241 
242     /* ------------------------------------------------------------ */
243     /**
244      * @return the period in seconds at which sessions are periodically saved to disk
245      */
246     public int getSavePeriod ()
247     {
248         if (_savePeriodMs<=0)
249             return 0;
250         
251         return _savePeriodMs/1000;
252     }
253     
254     /* ------------------------------------------------------------ */
255     /**
256      * @param seconds the period in seconds at which a check is made for sessions to be invalidated.
257      */
258     public void setScavengePeriod(int seconds)
259     {
260         if (seconds==0)
261             seconds=60;
262 
263         int old_period=_scavengePeriodMs;
264         int period=seconds*1000;
265         if (period>60000)
266             period=60000;
267         if (period<1000)
268             period=1000;
269 
270         _scavengePeriodMs=period;
271         if (_timer!=null && (period!=old_period || _task==null))
272         {
273             synchronized (this)
274             {
275                 if (_task!=null)
276                     _task.cancel();
277                 _task = new TimerTask()
278                 {
279                     @Override
280                     public void run()
281                     {
282                         scavenge();
283                     }   
284                 };
285                 _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs);
286             }
287         }
288     }
289     
290     /* -------------------------------------------------------------- */
291     /**
292      * Find sessions that have timed out and invalidate them. This runs in the
293      * SessionScavenger thread.
294      */
295     protected void scavenge()
296     {
297         //don't attempt to scavenge if we are shutting down
298         if (isStopping() || isStopped())
299             return;
300         
301         Thread thread=Thread.currentThread();
302         ClassLoader old_loader=thread.getContextClassLoader();
303         try
304         {
305             if (_loader!=null)
306                 thread.setContextClassLoader(_loader);
307             
308             // For each session
309             long now=System.currentTimeMillis();
310             for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();)
311             {
312                 HashedSession session=i.next();
313                 long idleTime=session._maxIdleMs;
314                 if (idleTime>0&&session._accessed+idleTime<now)
315                 {
316                     // Found a stale session, add it to the list
317                     session.timeout();
318                 }
319                 else if (_idleSavePeriodMs>0&&session._accessed+_idleSavePeriodMs<now)
320                 {
321                     session.idle(); 
322                 }
323             }
324         }
325         catch (Throwable t)
326         {
327             if (t instanceof ThreadDeath)
328                 throw ((ThreadDeath)t);
329             else
330                 Log.warn("Problem scavenging sessions", t);
331         }
332         finally
333         {
334             thread.setContextClassLoader(old_loader);
335         }
336     }
337     
338     /* ------------------------------------------------------------ */
339     @Override
340     protected void addSession(AbstractSessionManager.Session session)
341     {
342         if (isRunning())
343             _sessions.put(session.getClusterId(),(HashedSession)session);
344     }
345     
346     /* ------------------------------------------------------------ */
347     @Override
348     public AbstractSessionManager.Session getSession(String idInCluster)
349     {
350         if ( _lazyLoad && !_sessionsLoaded)
351         {
352             try
353             {
354                 restoreSessions();
355             }
356             catch(Exception e)
357             {
358                 Log.warn(e);
359             }
360         }
361 
362         Map<String,HashedSession> sessions=_sessions;
363         if (sessions==null)
364             return null;
365         
366         HashedSession session = sessions.get(idInCluster);
367 
368         if (session == null && _lazyLoad)
369             session=restoreSession(idInCluster);
370         if (session == null)
371             return null;
372 
373         if (_idleSavePeriodMs!=0)
374             session.deIdle();
375         
376         return session;
377     }
378 
379     /* ------------------------------------------------------------ */
380     @Override
381     protected void invalidateSessions() throws Exception
382     {
383         // Invalidate all sessions to cause unbind events
384         ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values());
385         int loop=100;
386         while (sessions.size()>0 && loop-->0)
387         {
388             // If we are called from doStop
389             if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite())
390             {
391                 // Then we only save and remove the session - it is not invalidated.
392                 for (HashedSession session : sessions)
393                 {
394                     session.save(false);
395                     removeSession(session,false);
396                 }
397             }
398             else
399             {
400                 for (HashedSession session : sessions)
401                     session.invalidate();
402             }
403             
404             // check that no new sessions were created while we were iterating
405             sessions=new ArrayList<HashedSession>(_sessions.values());
406         }
407     }
408 
409     /* ------------------------------------------------------------ */
410     @Override
411     protected AbstractSessionManager.Session newSession(HttpServletRequest request)
412     {
413         return new HashedSession(request);
414     }
415     
416     /* ------------------------------------------------------------ */
417     protected AbstractSessionManager.Session newSession(long created, long accessed, String clusterId)
418     {
419         return new HashedSession(created,accessed, clusterId);
420     }
421     
422     /* ------------------------------------------------------------ */
423     @Override
424     protected boolean removeSession(String clusterId)
425     {
426         return _sessions.remove(clusterId)!=null;
427     }
428 
429     /* ------------------------------------------------------------ */
430     public void setStoreDirectory (File dir)
431     {
432         _storeDir=dir;
433     }
434 
435     /* ------------------------------------------------------------ */
436     public File getStoreDirectory ()
437     {
438         return _storeDir;
439     }
440 
441     /* ------------------------------------------------------------ */
442     public void setLazyLoad(boolean lazyLoad)
443     {
444         _lazyLoad = lazyLoad;
445     }
446     
447     /* ------------------------------------------------------------ */
448     public boolean isLazyLoad()
449     {
450         return _lazyLoad;
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         try
480         {
481             File file = new File(_storeDir,idInCuster);
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             Log.warn("Problem restoring session "+idInCuster, e);
496         }
497         return null;
498     }
499     
500     /* ------------------------------------------------------------ */
501     public void saveSessions(boolean reactivate) throws Exception
502     {
503         if (_storeDir==null || !_storeDir.exists())
504         {
505             return;
506         }
507         
508         if (!_storeDir.canWrite())
509         {
510             Log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable");
511             return;
512         }
513 
514         for (HashedSession session : _sessions.values())
515             session.save(true);
516     }
517 
518     /* ------------------------------------------------------------ */
519     public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception
520     {
521         /*
522          * Take care of this class's fields first by calling 
523          * defaultReadObject
524          */
525         DataInputStream in = new DataInputStream(is);
526         String clusterId = in.readUTF();
527         String nodeId = in.readUTF();
528         boolean idChanged = in.readBoolean();
529         long created = in.readLong();
530         long cookieSet = in.readLong();
531         long accessed = in.readLong();
532         long lastAccessed = in.readLong();
533         //boolean invalid = in.readBoolean();
534         //boolean invalidate = in.readBoolean();
535         //long maxIdle = in.readLong();
536         //boolean isNew = in.readBoolean();
537         int requests = in.readInt();
538       
539         if (session == null)
540             session = (HashedSession)newSession(created, System.currentTimeMillis(), clusterId);
541         
542         session._cookieSet = cookieSet;
543         session._lastAccessed = lastAccessed;
544         
545         int size = in.readInt();
546         if (size>0)
547         {
548             ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
549             for (int i=0; i<size;i++)
550             {
551                 String key = ois.readUTF();
552                 Object value = ois.readObject();
553                 session.setAttribute(key,value);
554             }
555             ois.close();
556         }
557         else
558             in.close();
559         return session;
560     }
561 
562     
563     /* ------------------------------------------------------------ */
564     /* ------------------------------------------------------------ */
565     /* ------------------------------------------------------------ */
566     protected class HashedSession extends Session
567     {
568         /* ------------------------------------------------------------ */
569         private static final long serialVersionUID=-2134521374206116367L;
570         
571         /** Whether the session has been saved because it has been deemed idle; 
572          * in which case its attribute map will have been saved and cleared. */
573         private transient boolean _idled = false;
574  
575         /** Whether there has already been an attempt to save this session
576          * which has failed.  If there has, there will be no more save attempts
577          * for this session.  This is to stop the logs being flooded with errors
578          * due to serialization failures that are most likely caused by user
579          * data stored in the session that is not serializable. */
580         private transient boolean _saveFailed = false;
581  
582         /* ------------------------------------------------------------- */
583         protected HashedSession(HttpServletRequest request)
584         {
585             super(request);
586         }
587 
588         /* ------------------------------------------------------------- */
589         protected HashedSession(long created, long accessed, String clusterId)
590         {
591             super(created, accessed, clusterId);
592         }
593 
594         /* ------------------------------------------------------------- */
595         protected boolean isNotAvailable()
596         {
597             if (_idleSavePeriodMs!=0)
598                 deIdle();
599             return _invalid;
600         }
601         
602         /* ------------------------------------------------------------- */
603         @Override
604         public void setMaxInactiveInterval(int secs)
605         {
606             super.setMaxInactiveInterval(secs);
607             if (_maxIdleMs>0&&(_maxIdleMs/10)<_scavengePeriodMs)
608                 HashSessionManager.this.setScavengePeriod((secs+9)/10);
609         }
610 
611         /* ------------------------------------------------------------ */
612         @Override
613         protected void doInvalidate()
614         throws IllegalStateException
615         {
616             super.doInvalidate();
617             
618             // Remove from the disk
619             if (_storeDir!=null && getId()!=null)
620             {
621                 String id=getId();
622                 File f = new File(_storeDir, id);
623                 f.delete();
624             }
625         }
626 
627         /* ------------------------------------------------------------ */
628         private synchronized void save(boolean reactivate)
629         {
630             // Only idle the session if not already idled and no previous save/idle has failed
631             if (!isIdled() && !_saveFailed)
632             {
633                 if (Log.isDebugEnabled())
634                     Log.debug("Saving {} {}",super.getId(),reactivate);
635 
636                 File file = null;
637                 FileOutputStream fos = null;
638                 
639                 try
640                 {
641                     file = new File(_storeDir, super.getId());
642 
643                     if (file.exists())
644                         file.delete();
645                     file.createNewFile();
646                     fos = new FileOutputStream(file);
647                     willPassivate();
648                     save(fos);
649                     if (reactivate)
650                         didActivate();
651                     else
652                         clearAttributes();
653                 }
654                 catch (Exception e)
655                 {
656                     saveFailed(); // We won't try again for this session
657 
658                     Log.warn("Problem saving session " + super.getId(), e);
659 
660                     if (fos != null)
661                     {
662                         // Must not leave the file open if the saving failed
663                         IO.close(fos);
664                         // No point keeping the file if we didn't save the whole session
665                         file.delete();
666                         _idled=false; // assume problem was before _values.clear();
667                     }
668                 }
669             }
670         }
671         /* ------------------------------------------------------------ */
672         public synchronized void save(OutputStream os)  throws IOException 
673         {
674             DataOutputStream out = new DataOutputStream(os);
675             out.writeUTF(_clusterId);
676             out.writeUTF(_nodeId);
677             out.writeBoolean(_idChanged);
678             out.writeLong( _created);
679             out.writeLong(_cookieSet);
680             out.writeLong(_accessed);
681             out.writeLong(_lastAccessed);
682             
683             /* Don't write these out, as they don't make sense to store because they
684              * either they cannot be true or their value will be restored in the 
685              * Session constructor.
686              */
687             //out.writeBoolean(_invalid);
688             //out.writeBoolean(_doInvalidate);
689             //out.writeLong(_maxIdleMs);
690             //out.writeBoolean( _newSession);
691             out.writeInt(_requests);
692             if (_attributes != null)
693             {
694                 out.writeInt(_attributes.size());
695                 ObjectOutputStream oos = new ObjectOutputStream(out);
696                 for (Map.Entry<String,Object> entry: _attributes.entrySet())
697                 {
698                     oos.writeUTF(entry.getKey());
699                     oos.writeObject(entry.getValue());
700                 }
701                 oos.close();
702             }
703             else
704             {
705                 out.writeInt(0);
706                 out.close();
707             }
708         }
709 
710         /* ------------------------------------------------------------ */
711         public synchronized void deIdle()
712         {
713             if (isIdled())
714             {
715                 // Access now to prevent race with idling period
716                 access(System.currentTimeMillis());
717 
718                 
719                 if (Log.isDebugEnabled())
720                 {
721                     Log.debug("Deidling " + super.getId());
722                 }
723 
724                 FileInputStream fis = null;
725 
726                 try
727                 {
728                     File file = new File(_storeDir, super.getId());
729                     if (!file.exists() || !file.canRead())
730                         throw new FileNotFoundException(file.getName());
731 
732                     fis = new FileInputStream(file);
733                     _idled = false;
734                     restoreSession(fis, this);
735 
736                     didActivate();
737                     
738                     // If we are doing period saves, then there is no point deleting at this point 
739                     if (_savePeriodMs == 0)
740                         file.delete();
741                 }
742                 catch (Exception e)
743                 {
744                     Log.warn("Problem deidling session " + super.getId(), e);
745                     IO.close(fis);
746                     invalidate();
747                 }
748             }
749         }
750 
751         
752         /* ------------------------------------------------------------ */
753         /**
754          * Idle the session to reduce session memory footprint.
755          * 
756          * The session is idled by persisting it, then clearing the session values attribute map and finally setting 
757          * it to an idled state.  
758          */
759         public synchronized void idle()
760         {
761             save(false);
762         }
763         
764         /* ------------------------------------------------------------ */
765         public boolean isIdled()
766         {
767           return _idled;
768         }
769 
770         /* ------------------------------------------------------------ */
771         public boolean isSaveFailed()
772         {
773           return _saveFailed;
774         }
775         
776         /* ------------------------------------------------------------ */
777         public void saveFailed()
778         {
779           _saveFailed = true;
780         }
781 
782     }
783     
784 
785     /* ------------------------------------------------------------ */
786     /* ------------------------------------------------------------ */
787     protected class ClassLoadingObjectInputStream extends ObjectInputStream
788     {
789         /* ------------------------------------------------------------ */
790         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
791         {
792             super(in);
793         }
794 
795         /* ------------------------------------------------------------ */
796         public ClassLoadingObjectInputStream () throws IOException
797         {
798             super();
799         }
800 
801         /* ------------------------------------------------------------ */
802         @Override
803         public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
804         {
805             try
806             {
807                 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
808             }
809             catch (ClassNotFoundException e)
810             {
811                 return super.resolveClass(cl);
812             }
813         }
814     }
815 }