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