View Javadoc

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