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  
20  package org.eclipse.jetty.server.session;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.InputStream;
25  import java.io.ObjectOutputStream;
26  import java.sql.Connection;
27  import java.sql.PreparedStatement;
28  import java.sql.ResultSet;
29  import java.sql.SQLException;
30  import java.util.ArrayList;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.concurrent.ConcurrentHashMap;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.atomic.AtomicReference;
38  
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpSessionEvent;
41  import javax.servlet.http.HttpSessionListener;
42  
43  import org.eclipse.jetty.server.SessionIdManager;
44  import org.eclipse.jetty.server.handler.ContextHandler;
45  import org.eclipse.jetty.server.session.JDBCSessionIdManager.SessionTableSchema;
46  import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
47  import org.eclipse.jetty.util.log.Log;
48  import org.eclipse.jetty.util.log.Logger;
49  
50  /**
51   * JDBCSessionManager.
52   * <p>
53   * SessionManager that persists sessions to a database to enable clustering.
54   * <p>
55   * Session data is persisted to the JettySessions table:
56   * <dl>
57   * <dt>rowId</dt><dd>(unique in cluster: webapp name/path + virtualhost + sessionId)</dd>
58   * <dt>contextPath</dt><dd>(of the context owning the session)</dd>
59   * <dt>sessionId</dt><dd>(unique in a context)</dd>
60   * <dt>lastNode</dt><dd>(name of node last handled session)</dd>
61   * <dt>accessTime</dt><dd>(time in milliseconds session was accessed)</dd>
62   * <dt>lastAccessTime</dt><dd>(previous time in milliseconds session was accessed)</dd>
63   * <dt>createTime</dt><dd>(time in milliseconds session created)</dd>
64   * <dt>cookieTime</dt><dd>(time in milliseconds session cookie created)</dd>
65   * <dt>lastSavedTime</dt><dd>(last time in milliseconds session access times were saved)</dd>
66   * <dt>expiryTime</dt><dd>(time in milliseconds that the session is due to expire)</dd>
67   * <dt>map</dt><dd>(attribute map)</dd>
68   * </dl>
69   *
70   * As an optimization, to prevent thrashing the database, we do not persist
71   * the accessTime and lastAccessTime every time the session is accessed. Rather,
72   * we write it out every so often. The frequency is controlled by the saveIntervalSec
73   * field.
74   */
75  public class JDBCSessionManager extends AbstractSessionManager
76  {
77      private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
78  
79      private ConcurrentHashMap<String, Session> _sessions;
80      protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
81      protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
82      protected SessionTableSchema _sessionTableSchema;
83  
84     
85  
86  
87      /**
88       * Session
89       *
90       * Session instance.
91       */
92      public class Session extends MemSession
93      {
94          private static final long serialVersionUID = 5208464051134226143L;
95          
96          /**
97           * If dirty, session needs to be (re)persisted
98           */
99          protected boolean _dirty=false;
100         
101         
102      
103         
104         
105         /**
106          * Time in msec since the epoch that the session will expire
107          */
108         protected long _expiryTime;
109         
110         
111         /**
112          * Time in msec since the epoch that the session was last persisted
113          */
114         protected long _lastSaved;
115         
116         
117         /**
118          * Unique identifier of the last node to host the session
119          */
120         protected String _lastNode;
121         
122         
123         /**
124          * Virtual host for context (used to help distinguish 2 sessions with same id on different contexts)
125          */
126         protected String _virtualHost;
127         
128         
129         /**
130          * Unique row in db for session
131          */
132         protected String _rowId;
133         
134         
135         /**
136          * Mangled context name (used to help distinguish 2 sessions with same id on different contexts)
137          */
138         protected String _canonicalContext;
139         
140    
141         /**
142          * Session from a request.
143          *
144          * @param request the request
145          */
146         protected Session (HttpServletRequest request)
147         {
148             super(JDBCSessionManager.this,request);
149             int maxInterval=getMaxInactiveInterval();
150             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
151             _virtualHost = JDBCSessionManager.getVirtualHost(_context);
152             _canonicalContext = canonicalize(_context.getContextPath());
153             _lastNode = getSessionIdManager().getWorkerName();
154         }
155         
156         
157         /**
158          * Session restored from database
159          * @param sessionId the session id
160          * @param rowId the row id
161          * @param created the created timestamp
162          * @param accessed the access timestamp
163          * @param maxInterval the max inactive interval (in seconds)
164          */
165         protected Session (String sessionId, String rowId, long created, long accessed, long maxInterval)
166         {
167             super(JDBCSessionManager.this, created, accessed, sessionId);
168             _rowId = rowId;
169             super.setMaxInactiveInterval((int)maxInterval); //restore the session's previous inactivity interval setting
170             _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
171         }
172         
173         
174         protected synchronized String getRowId()
175         {
176             return _rowId;
177         }
178         
179         protected synchronized void setRowId(String rowId)
180         {
181             _rowId = rowId;
182         }
183         
184         public synchronized void setVirtualHost (String vhost)
185         {
186             _virtualHost=vhost;
187         }
188 
189         public synchronized String getVirtualHost ()
190         {
191             return _virtualHost;
192         }
193         
194         public synchronized long getLastSaved ()
195         {
196             return _lastSaved;
197         }
198 
199         public synchronized void setLastSaved (long time)
200         {
201             _lastSaved=time;
202         }
203 
204         public synchronized void setExpiryTime (long time)
205         {
206             _expiryTime=time;
207         }
208 
209         public synchronized long getExpiryTime ()
210         {
211             return _expiryTime;
212         }
213         
214 
215         public synchronized void setCanonicalContext(String str)
216         {
217             _canonicalContext=str;
218         }
219 
220         public synchronized String getCanonicalContext ()
221         {
222             return _canonicalContext;
223         }
224         
225        
226         public synchronized void setLastNode (String node)
227         {
228             _lastNode=node;
229         }
230 
231         public synchronized String getLastNode ()
232         {
233             return _lastNode;
234         }
235 
236         @Override
237         public void setAttribute (String name, Object value)
238         {
239             Object old = changeAttribute(name, value);
240             if (value == null && old == null)
241                 return; //if same as remove attribute but attribute was already removed, no change
242             
243             _dirty = true;
244         }
245 
246         @Override
247         public void removeAttribute (String name)
248         {
249             Object old = changeAttribute(name, null);
250             if (old != null) //only dirty if there was a previous value
251                 _dirty=true;
252         }
253 
254 
255         /**
256          * Entry to session.
257          * Called by SessionHandler on inbound request and the session already exists in this node's memory.
258          *
259          * @see org.eclipse.jetty.server.session.AbstractSession#access(long)
260          */
261         @Override
262         protected boolean access(long time)
263         {
264             synchronized (this)
265             {
266                 if (super.access(time))
267                 {
268                     int maxInterval=getMaxInactiveInterval();
269                     _expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L));
270                     return true;
271                 }
272                 return false;
273             }
274         }
275         
276         
277         
278 
279 
280         /** 
281          * Change the max idle time for this session. This recalculates the expiry time.
282          * @see org.eclipse.jetty.server.session.AbstractSession#setMaxInactiveInterval(int)
283          */
284         @Override
285         public void setMaxInactiveInterval(int secs)
286         {
287             synchronized (this)
288             {
289                 super.setMaxInactiveInterval(secs);
290                 int maxInterval=getMaxInactiveInterval();
291                 _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
292                 //force the session to be written out right now
293                 try
294                 {
295                     updateSessionAccessTime(this);
296                 }
297                 catch (Exception e)
298                 {
299                     LOG.warn("Problem saving changed max idle time for session "+ this, e);
300                 }
301             }
302         }
303 
304 
305         /**
306          * Exit from session
307          * @see org.eclipse.jetty.server.session.AbstractSession#complete()
308          */
309         @Override
310         protected void complete()
311         {
312             synchronized (this)
313             {
314                 super.complete();
315                 try
316                 {
317                     if (isValid())
318                     {
319                         if (_dirty)
320                         {
321                             //The session attributes have changed, write to the db, ensuring
322                             //http passivation/activation listeners called
323                             save(true);
324                         }
325                         else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L))
326                         {
327                             updateSessionAccessTime(this);
328                         }
329                     }
330                 }
331                 catch (Exception e)
332                 {
333                     LOG.warn("Problem persisting changed session data id="+getId(), e);
334                 }
335                 finally
336                 {
337                     _dirty=false;
338                 }
339             }
340         }
341 
342         protected void save() throws Exception
343         {
344             synchronized (this)
345             {
346                 try
347                 {
348                     updateSession(this);
349                 }
350                 finally
351                 {
352                     _dirty = false;
353                 }
354             }
355         }
356 
357         protected void save (boolean reactivate) throws Exception
358         {
359             synchronized (this)
360             {
361                 if (_dirty)
362                 {
363                     //The session attributes have changed, write to the db, ensuring
364                     //http passivation/activation listeners called
365                     willPassivate();                      
366                     updateSession(this);
367                     if (reactivate)
368                         didActivate();  
369                 }
370             }
371         }
372 
373         
374         @Override
375         protected void timeout() throws IllegalStateException
376         {
377             if (LOG.isDebugEnabled())
378                 LOG.debug("Timing out session id="+getClusterId());
379             super.timeout();
380         }
381         
382         
383         @Override
384         public String toString ()
385         {
386             return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+
387                             ",created="+getCreationTime()+",accessed="+getAccessed()+
388                             ",lastAccessed="+getLastAccessedTime()+",cookieSet="+getCookieSetTime()+
389                             ",maxInterval="+getMaxInactiveInterval()+",lastSaved="+_lastSaved+",expiry="+_expiryTime;
390         }
391     }
392 
393 
394 
395 
396     /**
397      * Set the time in seconds which is the interval between
398      * saving the session access time to the database.
399      *
400      * This is an optimization that prevents the database from
401      * being overloaded when a session is accessed very frequently.
402      *
403      * On session exit, if the session attributes have NOT changed,
404      * the time at which we last saved the accessed
405      * time is compared to the current accessed time. If the interval
406      * is at least saveIntervalSecs, then the access time will be
407      * persisted to the database.
408      *
409      * If any session attribute does change, then the attributes and
410      * the accessed time are persisted.
411      *
412      * @param sec the save interval in seconds
413      */
414     public void setSaveInterval (long sec)
415     {
416         _saveIntervalSec=sec;
417     }
418 
419     public long getSaveInterval ()
420     {
421         return _saveIntervalSec;
422     }
423 
424 
425 
426     /**
427      * A method that can be implemented in subclasses to support
428      * distributed caching of sessions. This method will be
429      * called whenever the session is written to the database
430      * because the session data has changed.
431      *
432      * This could be used eg with a JMS backplane to notify nodes
433      * that the session has changed and to delete the session from
434      * the node's cache, and re-read it from the database.
435      * @param session the session to invalidate
436      */
437     public void cacheInvalidate (Session session)
438     {
439 
440     }
441 
442 
443     /**
444      * A session has been requested by its id on this node.
445      *
446      * Load the session by id AND context path from the database.
447      * Multiple contexts may share the same session id (due to dispatching)
448      * but they CANNOT share the same contents.
449      *
450      * Check if last node id is my node id, if so, then the session we have
451      * in memory cannot be stale. If another node used the session last, then
452      * we need to refresh from the db.
453      *
454      * NOTE: this method will go to the database, so if you only want to check
455      * for the existence of a Session in memory, use _sessions.get(id) instead.
456      *
457      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String)
458      */
459     @Override
460     public Session getSession(String idInCluster)
461     {
462         Session session = null;
463         
464         synchronized (this)
465         {
466             Session memSession = (Session)_sessions.get(idInCluster);
467             
468             //check if we need to reload the session -
469             //as an optimization, don't reload on every access
470             //to reduce the load on the database. This introduces a window of
471             //possibility that the node may decide that the session is local to it,
472             //when the session has actually been live on another node, and then
473             //re-migrated to this node. This should be an extremely rare occurrence,
474             //as load-balancers are generally well-behaved and consistently send
475             //sessions to the same node, changing only iff that node fails.
476             //Session data = null;
477             long now = System.currentTimeMillis();
478             if (LOG.isDebugEnabled())
479             {
480                 if (memSession==null)
481                     LOG.debug("getSession("+idInCluster+"): not in session map,"+
482                             " now="+now+
483                             " lastSaved="+(memSession==null?0:memSession._lastSaved)+
484                             " interval="+(_saveIntervalSec * 1000L));
485                 else
486                     LOG.debug("getSession("+idInCluster+"): in session map, "+
487                             " hashcode="+memSession.hashCode()+
488                             " now="+now+
489                             " lastSaved="+(memSession==null?0:memSession._lastSaved)+
490                             " interval="+(_saveIntervalSec * 1000L)+
491                             " lastNode="+memSession._lastNode+
492                             " thisNode="+getSessionIdManager().getWorkerName()+
493                             " difference="+(now - memSession._lastSaved));
494             }
495 
496             try
497             {
498                 if (memSession==null)
499                 {
500                     if (LOG.isDebugEnabled())
501                         LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db.");
502                     session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
503                 }
504                 else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L))
505                 {
506                     if (LOG.isDebugEnabled())
507                         LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db.");
508                     session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
509                 }
510                 else
511                 {
512                     if (LOG.isDebugEnabled())
513                         LOG.debug("getSession("+idInCluster+"): session in session map");
514                     session = memSession;
515                 }
516             }
517             catch (Exception e)
518             {
519                 LOG.warn("Unable to load session "+idInCluster, e);
520                 return null;
521             }
522 
523 
524             //If we have a session
525             if (session != null)
526             {
527                 //If the session was last used on a different node, or session doesn't exist on this node
528                 if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null)
529                 {
530                     //if session doesn't expire, or has not already expired, update it and put it in this nodes' memory
531                     if (session._expiryTime <= 0 || session._expiryTime > now)
532                     {
533                         if (LOG.isDebugEnabled()) 
534                             LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName());
535 
536                         session.setLastNode(getSessionIdManager().getWorkerName());                            
537                         _sessions.put(idInCluster, session);
538 
539                         //update in db
540                         try
541                         {
542                             updateSessionNode(session);
543                             session.didActivate();
544                         }
545                         catch (Exception e)
546                         {
547                             LOG.warn("Unable to update freshly loaded session "+idInCluster, e);
548                             return null;
549                         }
550                     }
551                     else
552                     {
553                         if (LOG.isDebugEnabled())
554                             LOG.debug("getSession ({}): Session has expired", idInCluster);
555                         //ensure that the session id for the expired session is deleted so that a new session with the 
556                         //same id cannot be created (because the idInUse() test would succeed)
557                         _jdbcSessionIdMgr.removeSession(idInCluster);
558                         session=null;
559                     }
560 
561                 }
562                 else
563                 {
564                     //the session loaded from the db and the one in memory are the same, so keep using the one in memory
565                     session = memSession;
566                     if (LOG.isDebugEnabled())
567                         LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
568                 }
569             }
570             else
571             {
572                 if (memSession != null)
573                 {
574                     //Session must have been removed from db by another node
575                     removeSession(memSession, true);
576                 }
577                 //No session in db with matching id and context path.
578                 LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
579             }
580 
581             return session;
582         }
583     }
584     
585 
586     /**
587      * Get the number of sessions.
588      *
589      * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
590      */
591     @Override
592     public int getSessions()
593     {
594         return _sessions.size();
595     }
596 
597 
598     /**
599      * Start the session manager.
600      *
601      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
602      */
603     @Override
604     public void doStart() throws Exception
605     {
606         if (_sessionIdManager==null)
607             throw new IllegalStateException("No session id manager defined");
608 
609         _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
610         _sessionTableSchema = _jdbcSessionIdMgr.getSessionTableSchema();
611 
612         _sessions = new ConcurrentHashMap<String, Session>();
613 
614         super.doStart();
615     }
616 
617 
618     /**
619      * Stop the session manager.
620      *
621      * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
622      */
623     @Override
624     public void doStop() throws Exception
625     {
626         super.doStop();
627         _sessions.clear();
628         _sessions = null;
629     }
630 
631     @Override
632     protected void shutdownSessions()
633     {
634         //Save the current state of all of our sessions,
635         //do NOT delete them (so other nodes can manage them)
636         long gracefulStopMs = getContextHandler().getServer().getStopTimeout();
637         long stopTime = 0;
638         if (gracefulStopMs > 0)
639             stopTime = System.nanoTime() + (TimeUnit.NANOSECONDS.convert(gracefulStopMs, TimeUnit.MILLISECONDS));        
640 
641         ArrayList<Session> sessions = (_sessions == null? new ArrayList<Session>() :new ArrayList<Session>(_sessions.values()) );
642 
643         // loop while there are sessions, and while there is stop time remaining, or if no stop time, just 1 loop
644         while (sessions.size() > 0 && ((stopTime > 0 && (System.nanoTime() < stopTime)) || (stopTime == 0)))
645         {
646             for (Session session : sessions)
647             {
648                 try
649                 {
650                     session.save(false);
651                 }
652                 catch (Exception e)
653                 {
654                     LOG.warn(e);
655                 }
656                 _sessions.remove(session.getClusterId());
657             }
658 
659             //check if we should terminate our loop if we're not using the stop timer
660             if (stopTime == 0)
661                 break;
662             
663             // Get any sessions that were added by other requests during processing and go around the loop again
664             sessions=new ArrayList<Session>(_sessions.values());
665         }
666     }
667 
668     
669     /**
670      * 
671      * @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
672      */
673     public void renewSessionId (String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
674     {
675         Session session = null;
676         try
677         {
678             session = (Session)_sessions.remove(oldClusterId);
679             if (session != null)
680             {
681                 synchronized (session)
682                 {
683                     session.setClusterId(newClusterId); //update ids
684                     session.setNodeId(newNodeId);
685                     _sessions.put(newClusterId, session); //put it into list in memory
686                     updateSession(session); //update database
687                 }
688             }
689         }
690         catch (Exception e)
691         {
692             LOG.warn(e);
693         }
694 
695         super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
696     }
697 
698     
699 
700     /**
701      * Invalidate a session.
702      *
703      * @param idInCluster the id in the cluster
704      */
705     protected void invalidateSession (String idInCluster)
706     {
707         Session session = (Session)_sessions.get(idInCluster);
708 
709         if (session != null)
710         {
711             session.invalidate();
712         }
713     }
714 
715     /**
716      * Delete an existing session, both from the in-memory map and
717      * the database.
718      *
719      * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
720      */
721     @Override
722     protected boolean removeSession(String idInCluster)
723     {
724         Session session = (Session)_sessions.remove(idInCluster);
725         try
726         {
727             if (session != null)
728                 deleteSession(session);
729         }
730         catch (Exception e)
731         {
732             LOG.warn("Problem deleting session id="+idInCluster, e);
733         }
734         return session!=null;
735     }
736 
737 
738     /**
739      * Add a newly created session to our in-memory list for this node and persist it.
740      *
741      * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
742      */
743     @Override
744     protected void addSession(AbstractSession session)
745     {
746         if (session==null)
747             return;
748 
749         _sessions.put(session.getClusterId(), (Session)session);
750 
751         try
752         {
753             synchronized (session)
754             {
755                 session.willPassivate();
756                 storeSession(((JDBCSessionManager.Session)session));
757                 session.didActivate();
758             }
759         }
760         catch (Exception e)
761         {
762             LOG.warn("Unable to store new session id="+session.getId() , e);
763         }
764     }
765 
766 
767     /**
768      * Make a new Session.
769      *
770      * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
771      */
772     @Override
773     protected AbstractSession newSession(HttpServletRequest request)
774     {
775         return new Session(request);
776     }
777     
778     protected AbstractSession newSession (String sessionId, String rowId, long created, long accessed, long maxInterval)
779     {
780         return new Session(sessionId, rowId, created, accessed, maxInterval);
781     }
782 
783     /* ------------------------------------------------------------ */
784     /** Remove session from manager
785      * @param session The session to remove
786      * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
787      * {@link SessionIdManager#invalidateAll(String)} should be called.
788      */
789     @Override
790     public boolean removeSession(AbstractSession session, boolean invalidate)
791     {
792         // Remove session from context and global maps
793         boolean removed = super.removeSession(session, invalidate);
794 
795         if (removed)
796         {
797             if (!invalidate)
798             {
799                 session.willPassivate();
800             }
801         }
802         
803         return removed;
804     }
805 
806 
807     /**
808      * Expire any Sessions we have in memory matching the list of
809      * expired Session ids.
810      *
811      * @param sessionIds the session ids to expire
812      * @return the set of successfully expired ids
813      */
814     protected Set<String> expire (Set<String> sessionIds)
815     {
816         //don't attempt to scavenge if we are shutting down
817         if (isStopping() || isStopped())
818             return null;
819 
820         
821         Thread thread=Thread.currentThread();
822         ClassLoader old_loader=thread.getContextClassLoader();
823         
824         Set<String> successfullyExpiredIds = new HashSet<String>();
825         try
826         {
827             Iterator<?> itor = sessionIds.iterator();
828             while (itor.hasNext())
829             {
830                 String sessionId = (String)itor.next();
831                 if (LOG.isDebugEnabled())
832                     LOG.debug("Expiring session id "+sessionId);
833 
834                 Session session = (Session)_sessions.get(sessionId);
835 
836                 //if session is not in our memory, then fetch from db so we can call the usual listeners on it
837                 if (session == null)
838                 {
839                     if (LOG.isDebugEnabled())LOG.debug("Force loading session id "+sessionId);
840                     session = loadSession(sessionId, canonicalize(_context.getContextPath()), getVirtualHost(_context));
841                     if (session != null)
842                     {
843                         //loaded an expired session last managed on this node for this context, add it to the list so we can 
844                         //treat it like a normal expired session
845                         _sessions.put(session.getClusterId(), session);
846                     }
847                     else
848                     {
849                         if (LOG.isDebugEnabled())
850                             LOG.debug("Unrecognized session id="+sessionId);
851                         continue;
852                     }
853                 }
854 
855                 if (session != null)
856                 {
857                     session.timeout();
858                     successfullyExpiredIds.add(session.getClusterId());
859                 }
860             }
861             return successfullyExpiredIds;
862         }
863         catch (Throwable t)
864         {
865             LOG.warn("Problem expiring sessions", t);
866             return successfullyExpiredIds;
867         }
868         finally
869         {
870             thread.setContextClassLoader(old_loader);
871         }
872     }
873     
874     protected void expireCandidates (Set<String> candidateIds)
875     {
876         Iterator<String> itor = candidateIds.iterator();
877         long now = System.currentTimeMillis();
878         while (itor.hasNext())
879         {
880             String id = itor.next();
881 
882             //check if expired in db
883             try
884             {
885                 Session memSession = _sessions.get(id);
886                 if (memSession == null)
887                 {
888                     continue; //no longer in memory
889                 }
890 
891                 Session s = loadSession(id,  canonicalize(_context.getContextPath()), getVirtualHost(_context));
892                 if (s == null)
893                 {
894                     //session no longer exists, can be safely expired
895                     memSession.timeout();
896                 }
897             }
898             catch (Exception e)
899             {
900                 LOG.warn("Error checking db for expiry for session {}", id);
901             }
902         }
903     }
904     
905     protected Set<String> getCandidateExpiredIds ()
906     {
907         HashSet<String> expiredIds = new HashSet<>();
908 
909         Iterator<String> itor = _sessions.keySet().iterator();
910         while (itor.hasNext())
911         {
912             String id = itor.next();
913             //check to see if session should have expired
914             Session session = _sessions.get(id);
915             if (session._expiryTime > 0 &&  System.currentTimeMillis() > session._expiryTime)
916                 expiredIds.add(id);           
917         }
918         return expiredIds;
919     }
920 
921 
922     /**
923      * Load a session from the database
924      * @param id the id
925      * @param canonicalContextPath the canonical context path
926      * @param vhost the virtual host
927      * @return the session data that was loaded
928      * @throws Exception if unable to load the session
929      */
930     protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
931     throws Exception
932     {
933         final AtomicReference<Session> _reference = new AtomicReference<Session>();
934         final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
935         Runnable load = new Runnable()
936         {
937             /** 
938              * @see java.lang.Runnable#run()
939              */
940             @SuppressWarnings("unchecked")
941             public void run()
942             {
943                 try (Connection connection = getConnection();
944                         PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, canonicalContextPath, vhost);
945                         ResultSet result = statement.executeQuery())
946                 {
947                     Session session = null;
948                     if (result.next())
949                     {                    
950                         long maxInterval = result.getLong(_sessionTableSchema.getMaxIntervalColumn());
951                         if (maxInterval == JDBCSessionIdManager.MAX_INTERVAL_NOT_SET)
952                         {
953                             maxInterval = getMaxInactiveInterval(); //if value not saved for maxInactiveInterval, use current value from sessionmanager
954                         }
955                         session = (Session)newSession(id, result.getString(_sessionTableSchema.getRowIdColumn()), 
956                                                   result.getLong(_sessionTableSchema.getCreateTimeColumn()), 
957                                                   result.getLong(_sessionTableSchema.getAccessTimeColumn()), 
958                                                   maxInterval);
959                         session.setCookieSetTime(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
960                         session.setLastAccessedTime(result.getLong(_sessionTableSchema.getLastAccessTimeColumn()));
961                         session.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
962                         session.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
963                         session.setExpiryTime(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
964                         session.setCanonicalContext(result.getString(_sessionTableSchema.getContextPathColumn()));
965                         session.setVirtualHost(result.getString(_sessionTableSchema.getVirtualHostColumn()));
966                                            
967                         try (InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
968                                 ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
969                         {
970                             Object o = ois.readObject();
971                             session.addAttributes((Map<String,Object>)o);
972                         }
973 
974                         if (LOG.isDebugEnabled())
975                             LOG.debug("LOADED session "+session);
976                     }
977                     else
978                         if (LOG.isDebugEnabled())
979                             LOG.debug("Failed to load session "+id);
980                     _reference.set(session);
981                 }
982                 catch (Exception e)
983                 {
984                     _exception.set(e);
985                 }
986             }
987         };
988 
989         if (_context==null)
990             load.run();
991         else
992             _context.getContextHandler().handle(null,load);
993 
994         if (_exception.get()!=null)
995         {
996             //if the session could not be restored, take its id out of the pool of currently-in-use
997             //session ids
998             _jdbcSessionIdMgr.removeSession(id);
999             throw _exception.get();
1000         }
1001 
1002         return _reference.get();
1003     }
1004 
1005     /**
1006      * Insert a session into the database.
1007      *
1008      * @param session the session
1009      * @throws Exception if unable to store the session
1010      */
1011     protected void storeSession (Session session)
1012     throws Exception
1013     {
1014         if (session==null)
1015             return;
1016 
1017         //put into the database
1018         try (Connection connection = getConnection();
1019                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession))
1020         {
1021             String rowId = calculateRowId(session);
1022 
1023             long now = System.currentTimeMillis();
1024             connection.setAutoCommit(true);
1025             statement.setString(1, rowId); //rowId
1026             statement.setString(2, session.getClusterId()); //session id
1027             statement.setString(3, session.getCanonicalContext()); //context path
1028             statement.setString(4, session.getVirtualHost()); //first vhost
1029             statement.setString(5, getSessionIdManager().getWorkerName());//my node id
1030             statement.setLong(6, session.getAccessed());//accessTime
1031             statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
1032             statement.setLong(8, session.getCreationTime()); //time created
1033             statement.setLong(9, session.getCookieSetTime());//time cookie was set
1034             statement.setLong(10, now); //last saved time
1035             statement.setLong(11, session.getExpiryTime());
1036             statement.setLong(12, session.getMaxInactiveInterval());
1037 
1038             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1039             ObjectOutputStream oos = new ObjectOutputStream(baos);
1040             oos.writeObject(session.getAttributeMap());
1041             oos.flush();
1042             byte[] bytes = baos.toByteArray();
1043 
1044             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1045             statement.setBinaryStream(13, bais, bytes.length);//attribute map as blob
1046            
1047 
1048             statement.executeUpdate();
1049             session.setRowId(rowId); //set it on the in-memory data as well as in db
1050             session.setLastSaved(now);
1051         }
1052         if (LOG.isDebugEnabled())
1053             LOG.debug("Stored session "+session);
1054     }
1055 
1056 
1057     /**
1058      * Update data on an existing persisted session.
1059      *
1060      * @param data the session
1061      * @throws Exception if unable to update the session
1062      */
1063     protected void updateSession (Session data)
1064     throws Exception
1065     {
1066         if (data==null)
1067             return;
1068 
1069         try (Connection connection = getConnection();
1070                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession))
1071         {
1072             long now = System.currentTimeMillis();
1073             connection.setAutoCommit(true);
1074             statement.setString(1, data.getClusterId());
1075             statement.setString(2, getSessionIdManager().getWorkerName());//my node id
1076             statement.setLong(3, data.getAccessed());//accessTime
1077             statement.setLong(4, data.getLastAccessedTime()); //lastAccessTime
1078             statement.setLong(5, now); //last saved time
1079             statement.setLong(6, data.getExpiryTime());
1080             statement.setLong(7, data.getMaxInactiveInterval());
1081 
1082             ByteArrayOutputStream baos = new ByteArrayOutputStream();
1083             ObjectOutputStream oos = new ObjectOutputStream(baos);
1084             oos.writeObject(data.getAttributeMap());
1085             oos.flush();
1086             byte[] bytes = baos.toByteArray();
1087             ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
1088 
1089             statement.setBinaryStream(8, bais, bytes.length);//attribute map as blob
1090             statement.setString(9, data.getRowId()); //rowId
1091             statement.executeUpdate();
1092 
1093             data.setLastSaved(now);
1094         }
1095         if (LOG.isDebugEnabled())
1096             LOG.debug("Updated session "+data);
1097     }
1098 
1099 
1100     /**
1101      * Update the node on which the session was last seen to be my node.
1102      *
1103      * @param data the session
1104      * @throws Exception if unable to update the session node
1105      */
1106     protected void updateSessionNode (Session data)
1107     throws Exception
1108     {
1109         String nodeId = getSessionIdManager().getWorkerName();
1110         try (Connection connection = getConnection();
1111                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode))
1112         {
1113             connection.setAutoCommit(true);
1114             statement.setString(1, nodeId);
1115             statement.setString(2, data.getRowId());
1116             statement.executeUpdate();
1117         }
1118         if (LOG.isDebugEnabled())
1119             LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
1120     }
1121 
1122     /**
1123      * Persist the time the session was last accessed.
1124      *
1125      * @param data the session
1126      * @throws Exception
1127      */
1128     private void updateSessionAccessTime (Session data)
1129     throws Exception
1130     {
1131         try (Connection connection = getConnection();
1132                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime))
1133         {
1134             long now = System.currentTimeMillis();
1135             connection.setAutoCommit(true);
1136             statement.setString(1, getSessionIdManager().getWorkerName());
1137             statement.setLong(2, data.getAccessed());
1138             statement.setLong(3, data.getLastAccessedTime());
1139             statement.setLong(4, now);
1140             statement.setLong(5, data.getExpiryTime());
1141             statement.setLong(6, data.getMaxInactiveInterval());
1142             statement.setString(7, data.getRowId());
1143           
1144             statement.executeUpdate();
1145             data.setLastSaved(now);
1146         }
1147         if (LOG.isDebugEnabled())
1148             LOG.debug("Updated access time session id="+data.getId()+" with lastsaved="+data.getLastSaved());
1149     }
1150 
1151 
1152 
1153 
1154     /**
1155      * Delete a session from the database. Should only be called
1156      * when the session has been invalidated.
1157      *
1158      * @param data the session data
1159      * @throws Exception if unable to delete the session
1160      */
1161     protected void deleteSession (Session data)
1162     throws Exception
1163     {
1164         try (Connection connection = getConnection();
1165                 PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession))
1166         {
1167             connection.setAutoCommit(true);
1168             statement.setString(1, data.getRowId());
1169             statement.executeUpdate();
1170             if (LOG.isDebugEnabled())
1171                 LOG.debug("Deleted Session "+data);
1172         }
1173     }
1174 
1175 
1176 
1177     /**
1178      * Get a connection from the driver.
1179      * @return
1180      * @throws SQLException
1181      */
1182     private Connection getConnection ()
1183     throws SQLException
1184     {
1185         return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
1186     }
1187 
1188     /**
1189      * Calculate a unique id for this session across the cluster.
1190      *
1191      * Unique id is composed of: contextpath_virtualhost0_sessionid
1192      * @param data
1193      * @return
1194      */
1195     private String calculateRowId (Session data)
1196     {
1197         String rowId = canonicalize(_context.getContextPath());
1198         rowId = rowId + "_" + getVirtualHost(_context);
1199         rowId = rowId+"_"+data.getId();
1200         return rowId;
1201     }
1202 
1203     /**
1204      * Get the first virtual host for the context.
1205      *
1206      * Used to help identify the exact session/contextPath.
1207      *
1208      * @return 0.0.0.0 if no virtual host is defined
1209      */
1210     private static String getVirtualHost (ContextHandler.Context context)
1211     {
1212         String vhost = "0.0.0.0";
1213 
1214         if (context==null)
1215             return vhost;
1216 
1217         String [] vhosts = context.getContextHandler().getVirtualHosts();
1218         if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
1219             return vhost;
1220 
1221         return vhosts[0];
1222     }
1223 
1224     /**
1225      * Make an acceptable file name from a context path.
1226      *
1227      * @param path
1228      * @return
1229      */
1230     private static String canonicalize (String path)
1231     {
1232         if (path==null)
1233             return "";
1234 
1235         return path.replace('/', '_').replace('.','_').replace('\\','_');
1236     }
1237 }