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