View Javadoc

1   // ========================================================================
2   // Copyright 2004-2008 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.terracotta.servlet;
16  
17  import java.util.Collections;
18  import java.util.Enumeration;
19  import java.util.HashMap;
20  import java.util.HashSet;
21  import java.util.Hashtable;
22  import java.util.Map;
23  import java.util.Set;
24  import java.util.concurrent.Executors;
25  import java.util.concurrent.ScheduledExecutorService;
26  import java.util.concurrent.ScheduledFuture;
27  import java.util.concurrent.TimeUnit;
28  
29  import javax.servlet.http.Cookie;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpSession;
32  
33  import com.tc.object.bytecode.Manageable;
34  import com.tc.object.bytecode.Manager;
35  import com.tc.object.bytecode.ManagerUtil;
36  import org.mortbay.jetty.Request;
37  import org.mortbay.jetty.handler.ContextHandler;
38  import org.mortbay.jetty.servlet.AbstractSessionManager;
39  import org.mortbay.log.Log;
40  
41  /**
42   * A specialized SessionManager to be used with <a href="http://www.terracotta.org">Terracotta</a>.
43   * <br />
44   * <h3>IMPLEMENTATION NOTES</h3>
45   * <h4>Requirements</h4>
46   * This implementation of the session management requires J2SE 5 or superior.
47   * <h4>Use of Hashtable</h4>
48   * In Terracotta, collections classes are
49   * <a href="http://www.terracotta.org/web/display/docs/Concept+and+Architecture+Guide">logically managed</a>
50   * and we need two levels of locking: a local locking to handle concurrent requests on the same node
51   * and a distributed locking to handle concurrent requests on different nodes.
52   * Natively synchronized classes such as Hashtable fit better than synchronized wrappers obtained via, for
53   * example, {@link Collections#synchronizedMap(Map)}. This is because Terracotta may replay the method call
54   * on the inner unsynchronized collection without invoking the external wrapper, so the synchronization will
55   * be lost. Natively synchronized collections does not have this problem.
56   * <h4>Use of Hashtable as a Set</h4>
57   * There is no natively synchronized Set implementation, so we use Hashtable instead, see
58   * {@link TerracottaSessionIdManager}.
59   * However, we don't map the session id to itself, because Strings are treated specially by Terracotta,
60   * causing more traffic to the Terracotta server. Instead we use the same pattern used in the implementation
61   * of <code>java.util.HashSet</code>: use a single shared object to indicate the presence of a key.
62   * This is necessary since Hashtable does not allow null values.
63   * <h4>Sessions expiration map</h4>
64   * In order to scavenge expired sessions, we need a way to know if they are expired. This information
65   * is normally held in the session itself via the <code>lastAccessedTime</code> property.
66   * However, we would need to iterate over all sessions to check if each one is expired, and this migrates
67   * all sessions to the node, causing a lot of unneeded traffic between nodes and the Terracotta server.
68   * To avoid this, we keep a separate map from session id to expiration time, so we only need to migrate
69   * all the expirations times to see if a session is expired or not.
70   * <h4>Update of lastAccessedTime</h4>
71   * As a performance improvement, the lastAccessedTime is updated only periodically, and not every time
72   * a request enters a node. This optimization allows applications that have frequent requests but less
73   * frequent accesses to the session to perform better, because the traffic between the node and the
74   * Terracotta server is reduced. The update period is the scavenger period, see {@link Session#access(long)}.
75   * <h4>Terracotta lock id</h4>
76   * The Terracotta lock id is based on the session id, but this alone is not sufficient, as there may be
77   * two sessions with the same id for two different contexts. So we need session id and context path.
78   * However, this also is not enough, as we may have the rare case of the same webapp mapped to two different
79   * virtual hosts, and each virtual host must have a different session object.
80   * Therefore the lock id we need to use is a combination of session id, context path and virtual host, see
81   * {@link #newLockId(String)}.
82   *
83   * @see TerracottaSessionIdManager
84   */
85  public class TerracottaSessionManager extends AbstractSessionManager implements Runnable
86  {
87      /**
88       * The local cache of session objects.
89       */
90      private Map<String, Session> _sessions;
91      /**
92       * The distributed shared SessionData map.
93       * Putting objects into the map result in the objects being sent to Terracotta, and any change
94       * to the objects are also replicated, recursively.
95       * Getting objects from the map result in the objects being fetched from Terracotta.
96       */
97      private Hashtable<String, SessionData> _sessionDatas;
98      /**
99       * The distributed shared session expirations map, needed for scavenging.
100      * In particular it supports removal of sessions that have been orphaned by nodeA
101      * (for example because it crashed) by virtue of scavenging performed by nodeB.
102      */
103     private Hashtable<String, MutableLong> _sessionExpirations;
104     private String _contextPath;
105     private String _virtualHost;
106     private long _scavengePeriodMs = 30000;
107     private ScheduledExecutorService _scheduler;
108     private ScheduledFuture<?> _scavenger;
109 
110     public void doStart() throws Exception
111     {
112         super.doStart();
113 
114         _contextPath = canonicalize(_context.getContextPath());
115         _virtualHost = virtualHostFrom(_context);
116 
117         _sessions = Collections.synchronizedMap(new HashMap<String, Session>());
118         _sessionDatas = newSharedMap("sessionData:" + _contextPath + ":" + _virtualHost);
119         _sessionExpirations = newSharedMap("sessionExpirations:" + _contextPath + ":" + _virtualHost);
120         _scheduler = Executors.newSingleThreadScheduledExecutor();
121         scheduleScavenging();
122     }
123 
124     private Hashtable newSharedMap(String name)
125     {
126         // We want to partition the session data among contexts, so we need to have different roots for
127         // different contexts, and each root must have a different name, since roots with the same name are shared.
128         Lock.lock(name);
129         try
130         {
131             // We need a synchronized data structure to have node-local synchronization.
132             // We use Hashtable because it is a natively synchronized collection that behaves
133             // better in Terracotta than synchronized wrappers obtained with Collections.synchronized*().
134             Hashtable result = (Hashtable)ManagerUtil.lookupOrCreateRootNoDepth(name, new Hashtable());
135             ((Manageable)result).__tc_managed().disableAutoLocking();
136             return result;
137         }
138         finally
139         {
140             Lock.unlock(name);
141         }
142     }
143 
144     private void scheduleScavenging()
145     {
146         if (_scavenger != null)
147         {
148             _scavenger.cancel(false);
149             _scavenger = null;
150         }
151         long scavengePeriod = getScavengePeriodMs();
152         if (scavengePeriod > 0 && _scheduler != null)
153             _scavenger = _scheduler.scheduleWithFixedDelay(this, scavengePeriod, scavengePeriod, TimeUnit.MILLISECONDS);
154     }
155 
156     public void doStop() throws Exception
157     {
158         if (_scavenger != null) _scavenger.cancel(true);
159         if (_scheduler != null) _scheduler.shutdownNow();
160         super.doStop();
161     }
162 
163     public void run()
164     {
165         scavenge();
166     }
167 
168     public void enter(Request request)
169     {
170         /**
171          * SESSION LOCKING
172          * This is an entry point for session locking.
173          * We arrive here at the beginning of every request
174          */
175 
176         String requestedSessionId = request.getRequestedSessionId();
177         HttpSession session = request.getSession(false);
178         Log.debug("Entering, requested session id {}, session id {}", requestedSessionId, session == null ? null : getClusterId(session));
179         if (requestedSessionId == null)
180         {
181             // The request does not have a session id, do not lock.
182             // If the session, later in the request, is created by the user,
183             // it will be locked when it will be created
184         }
185         else
186         {
187             // We lock anyway with the requested session id.
188             // The requested session id may not be a valid one,
189             // for example because the session expired.
190             // If the user creates a new session, it will have
191             // a different session id and that also will be locked.
192             enter(getIdManager().getClusterId(requestedSessionId));
193         }
194     }
195 
196     protected void enter(String clusterId)
197     {
198         Lock.lock(newLockId(clusterId));
199         Log.debug("Entered, session id {}", clusterId);
200     }
201 
202     protected boolean tryEnter(String clusterId)
203     {
204         return Lock.tryLock(newLockId(clusterId));
205     }
206 
207     public void exit(Request request)
208     {
209         /**
210          * SESSION LOCKING
211          * This is an exit point for session locking.
212          * We arrive here at the end of every request
213          */
214 
215         String requestedSessionId = request.getRequestedSessionId();
216         HttpSession session = request.getSession(false);
217         Log.debug("Exiting, requested session id {}, session id {}", requestedSessionId, session == null ? null : getClusterId(session));
218         if (requestedSessionId == null)
219         {
220             if (session == null)
221             {
222                 // No session has been created in the request, just return
223             }
224             else
225             {
226                 // A new session has been created by the user, unlock it
227                 exit(getClusterId(session));
228             }
229         }
230         else
231         {
232             // There was a requested session id, and we locked it, so here release it
233             String requestedClusterId = getIdManager().getClusterId(requestedSessionId);
234             exit(requestedClusterId);
235 
236             if (session != null)
237             {
238                 if (!requestedClusterId.equals(getClusterId(session)))
239                 {
240                     // The requested session id was invalid, and a
241                     // new session has been created by the user with
242                     // a different session id, unlock it
243                     exit(getClusterId(session));
244                 }
245             }
246         }
247     }
248 
249     protected void exit(String clusterId)
250     {
251         Lock.unlock(newLockId(clusterId));
252         Log.debug("Exited, session id {}", clusterId);
253     }
254 
255     protected void addSession(AbstractSessionManager.Session session)
256     {
257         /**
258          * SESSION LOCKING
259          * When this method is called, we already hold the session lock.
260          * See {@link #newSession(HttpServletRequest)}
261          */
262         String clusterId = getClusterId(session);
263         Session tcSession = (Session)session;
264         SessionData sessionData = tcSession.getSessionData();
265         _sessionExpirations.put(clusterId, sessionData._expiration);
266         _sessionDatas.put(clusterId, sessionData);
267         _sessions.put(clusterId, tcSession);
268         Log.debug("Added session {} with id {}", tcSession, clusterId);
269     }
270 
271     @Override
272     public Cookie access(HttpSession session, boolean secure)
273     {
274         Cookie cookie = super.access(session, secure);
275         Log.debug("Accessed session {} with id {}", session, session.getId());
276         return cookie;
277     }
278 
279     @Override
280     public void complete(HttpSession session)
281     {
282         super.complete(session);
283         Log.debug("Completed session {} with id {}", session, session.getId());
284     }
285 
286     protected void removeSession(String clusterId)
287     {
288         /**
289          * SESSION LOCKING
290          * When this method is called, we already hold the session lock.
291          * Either the scavenger acquired it, or the user invalidated
292          * the existing session and thus {@link #enter(String)} was called.
293          */
294 
295         // Remove locally cached session
296         Session session = _sessions.remove(clusterId);
297         Log.debug("Removed session {} with id {}", session, clusterId);
298 
299         // It may happen that one node removes its expired session data,
300         // so that when this node does the same, the session data is already gone
301         SessionData sessionData = _sessionDatas.remove(clusterId);
302         Log.debug("Removed session data {} with id {}", sessionData, clusterId);
303 
304         // Remove the expiration entry used in scavenging
305         _sessionExpirations.remove(clusterId);
306     }
307 
308     public void setScavengePeriodMs(long ms)
309     {
310         ms = ms == 0 ? 60000: ms;
311         ms = ms > 60000 ? 60000: ms;
312         ms = ms < 1000 ? 1000: ms;
313         this._scavengePeriodMs = ms;
314         scheduleScavenging();
315     }
316 
317     public long getScavengePeriodMs()
318     {
319         return _scavengePeriodMs;
320     }
321 
322     public AbstractSessionManager.Session getSession(String clusterId)
323     {
324         Session result = null;
325 
326         /**
327          * SESSION LOCKING
328          * This is an entry point for session locking.
329          * We lookup the session given the id, and if it exist we hold the lock.
330          * We unlock on end of method, since this method can be called outside
331          * an {@link #enter(String)}/{@link #exit(String)} pair.
332          */
333         enter(clusterId);
334         try
335         {
336             // Need to synchronize because we use a get-then-put that must be atomic
337             // on the local session cache
338             // Refer to method {@link #scavenge()} for an explanation of synchronization order:
339             // first on _sessions, then on _sessionExpirations.
340             synchronized (_sessions)
341             {
342                 result = _sessions.get(clusterId);
343                 if (result == null)
344                 {
345                     Log.debug("Session with id {} --> local cache miss", clusterId);
346 
347                     // Lookup the distributed shared sessionData object.
348                     // This will migrate the session data to this node from the Terracotta server
349                     // We have not grabbed the distributed lock associated with this session yet,
350                     // so another node can migrate the session data as well. This is no problem,
351                     // since just after this method returns the distributed lock will be grabbed by
352                     // one node, the session data will be changed and the lock released.
353                     // The second node contending for the distributed lock will then acquire it,
354                     // and the session data information will be migrated lazily by Terracotta means.
355                     // We are only interested in having a SessionData reference locally.
356                     Log.debug("Distributed session data with id {} --> lookup", clusterId);
357                     SessionData sessionData = _sessionDatas.get(clusterId);
358                     if (sessionData == null)
359                     {
360                         Log.debug("Distributed session data with id {} --> not found", clusterId);
361                     }
362                     else
363                     {
364                         Log.debug("Distributed session data with id {} --> found", clusterId);
365                         // Wrap the migrated session data and cache the Session object
366                         result = new Session(sessionData);
367                         _sessions.put(clusterId, result);
368                     }
369                 }
370                 else
371                 {
372                     Log.debug("Session with id {} --> local cache hit", clusterId);
373                     if (!_sessionExpirations.containsKey(clusterId))
374                     {
375                         // A session is present in the local cache, but it has been expired
376                         // or invalidated on another node, perform local clean up.
377                         _sessions.remove(clusterId);
378                         result = null;
379                         Log.debug("Session with id {} --> local cache stale");
380                     }
381                 }
382             }
383         }
384         finally
385         {
386             /**
387              * SESSION LOCKING
388              */
389             exit(clusterId);
390         }
391         return result;
392     }
393 
394     protected String newLockId(String clusterId)
395     {
396         StringBuilder builder = new StringBuilder(clusterId);
397         builder.append(":").append(_contextPath);
398         builder.append(":").append(_virtualHost);
399         return builder.toString();
400     }
401 
402     // TODO: This method is not needed, only used for testing
403     public Map getSessionMap()
404     {
405         return Collections.unmodifiableMap(_sessions);
406     }
407 
408     // TODO: rename to getSessionsCount()
409     // TODO: also, not used if not by superclass for unused statistics data
410     public int getSessions()
411     {
412         return _sessions.size();
413     }
414 
415     protected Session newSession(HttpServletRequest request)
416     {
417         /**
418          * SESSION LOCKING
419          * This is an entry point for session locking.
420          * We arrive here when we have to create a new
421          * session, for a request.getSession(true) call.
422          */
423         Session result = new Session(request);
424 
425         String requestedSessionId = request.getRequestedSessionId();
426         if (requestedSessionId == null)
427         {
428             // Here the user requested a fresh new session, lock it.
429             enter(result.getClusterId());
430         }
431         else
432         {
433             if (result.getClusterId().equals(getIdManager().getClusterId(requestedSessionId)))
434             {
435                 // Here we have a cross context dispatch where the same session id
436                 // is used for two different sessions; we do not lock because the lock
437                 // has already been acquired in enter(Request), based on the requested
438                 // session id.
439             }
440             else
441             {
442                 // Here the requested session id is invalid (the session expired),
443                 // and a new session is created, lock it.
444                 enter(result.getClusterId());
445             }
446         }
447         return result;
448     }
449 
450     protected void invalidateSessions()
451     {
452         // Do nothing.
453         // We don't want to remove and invalidate all the sessions,
454         // because this method is called from doStop(), and just
455         // because this context is stopping does not mean that we
456         // should remove the session from any other node (remember
457         // the session map is shared)
458     }
459 
460     private void scavenge()
461     {
462         Thread thread = Thread.currentThread();
463         ClassLoader old_loader = thread.getContextClassLoader();
464         if (_loader != null) thread.setContextClassLoader(_loader);
465         try
466         {
467             long now = System.currentTimeMillis();
468             Log.debug(this + " scavenging at {}, scavenge period {}", now, getScavengePeriodMs());
469 
470             // Detect the candidates that may have expired already, checking the estimated expiration time.
471             Set<String> candidates = new HashSet<String>();
472             String lockId = "scavenge:" + _contextPath + ":" + _virtualHost;
473             Lock.lock(lockId);
474             try
475             {
476                 /**
477                  * Synchronize in order, to avoid deadlocks with method {@link #getSession(String)}.
478                  * In that method, we first synchronize on _session, then we call _sessionExpirations.containsKey(),
479                  * which is synchronized by virtue of being a Collection.synchronizedMap.
480                  * Here we must synchronize in the same order to avoid deadlock.
481                  */
482                 synchronized (_sessions)
483                 {
484                     synchronized (_sessionExpirations)
485                     {
486                         // Do not use iterators that throw ConcurrentModificationException
487                         // We do a best effort here, and leave possible imprecisions to the next scavenge
488                         Enumeration<String> keys = _sessionExpirations.keys();
489                         while (keys.hasMoreElements())
490                         {
491                             String sessionId = keys.nextElement();
492                             MutableLong value = _sessionExpirations.get(sessionId);
493                             if (value != null)
494                             {
495                                 long expirationTime = value.value;
496                                 Log.debug("Estimated expiration time {} for session {}", expirationTime, sessionId);
497                                 if (expirationTime > 0 && expirationTime < now) candidates.add(sessionId);
498                             }
499                         }
500 
501                         _sessions.keySet().retainAll(Collections.list(_sessionExpirations.keys()));
502                     }
503                 }
504             }
505             finally
506             {
507                 Lock.unlock(lockId);
508             }
509             Log.debug("Scavenging detected {} candidate sessions to expire", candidates.size());
510 
511             // Now validate that the candidates that do expire are really expired,
512             // grabbing the session lock for each candidate
513             for (String sessionId : candidates)
514             {
515                 Session candidate = (Session)getSession(sessionId);
516                 if (candidate == null)
517                     continue;
518                 
519                 // Here we grab the lock to avoid anyone else interfering
520                 boolean entered = tryEnter(sessionId);
521                 if (entered)
522                 {
523                     try
524                     {
525                         long maxInactiveTime = candidate.getMaxIdlePeriodMs();
526                         // Exclude sessions that never expire
527                         if (maxInactiveTime > 0)
528                         {
529                             // The lastAccessedTime is fetched from Terracotta, so we're sure it is up-to-date.
530                             long lastAccessedTime = candidate.getLastAccessedTime();
531                             // Since we write the shared lastAccessedTime every scavenge period,
532                             // take that in account before considering the session expired
533                             long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
534                             if (expirationTime < now)
535                             {
536                                 Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
537                                 // Calling timeout() result in calling removeSession(), that will clean the data structures
538                                 candidate.timeout();
539                             }
540                             else
541                             {
542                                 Log.debug("Scavenging skipping candidate session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
543                             }
544                         }
545                     }
546                     finally
547                     {
548                         exit(sessionId);
549                     }
550                 }
551             }
552 
553             int sessionCount = getSessions();
554             if (sessionCount < _minSessions) _minSessions = sessionCount;
555             if (sessionCount > _maxSessions) _maxSessions = sessionCount;
556         }
557         catch (Throwable x)
558         {
559             // Must avoid at all costs that the scavenge thread exits, so here we catch and log 
560             if(x instanceof ThreadDeath)
561                 throw (ThreadDeath)x;
562             Log.warn("Problem scavenging sessions", x);
563         }
564         finally
565         {
566             thread.setContextClassLoader(old_loader);
567         }
568     }
569 
570     private String canonicalize(String contextPath)
571     {
572         if (contextPath == null) return "";
573         return contextPath.replace('/', '_').replace('.', '_').replace('\\', '_');
574     }
575 
576     private String virtualHostFrom(ContextHandler.SContext context)
577     {
578         String result = "0.0.0.0";
579         if (context == null) return result;
580 
581         String[] vhosts = context.getContextHandler().getVirtualHosts();
582         if (vhosts == null || vhosts.length == 0 || vhosts[0] == null) return result;
583 
584         return vhosts[0];
585     }
586 
587     class Session extends AbstractSessionManager.Session
588     {
589         private static final long serialVersionUID = -2134521374206116367L;
590 
591         private final SessionData _sessionData;
592         private long _lastUpdate;
593 
594         protected Session(HttpServletRequest request)
595         {
596             super(request);
597             _sessionData = new SessionData(getClusterId(), _maxIdleMs);
598             _lastAccessed = _sessionData.getCreationTime();
599         }
600 
601         protected Session(SessionData sd)
602         {
603             super(sd.getCreationTime(), sd.getId());
604             _sessionData = sd;
605             _lastAccessed = getLastAccessedTime();
606             initValues();
607         }
608 
609         public SessionData getSessionData()
610         {
611             return _sessionData;
612         }
613 
614         @Override
615         public long getCookieSetTime()
616         {
617             return _sessionData.getCookieTime();
618         }
619 
620         @Override
621         protected void cookieSet()
622         {
623             _sessionData.setCookieTime(getLastAccessedTime());
624         }
625         
626         @Override
627         public void setMaxInactiveInterval(int secs)
628         {
629             super.setMaxInactiveInterval(secs);
630             if(_maxIdleMs > 0L && _maxIdleMs / 10L < (long)_scavengePeriodMs)
631             {
632                 long newScavengeSecs = (secs + 9) / 10;
633                 setScavengePeriodMs(1000L * newScavengeSecs);
634             }
635 
636             // Update the estimated expiration time
637             if (secs < 0) {
638                 this._sessionData._expiration.value = -1L;
639             } else {
640                 this._sessionData._expiration.value = System.currentTimeMillis() + (1000L * secs);
641             }
642         }
643 
644         @Override
645         public long getLastAccessedTime()
646         {
647             if (!isValid()) throw new IllegalStateException();
648             return _sessionData.getPreviousAccessTime();
649         }
650 
651         @Override
652         public long getCreationTime() throws IllegalStateException
653         {
654             if (!isValid()) throw new IllegalStateException();
655             return _sessionData.getCreationTime();
656         }
657 
658         // Overridden for visibility
659         @Override
660         protected String getClusterId()
661         {
662             return super.getClusterId();
663         }
664 
665         protected Map newAttributeMap()
666         {
667             // It is important to never return a new attribute map here (as other Session implementations do),
668             // but always return the shared attributes map, so that a new session created on a different cluster
669             // node is immediately filled with the session data from Terracotta.
670             return _sessionData.getAttributeMap();
671         }
672 
673         @Override
674         protected void access(long time)
675         {
676             // The local previous access time is always updated via the super.access() call.
677             // If the requests are steady and within the scavenge period, the distributed shared access times
678             // are never updated. If only one node gets hits, other nodes reach the expiration time and the
679             // scavenging on other nodes will believe the session is expired, since the distributed shared
680             // access times have never been updated.
681             // Therefore we need to update the distributed shared access times once in a while, no matter what.
682             long previousAccessTime = getPreviousAccessTime();
683             if (time - previousAccessTime > getScavengePeriodMs())
684             {
685                 Log.debug("Out-of-date update of distributed access times: previous {} - current {}", previousAccessTime, time);
686                 updateAccessTimes(time);
687             }
688             else
689             {
690                 if (time - _lastUpdate > getScavengePeriodMs())
691                 {
692                     Log.debug("Periodic update of distributed access times: last update {} - current {}", _lastUpdate, time);
693                     updateAccessTimes(time);
694                 }
695                 else
696                 {
697                     Log.debug("Skipping update of distributed access times: previous {} - current {}", previousAccessTime, time);
698                 }
699             }
700             super.access(time);
701         }
702 
703         /**
704          * Updates the shared distributed access times that need to be updated
705          *
706          * @param time the update value
707          */
708         private void updateAccessTimes(long time)
709         {
710             _sessionData.setPreviousAccessTime(_accessed);
711             if (getMaxIdlePeriodMs() > 0) _sessionData.setExpirationTime(time + getMaxIdlePeriodMs());
712             _lastUpdate = time;
713         }
714 
715         // Overridden for visibility
716         @Override
717         protected void timeout()
718         {
719             super.timeout();
720             Log.debug("Timed out session {} with id {}", this, getClusterId());
721         }
722 
723         @Override
724         public void invalidate()
725         {
726             super.invalidate();
727             Log.debug("Invalidated session {} with id {}", this, getClusterId());
728         }
729 
730         private long getMaxIdlePeriodMs()
731         {
732             return _maxIdleMs;
733         }
734 
735         private long getPreviousAccessTime()
736         {
737             return super.getLastAccessedTime();
738         }
739     }
740 
741     /**
742      * The session data that is distributed to cluster nodes via Terracotta.
743      */
744     public static class SessionData
745     {
746         private final String _id;
747         private final Map _attributes;
748         private final long _creation;
749         private final MutableLong _expiration;
750         private long _previousAccess;
751         private long _cookieTime;
752 
753         public SessionData(String sessionId, long maxIdleMs)
754         {
755             _id = sessionId;
756             // Don't need synchronization, as we grab a distributed session id lock
757             // when this map is accessed.
758             _attributes = new HashMap();
759             _creation = System.currentTimeMillis();
760             _expiration = new MutableLong();
761             _previousAccess = _creation;
762             // Set expiration time to negative value if the session never expires
763             _expiration.value = maxIdleMs > 0 ? _creation + maxIdleMs : -1L;
764         }
765 
766         public String getId()
767         {
768             return _id;
769         }
770 
771         protected Map getAttributeMap()
772         {
773             return _attributes;
774         }
775 
776         public long getCreationTime()
777         {
778             return _creation;
779         }
780 
781         public long getExpirationTime()
782         {
783             return _expiration.value;
784         }
785 
786         public void setExpirationTime(long time)
787         {
788             _expiration.value = time;
789         }
790 
791         public long getCookieTime()
792         {
793             return _cookieTime;
794         }
795 
796         public void setCookieTime(long time)
797         {
798             _cookieTime = time;
799         }
800 
801         public long getPreviousAccessTime()
802         {
803             return _previousAccess;
804         }
805 
806         public void setPreviousAccessTime(long time)
807         {
808             _previousAccess = time;
809         }
810     }
811 
812     protected static class Lock
813     {
814         private static final ThreadLocal<Map<String, Integer>> nestings = new ThreadLocal<Map<String, Integer>>()
815         {
816             @Override
817             protected Map<String, Integer> initialValue()
818             {
819                 return new HashMap<String, Integer>();
820             }
821         };
822 
823         private Lock()
824         {
825         }
826 
827         public static void lock(String lockId)
828         {
829             Integer nestingLevel = nestings.get().get(lockId);
830             if (nestingLevel == null) nestingLevel = 0;
831             if (nestingLevel < 0)
832                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
833             if (nestingLevel == 0)
834             {
835                 ManagerUtil.beginLock(lockId, Manager.LOCK_TYPE_WRITE);
836                 Log.debug("Lock({}) acquired by thread {}", lockId, Thread.currentThread().getName());
837             }
838             nestings.get().put(lockId, nestingLevel + 1);
839             Log.debug("Lock({}) nestings {}", lockId, getLocks());
840         }
841 
842         public static boolean tryLock(String lockId)
843         {
844             boolean result = ManagerUtil.tryBeginLock(lockId, Manager.LOCK_TYPE_WRITE);
845             Log.debug("Lock({}) tried and" + (result ? "" : " not") + " acquired by thread {}", lockId, Thread.currentThread().getName());
846             if (result)
847             {
848                 Integer nestingLevel = nestings.get().get(lockId);
849                 if (nestingLevel == null) nestingLevel = 0;
850                 nestings.get().put(lockId, nestingLevel + 1);
851                 Log.debug("Lock({}) nestings {}", lockId, getLocks());
852             }
853             return result;
854         }
855 
856         public static void unlock(String lockId)
857         {
858             Integer nestingLevel = nestings.get().get(lockId);
859             if (nestingLevel == null) return;
860             if (nestingLevel < 1)
861                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
862             if (nestingLevel == 1)
863             {
864                 ManagerUtil.commitLock(lockId);
865                 Log.debug("Lock({}) released by thread {}", lockId, Thread.currentThread().getName());
866                 nestings.get().remove(lockId);
867             }
868             else
869             {
870                 nestings.get().put(lockId, nestingLevel - 1);
871             }
872             Log.debug("Lock({}) nestings {}", lockId, getLocks());
873         }
874 
875         /**
876          * For testing and debugging purposes only.
877          * @return the lock ids held by the current thread
878          */
879         protected static Map<String, Integer> getLocks()
880         {
881             return Collections.unmodifiableMap(nestings.get());
882         }
883     }
884 
885     private static class MutableLong
886     {
887         private long value;
888     }
889 }