View Javadoc

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