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