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.HashMap;
30  import java.util.Iterator;
31  import java.util.Map;
32  import java.util.Timer;
33  import java.util.TimerTask;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.ConcurrentMap;
36  
37  import javax.servlet.http.HttpServletRequest;
38  
39  import org.eclipse.jetty.util.IO;
40  import org.eclipse.jetty.util.LazyList;
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   * 
50   */
51  public class HashSessionManager extends AbstractSessionManager
52  {
53      private static int __id;
54      private Timer _timer;
55      private TimerTask _task;
56      private int _scavengePeriodMs=30000;
57      private int _savePeriodMs=0; //don't do period saves by default
58      private int _idleSavePeriodMs = 0; // don't idle save sessions by default.
59      private TimerTask _saveTask;
60      protected ConcurrentMap<String,HashedSession> _sessions;
61      private File _storeDir;
62      private boolean _lazyLoad=false;
63      private volatile boolean _sessionsLoaded=false;
64      
65      /* ------------------------------------------------------------ */
66      public HashSessionManager()
67      {
68          super();
69      }
70  
71      /* ------------------------------------------------------------ */
72      /* (non-Javadoc)
73       * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart()
74       */
75      @Override
76      public void doStart() throws Exception
77      {
78          _sessions=new ConcurrentHashMap<String,HashedSession>(); 
79          super.doStart();
80  
81          _timer=new Timer("HashSessionScavenger-"+__id++, true);
82          
83          setScavengePeriod(getScavengePeriod());
84  
85          if (_storeDir!=null)
86          {
87              if (!_storeDir.exists())
88                  _storeDir.mkdirs();
89  
90              if (!_lazyLoad)
91                  restoreSessions();
92          }
93   
94          setSavePeriod(getSavePeriod());
95      }
96  
97      /* ------------------------------------------------------------ */
98      /* (non-Javadoc)
99       * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop()
100      */
101     @Override
102     public void doStop() throws Exception
103     {     
104         if (_storeDir != null)
105             saveSessions();
106         
107         super.doStop();
108  
109         _sessions.clear();
110         _sessions=null;
111 
112         // stop the scavenger
113         synchronized(this)
114         {
115             if (_saveTask!=null)
116                 _saveTask.cancel();
117             _saveTask=null;
118             if (_task!=null)
119                 _task.cancel();
120             _task=null;
121             if (_timer!=null)
122                 _timer.cancel();
123             _timer=null;
124         }
125     }
126 
127     /* ------------------------------------------------------------ */
128     /** 
129      * @return the period in seconds at which a check is made for sessions to be invalidated.
130      */
131     public int getScavengePeriod()
132     {
133         return _scavengePeriodMs/1000;
134     }
135 
136     
137     /* ------------------------------------------------------------ */
138     @Override
139     public Map getSessionMap()
140     {
141         return Collections.unmodifiableMap(_sessions);
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 _idleSavePeriodMs;
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 * 1000;
182     }
183 
184     /* ------------------------------------------------------------ */
185     @Override
186     public void setMaxInactiveInterval(int seconds)
187     {
188         super.setMaxInactiveInterval(seconds);
189         if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000)
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         int period = (seconds * 1000);
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();
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 _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         int old_period=_scavengePeriodMs;
255         int period=seconds*1000;
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             try
300             {
301                 if (!_sessionsLoaded && _lazyLoad)
302                     restoreSessions();
303             }
304             catch(Exception e)
305             {
306                 Log.debug(e);
307             }
308             
309             // For each session
310             long now=System.currentTimeMillis();
311             for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();)
312             {
313                 HashedSession session=i.next();
314                 long idleTime=session._maxIdleMs;
315                 if (idleTime>0&&session._accessed+idleTime<now)
316                 {
317                     // Found a stale session, add it to the list
318                     session.timeout();
319                 }
320                 else if (_idleSavePeriodMs>0&&session._accessed+_idleSavePeriodMs<now)
321                 {
322                     session.idle(); 
323                 }
324             }
325         }
326         catch (Throwable t)
327         {
328             if (t instanceof ThreadDeath)
329                 throw ((ThreadDeath)t);
330             else
331                 Log.warn("Problem scavenging sessions", t);
332         }
333         finally
334         {
335             thread.setContextClassLoader(old_loader);
336         }
337     }
338     
339     /* ------------------------------------------------------------ */
340     @Override
341     protected void addSession(AbstractSessionManager.Session session)
342     {
343         if (isRunning())
344             _sessions.put(session.getClusterId(),(HashedSession)session);
345     }
346     
347     /* ------------------------------------------------------------ */
348     @Override
349     public AbstractSessionManager.Session getSession(String idInCluster)
350     {
351         if ( _lazyLoad && !_sessionsLoaded)
352         {
353             try
354             {
355                 restoreSessions();
356             }
357             catch(Exception e)
358             {
359                 Log.warn(e);
360             }
361         }
362 
363         Map<String,HashedSession> sessions=_sessions;
364         if (sessions==null)
365             return null;
366         
367         HashedSession session = sessions.get(idInCluster);
368         
369         if (session == null)
370             return null;
371 
372         if (_idleSavePeriodMs!=0)
373             session.deIdle();
374         
375         return session;
376     }
377 
378     /* ------------------------------------------------------------ */
379     @Override
380     protected void invalidateSessions()
381     {
382         // Invalidate all sessions to cause unbind events
383         ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values());
384         for (Iterator<HashedSession> i=sessions.iterator(); i.hasNext();)
385         {
386             HashedSession session=i.next();
387             session.invalidate();
388         }
389         _sessions.clear();
390     }
391 
392     /* ------------------------------------------------------------ */
393     @Override
394     protected AbstractSessionManager.Session newSession(HttpServletRequest request)
395     {
396         return new HashedSession(request);
397     }
398     
399     /* ------------------------------------------------------------ */
400     protected AbstractSessionManager.Session newSession(long created, long accessed, String clusterId)
401     {
402         return new HashedSession(created,accessed, clusterId);
403     }
404     
405     /* ------------------------------------------------------------ */
406     @Override
407     protected boolean removeSession(String clusterId)
408     {
409         return _sessions.remove(clusterId)!=null;
410     }
411     
412 
413     /* ------------------------------------------------------------ */
414     public void setStoreDirectory (File dir)
415     {
416         _storeDir=dir;
417     }
418 
419     /* ------------------------------------------------------------ */
420     public File getStoreDirectory ()
421     {
422         return _storeDir;
423     }
424 
425     /* ------------------------------------------------------------ */
426     public void setLazyLoad(boolean lazyLoad)
427     {
428         _lazyLoad = lazyLoad;
429     }
430     
431     /* ------------------------------------------------------------ */
432     public boolean isLazyLoad()
433     {
434         return _lazyLoad;
435     }
436 
437     /* ------------------------------------------------------------ */
438     public void restoreSessions () throws Exception
439     {
440         _sessionsLoaded = true;
441         
442         if (_storeDir==null || !_storeDir.exists())
443         {
444             return;
445         }
446 
447         if (!_storeDir.canRead())
448         {
449             Log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath());
450             return;
451         }
452 
453         File[] files = _storeDir.listFiles();
454         for (int i=0;files!=null&&i<files.length;i++)
455         {
456             try
457             {
458                 FileInputStream in = new FileInputStream(files[i]);           
459                 HashedSession session = restoreSession(in, null);
460                 in.close();          
461                 addSession(session, false);
462                 session.didActivate();
463                 files[i].delete();
464             }
465             catch (Exception e)
466             {
467                 Log.warn("Problem restoring session "+files[i].getName(), e);
468             }
469         }
470     }
471 
472     /* ------------------------------------------------------------ */
473     public void saveSessions() throws Exception
474     {
475         if (_storeDir==null || !_storeDir.exists())
476         {
477             return;
478         }
479         
480         if (!_storeDir.canWrite())
481         {
482             Log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable");
483             return;
484         }
485 
486         Iterator<Map.Entry<String, HashedSession>> itor = _sessions.entrySet().iterator();
487         while (itor.hasNext())
488         {
489             Map.Entry<String,HashedSession> entry = itor.next();
490             String id = entry.getKey();
491             HashedSession session = entry.getValue();
492             synchronized(session)
493             {
494                 // No point saving a session that has been idled or has had a previous save failure
495                 if (!session.isIdled() && !session.isSaveFailed())
496                 {
497                     File file = null;
498                     FileOutputStream fos = null;
499                     try
500                     {
501                         file = new File (_storeDir, id);
502                         if (file.exists())
503                             file.delete();
504                         file.createNewFile();
505                         fos = new FileOutputStream (file);
506                         session.willPassivate();
507                         session.save(fos);
508                         session.didActivate();
509                         fos.close();
510                     }
511                     catch (Exception e)
512                     {
513                         session.saveFailed();
514 
515                         Log.warn("Problem persisting session "+id, e);
516 
517                         if (fos != null)
518                         {
519                             // Must not leave files open if the saving failed
520                             IO.close(fos);
521                             // No point keeping the file if we didn't save the whole session
522                             file.delete();
523                         }
524                     }
525                 }
526             }
527         }
528     }
529 
530     /* ------------------------------------------------------------ */
531     public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception
532     {
533         /*
534          * Take care of this class's fields first by calling 
535          * defaultReadObject
536          */
537         DataInputStream in = new DataInputStream(is);
538         String clusterId = in.readUTF();
539         String nodeId = in.readUTF();
540         boolean idChanged = in.readBoolean();
541         long created = in.readLong();
542         long cookieSet = in.readLong();
543         long accessed = in.readLong();
544         long lastAccessed = in.readLong();
545         //boolean invalid = in.readBoolean();
546         //boolean invalidate = in.readBoolean();
547         //long maxIdle = in.readLong();
548         //boolean isNew = in.readBoolean();
549         int requests = in.readInt();
550       
551         if (session == null)
552             session = (HashedSession)newSession(created, System.currentTimeMillis(), clusterId);
553         
554         session._cookieSet = cookieSet;
555         session._lastAccessed = lastAccessed;
556         
557         int size = in.readInt();
558         if (size>0)
559         {
560             ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
561             for (int i=0; i<size;i++)
562             {
563                 String key = ois.readUTF();
564                 Object value = ois.readObject();
565                 session.setAttribute(key,value);
566             }
567             ois.close();
568         }
569         else
570             in.close();
571         return session;
572     }
573 
574     
575     /* ------------------------------------------------------------ */
576     /* ------------------------------------------------------------ */
577     /* ------------------------------------------------------------ */
578     protected class HashedSession extends Session
579     {
580         /* ------------------------------------------------------------ */
581         private static final long serialVersionUID=-2134521374206116367L;
582         
583         /** Whether the session has been saved because it has been deemed idle; 
584          * in which case its attribute map will have been saved and cleared. */
585         private transient boolean _idled = false;
586  
587         /** Whether there has already been an attempt to save this session
588          * which has failed.  If there has, there will be no more save attempts
589          * for this session.  This is to stop the logs being flooded with errors
590          * due to serialization failures that are most likely caused by user
591          * data stored in the session that is not serializable. */
592         private transient boolean _saveFailed = false;
593  
594         /* ------------------------------------------------------------- */
595         protected HashedSession(HttpServletRequest request)
596         {
597             super(request);
598         }
599 
600         /* ------------------------------------------------------------- */
601         protected HashedSession(long created, long accessed, String clusterId)
602         {
603             super(created, accessed, clusterId);
604         }
605 
606         /* ------------------------------------------------------------- */
607         protected boolean isNotAvailable()
608         {
609             if (_idleSavePeriodMs!=0)
610                 deIdle();
611             return _invalid;
612         }
613         
614         /* ------------------------------------------------------------- */
615         @Override
616         public void setMaxInactiveInterval(int secs)
617         {
618             super.setMaxInactiveInterval(secs);
619             if (_maxIdleMs>0&&(_maxIdleMs/10)<_scavengePeriodMs)
620                 HashSessionManager.this.setScavengePeriod((secs+9)/10);
621         }
622 
623         /* ------------------------------------------------------------ */
624         @Override
625         public void invalidate ()
626         throws IllegalStateException
627         {
628             if (isRunning())
629             {
630                 super.invalidate();
631                 remove();
632             }
633         }
634 
635         /* ------------------------------------------------------------ */
636         public void remove()
637         {
638             String id=getId();
639             if (id==null)
640                 return;
641             
642             //all sessions are invalidated when jetty is stopped, make sure we don't
643             //remove all the sessions in this case
644             if (isStopping() || isStopped())
645                 return;
646             
647             if (_storeDir==null || !_storeDir.exists())
648             {
649                 return;
650             }
651             
652             File f = new File(_storeDir, id);
653             f.delete();
654         }
655 
656         /* ------------------------------------------------------------ */
657         public synchronized void save(OutputStream os)  throws IOException 
658         {
659             DataOutputStream out = new DataOutputStream(os);
660             out.writeUTF(_clusterId);
661             out.writeUTF(_nodeId);
662             out.writeBoolean(_idChanged);
663             out.writeLong( _created);
664             out.writeLong(_cookieSet);
665             out.writeLong(_accessed);
666             out.writeLong(_lastAccessed);
667             
668             /* Don't write these out, as they don't make sense to store because they
669              * either they cannot be true or their value will be restored in the 
670              * Session constructor.
671              */
672             //out.writeBoolean(_invalid);
673             //out.writeBoolean(_doInvalidate);
674             //out.writeLong(_maxIdleMs);
675             //out.writeBoolean( _newSession);
676             out.writeInt(_requests);
677             if (_attributes != null)
678             {
679                 out.writeInt(_attributes.size());
680                 ObjectOutputStream oos = new ObjectOutputStream(out);
681                 for (Map.Entry<String,Object> entry: _attributes.entrySet())
682                 {
683                     oos.writeUTF(entry.getKey());
684                     oos.writeObject(entry.getValue());
685                 }
686                 oos.close();
687             }
688             else
689             {
690                 out.writeInt(0);
691                 out.close();
692             }
693         }
694 
695         /* ------------------------------------------------------------ */
696         public synchronized void deIdle()
697         {
698             if (isIdled())
699             {
700                 // Access now to prevent race with idling period
701                 access(System.currentTimeMillis());
702                 
703                 if (Log.isDebugEnabled())
704                 {
705                     Log.debug("Deidling " + super.getId());
706                 }
707 
708                 FileInputStream fis = null;
709 
710                 try
711                 {
712                     File file = new File(_storeDir, super.getId());
713                     if (!file.exists() || !file.canRead())
714                         throw new FileNotFoundException(file.getName());
715 
716                     fis = new FileInputStream(file);
717                     restoreSession(fis, this);
718 
719                     _idled = false;
720                     didActivate();
721                     
722                     // If we are doing period saves, then there is no point deleting at this point 
723                     if (_savePeriodMs == 0)
724                         file.delete();
725                 }
726                 catch (Exception e)
727                 {
728                     Log.warn("Problem deidling session " + super.getId(), e);
729                     IO.close(fis);
730                     invalidate();
731                 }
732             }
733         }
734 
735         /* ------------------------------------------------------------ */
736         /**
737          * Idle the session to reduce session memory footprint.
738          * 
739          * The session is idled by persisting it, then clearing the session values attribute map and finally setting 
740          * it to an idled state.  
741          */
742         public synchronized void idle()
743         {
744             // Only idle the session if not already idled and no previous save/idle has failed
745             if (!isIdled() && !_saveFailed)
746             {
747                 if (Log.isDebugEnabled())
748                     Log.debug("Idling " + super.getId());
749 
750                 File file = null;
751                 FileOutputStream fos = null;
752                 
753                 try
754                 {
755                     file = new File(_storeDir, super.getId());
756 
757                     if (file.exists())
758                         file.delete();
759                     file.createNewFile();
760                     fos = new FileOutputStream(file);
761                     willPassivate();
762                     save(fos);
763 
764                     _attributes.clear();
765 
766                     _idled = true;
767                 }
768                 catch (Exception e)
769                 {
770                     saveFailed(); // We won't try again for this session
771 
772                     Log.warn("Problem idling session " + super.getId(), e);
773 
774                     if (fos != null)
775                     {
776                         // Must not leave the file open if the saving failed
777                         IO.close(fos);
778                         // No point keeping the file if we didn't save the whole session
779                         file.delete();
780                         _idled=false; // assume problem was before _values.clear();
781                     }
782                 }
783             }
784         }
785         
786         /* ------------------------------------------------------------ */
787         public boolean isIdled()
788         {
789           return _idled;
790         }
791 
792         /* ------------------------------------------------------------ */
793         public boolean isSaveFailed()
794         {
795           return _saveFailed;
796         }
797         
798         /* ------------------------------------------------------------ */
799         public void saveFailed()
800         {
801           _saveFailed = true;
802         }
803 
804     }
805     
806 
807     /* ------------------------------------------------------------ */
808     /* ------------------------------------------------------------ */
809     protected class ClassLoadingObjectInputStream extends ObjectInputStream
810     {
811         /* ------------------------------------------------------------ */
812         public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
813         {
814             super(in);
815         }
816 
817         /* ------------------------------------------------------------ */
818         public ClassLoadingObjectInputStream () throws IOException
819         {
820             super();
821         }
822 
823         /* ------------------------------------------------------------ */
824         @Override
825         public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
826         {
827             try
828             {
829                 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
830             }
831             catch (ClassNotFoundException e)
832             {
833                 return super.resolveClass(cl);
834             }
835         }
836     }
837 }