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             synchronized (_sessions)
335             {
336                 result = _sessions.get(clusterId);
337                 if (result == null)
338                 {
339                     Log.debug("Session with id {} --> local cache miss", clusterId);
340 
341                     // Lookup the distributed shared sessionData object.
342                     // This will migrate the session data to this node from the Terracotta server
343                     // We have not grabbed the distributed lock associated with this session yet,
344                     // so another node can migrate the session data as well. This is no problem,
345                     // since just after this method returns the distributed lock will be grabbed by
346                     // one node, the session data will be changed and the lock released.
347                     // The second node contending for the distributed lock will then acquire it,
348                     // and the session data information will be migrated lazily by Terracotta means.
349                     // We are only interested in having a SessionData reference locally.
350                     Log.debug("Distributed session data with id {} --> lookup", clusterId);
351                     SessionData sessionData = _sessionDatas.get(clusterId);
352                     if (sessionData == null)
353                     {
354                         Log.debug("Distributed session data with id {} --> not found", clusterId);
355                     }
356                     else
357                     {
358                         Log.debug("Distributed session data with id {} --> found", clusterId);
359                         // Wrap the migrated session data and cache the Session object
360                         result = new Session(sessionData);
361                         _sessions.put(clusterId, result);
362                     }
363                 }
364                 else
365                 {
366                     Log.debug("Session with id {} --> local cache hit", clusterId);
367                     if (!_sessionExpirations.containsKey(clusterId))
368                     {
369                         // A session is present in the local cache, but it has been expired
370                         // or invalidated on another node, perform local clean up.
371                         _sessions.remove(clusterId);
372                         result = null;
373                         Log.debug("Session with id {} --> local cache stale");
374                     }
375                 }
376             }
377         }
378         finally
379         {
380             /**
381              * SESSION LOCKING
382              */
383             exit(clusterId);
384         }
385         return result;
386     }
387 
388     protected String newLockId(String clusterId)
389     {
390         StringBuilder builder = new StringBuilder(clusterId);
391         builder.append(":").append(_contextPath);
392         builder.append(":").append(_virtualHost);
393         return builder.toString();
394     }
395 
396     // TODO: This method is not needed, only used for testing
397     public Map getSessionMap()
398     {
399         return Collections.unmodifiableMap(_sessions);
400     }
401 
402     // TODO: rename to getSessionsCount()
403     // TODO: also, not used if not by superclass for unused statistics data
404     public int getSessions()
405     {
406         return _sessions.size();
407     }
408 
409     protected Session newSession(HttpServletRequest request)
410     {
411         /**
412          * SESSION LOCKING
413          * This is an entry point for session locking.
414          * We arrive here when we have to create a new
415          * session, for a request.getSession(true) call.
416          */
417         Session result = new Session(request);
418 
419         String requestedSessionId = request.getRequestedSessionId();
420         if (requestedSessionId == null)
421         {
422             // Here the user requested a fresh new session, lock it.
423             enter(result.getClusterId());
424         }
425         else
426         {
427             if (result.getClusterId().equals(getIdManager().getClusterId(requestedSessionId)))
428             {
429                 // Here we have a cross context dispatch where the same session id
430                 // is used for two different sessions; we do not lock because the lock
431                 // has already been acquired in enter(Request), based on the requested
432                 // session id.
433             }
434             else
435             {
436                 // Here the requested session id is invalid (the session expired),
437                 // and a new session is created, lock it.
438                 enter(result.getClusterId());
439             }
440         }
441         return result;
442     }
443 
444     protected void invalidateSessions()
445     {
446         // Do nothing.
447         // We don't want to remove and invalidate all the sessions,
448         // because this method is called from doStop(), and just
449         // because this context is stopping does not mean that we
450         // should remove the session from any other node (remember
451         // the session map is shared)
452     }
453 
454     private void scavenge()
455     {
456         Thread thread = Thread.currentThread();
457         ClassLoader old_loader = thread.getContextClassLoader();
458         if (_loader != null) thread.setContextClassLoader(_loader);
459         try
460         {
461             long now = System.currentTimeMillis();
462             Log.debug(this + " scavenging at {}, scavenge period {}", now, getScavengePeriodMs());
463 
464             // Detect the candidates that may have expired already, checking the estimated expiration time.
465             Set<String> candidates = new HashSet<String>();
466             String lockId = "scavenge:" + _contextPath + ":" + _virtualHost;
467             Lock.lock(lockId);
468             try
469             {
470                 synchronized (_sessionExpirations)
471                 {
472                     for (Map.Entry<String, MutableLong> entry : _sessionExpirations.entrySet())
473                     {
474                         String sessionId = entry.getKey();
475                         long expirationTime = entry.getValue().value;
476                         Log.debug("Estimated expiration time {} for session {}", expirationTime, sessionId);
477                         if (expirationTime > 0 && expirationTime < now) candidates.add(sessionId);
478                     }
479 
480                     synchronized (_sessions)
481                     {
482                         _sessions.keySet().retainAll(_sessionExpirations.keySet());
483                     }
484                 }
485             }
486             finally
487             {
488                 Lock.unlock(lockId);
489             }
490             Log.debug("Scavenging detected {} candidate sessions to expire", candidates.size());
491 
492             // Now validate that the candidates that do expire are really expired,
493             // grabbing the session lock for each candidate
494             for (String sessionId : candidates)
495             {
496                 Session candidate = (Session)getSession(sessionId);
497                 // Here we grab the lock to avoid anyone else interfering
498                 boolean entered = tryEnter(sessionId);
499                 if (entered)
500                 {
501                     try
502                     {
503                         long maxInactiveTime = candidate.getMaxIdlePeriodMs();
504                         // Exclude sessions that never expire
505                         if (maxInactiveTime > 0)
506                         {
507                             // The lastAccessedTime is fetched from Terracotta, so we're sure it is up-to-date.
508                             long lastAccessedTime = candidate.getLastAccessedTime();
509                             // Since we write the shared lastAccessedTime every scavenge period,
510                             // take that in account before considering the session expired
511                             long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
512                             if (expirationTime < now)
513                             {
514                                 Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
515                                 // Calling timeout() result in calling removeSession(), that will clean the data structures
516                                 candidate.timeout();
517                             }
518                             else
519                             {
520                                 Log.debug("Scavenging skipping candidate session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
521                             }
522                         }
523                     }
524                     finally
525                     {
526                         exit(sessionId);
527                     }
528                 }
529             }
530 
531             int sessionCount = getSessions();
532             if (sessionCount < _minSessions) _minSessions = sessionCount;
533             if (sessionCount > _maxSessions) _maxSessions = sessionCount;
534         }
535         finally
536         {
537             thread.setContextClassLoader(old_loader);
538         }
539     }
540 
541     private String canonicalize(String contextPath)
542     {
543         if (contextPath == null) return "";
544         return contextPath.replace('/', '_').replace('.', '_').replace('\\', '_');
545     }
546 
547     private String virtualHostFrom(ContextHandler.SContext context)
548     {
549         String result = "0.0.0.0";
550         if (context == null) return result;
551 
552         String[] vhosts = context.getContextHandler().getVirtualHosts();
553         if (vhosts == null || vhosts.length == 0 || vhosts[0] == null) return result;
554 
555         return vhosts[0];
556     }
557 
558     class Session extends AbstractSessionManager.Session
559     {
560         private static final long serialVersionUID = -2134521374206116367L;
561 
562         private final SessionData _sessionData;
563         private long _lastUpdate;
564 
565         protected Session(HttpServletRequest request)
566         {
567             super(request);
568             _sessionData = new SessionData(getClusterId(), _maxIdleMs);
569             _lastAccessed = _sessionData.getCreationTime();
570         }
571 
572         protected Session(SessionData sd)
573         {
574             super(sd.getCreationTime(), sd.getId());
575             _sessionData = sd;
576             _lastAccessed = getLastAccessedTime();
577             initValues();
578         }
579 
580         public SessionData getSessionData()
581         {
582             return _sessionData;
583         }
584 
585         @Override
586         public long getCookieSetTime()
587         {
588             return _sessionData.getCookieTime();
589         }
590 
591         @Override
592         protected void cookieSet()
593         {
594             _sessionData.setCookieTime(getLastAccessedTime());
595         }
596 
597         @Override
598         public long getLastAccessedTime()
599         {
600             if (!isValid()) throw new IllegalStateException();
601             return _sessionData.getPreviousAccessTime();
602         }
603 
604         @Override
605         public long getCreationTime() throws IllegalStateException
606         {
607             if (!isValid()) throw new IllegalStateException();
608             return _sessionData.getCreationTime();
609         }
610 
611         // Overridden for visibility
612         @Override
613         protected String getClusterId()
614         {
615             return super.getClusterId();
616         }
617 
618         protected Map newAttributeMap()
619         {
620             // It is important to never return a new attribute map here (as other Session implementations do),
621             // but always return the shared attributes map, so that a new session created on a different cluster
622             // node is immediately filled with the session data from Terracotta.
623             return _sessionData.getAttributeMap();
624         }
625 
626         @Override
627         protected void access(long time)
628         {
629             // The local previous access time is always updated via the super.access() call.
630             // If the requests are steady and within the scavenge period, the distributed shared access times
631             // are never updated. If only one node gets hits, other nodes reach the expiration time and the
632             // scavenging on other nodes will believe the session is expired, since the distributed shared
633             // access times have never been updated.
634             // Therefore we need to update the distributed shared access times once in a while, no matter what.
635             long previousAccessTime = getPreviousAccessTime();
636             if (time - previousAccessTime > getScavengePeriodMs())
637             {
638                 Log.debug("Out-of-date update of distributed access times: previous {} - current {}", previousAccessTime, time);
639                 updateAccessTimes(time);
640             }
641             else
642             {
643                 if (time - _lastUpdate > getScavengePeriodMs())
644                 {
645                     Log.debug("Periodic update of distributed access times: last update {} - current {}", _lastUpdate, time);
646                     updateAccessTimes(time);
647                 }
648                 else
649                 {
650                     Log.debug("Skipping update of distributed access times: previous {} - current {}", previousAccessTime, time);
651                 }
652             }
653             super.access(time);
654         }
655 
656         /**
657          * Updates the shared distributed access times that need to be updated
658          *
659          * @param time the update value
660          */
661         private void updateAccessTimes(long time)
662         {
663             _sessionData.setPreviousAccessTime(_accessed);
664             if (getMaxIdlePeriodMs() > 0) _sessionData.setExpirationTime(time + getMaxIdlePeriodMs());
665             _lastUpdate = time;
666         }
667 
668         // Overridden for visibility
669         @Override
670         protected void timeout()
671         {
672             super.timeout();
673             Log.debug("Timed out session {} with id {}", this, getClusterId());
674         }
675 
676         @Override
677         public void invalidate()
678         {
679             super.invalidate();
680             Log.debug("Invalidated session {} with id {}", this, getClusterId());
681         }
682 
683         private long getMaxIdlePeriodMs()
684         {
685             return _maxIdleMs;
686         }
687 
688         private long getPreviousAccessTime()
689         {
690             return super.getLastAccessedTime();
691         }
692     }
693 
694     /**
695      * The session data that is distributed to cluster nodes via Terracotta.
696      */
697     public static class SessionData
698     {
699         private final String _id;
700         private final Map _attributes;
701         private final long _creation;
702         private final MutableLong _expiration;
703         private long _previousAccess;
704         private long _cookieTime;
705 
706         public SessionData(String sessionId, long maxIdleMs)
707         {
708             _id = sessionId;
709             // Don't need synchronization, as we grab a distributed session id lock
710             // when this map is accessed.
711             _attributes = new HashMap();
712             _creation = System.currentTimeMillis();
713             _expiration = new MutableLong();
714             // Set expiration time to negative value if the session never expires
715             _expiration.value = maxIdleMs > 0 ? _creation + maxIdleMs : -1L;
716         }
717 
718         public String getId()
719         {
720             return _id;
721         }
722 
723         protected Map getAttributeMap()
724         {
725             return _attributes;
726         }
727 
728         public long getCreationTime()
729         {
730             return _creation;
731         }
732 
733         public long getExpirationTime()
734         {
735             return _expiration.value;
736         }
737 
738         public void setExpirationTime(long time)
739         {
740             _expiration.value = time;
741         }
742 
743         public long getCookieTime()
744         {
745             return _cookieTime;
746         }
747 
748         public void setCookieTime(long time)
749         {
750             _cookieTime = time;
751         }
752 
753         public long getPreviousAccessTime()
754         {
755             return _previousAccess;
756         }
757 
758         public void setPreviousAccessTime(long time)
759         {
760             _previousAccess = time;
761         }
762     }
763 
764     protected static class Lock
765     {
766         private static final ThreadLocal<Map<String, Integer>> nestings = new ThreadLocal<Map<String, Integer>>()
767         {
768             @Override
769             protected Map<String, Integer> initialValue()
770             {
771                 return new HashMap<String, Integer>();
772             }
773         };
774 
775         private Lock()
776         {
777         }
778 
779         public static void lock(String lockId)
780         {
781             Integer nestingLevel = nestings.get().get(lockId);
782             if (nestingLevel == null) nestingLevel = 0;
783             if (nestingLevel < 0)
784                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
785             if (nestingLevel == 0)
786             {
787                 ManagerUtil.beginLock(lockId, Manager.LOCK_TYPE_WRITE);
788                 Log.debug("Lock({}) acquired by thread {}", lockId, Thread.currentThread().getName());
789             }
790             nestings.get().put(lockId, nestingLevel + 1);
791             Log.debug("Lock({}) nestings {}", lockId, getLocks());
792         }
793 
794         public static boolean tryLock(String lockId)
795         {
796             boolean result = ManagerUtil.tryBeginLock(lockId, Manager.LOCK_TYPE_WRITE);
797             Log.debug("Lock({}) tried and" + (result ? "" : " not") + " acquired by thread {}", lockId, Thread.currentThread().getName());
798             if (result)
799             {
800                 Integer nestingLevel = nestings.get().get(lockId);
801                 if (nestingLevel == null) nestingLevel = 0;
802                 nestings.get().put(lockId, nestingLevel + 1);
803                 Log.debug("Lock({}) nestings {}", lockId, getLocks());
804             }
805             return result;
806         }
807 
808         public static void unlock(String lockId)
809         {
810             Integer nestingLevel = nestings.get().get(lockId);
811             if (nestingLevel == null || nestingLevel < 1)
812                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
813             if (nestingLevel == 1)
814             {
815                 ManagerUtil.commitLock(lockId);
816                 Log.debug("Lock({}) released by thread {}", lockId, Thread.currentThread().getName());
817                 nestings.get().remove(lockId);
818             }
819             else
820             {
821                 nestings.get().put(lockId, nestingLevel - 1);
822             }
823             Log.debug("Lock({}) nestings {}", lockId, getLocks());
824         }
825 
826         /**
827          * For testing and debugging purposes only.
828          * @return the lock ids held by the current thread
829          */
830         protected static Map<String, Integer> getLocks()
831         {
832             return Collections.unmodifiableMap(nestings.get());
833         }
834     }
835 
836     private static class MutableLong
837     {
838         private long value;
839     }
840 }