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 }