View Javadoc

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