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   *
43   * @see TerracottaSessionIdManager
44   */
45  public class TerracottaSessionManager extends AbstractSessionManager implements Runnable
46  {
47      /**
48       * The local cache of session objects.
49       */
50      private Map<String, Session> _sessions;
51      /**
52       * The distributed shared SessionData map.
53       * Putting objects into the map result in the objects being sent to Terracotta, and any change
54       * to the objects are also replicated, recursively.
55       * Getting objects from the map result in the objects being fetched from Terracotta.
56       * The locking of this object in the cluster is automatically handled by Terracotta.
57       */
58      private Map<String, SessionData> _sessionDatas;
59      /**
60       * The distributed shared session expirations map, needed for scavenging.
61       * In particular it supports removal of sessions that have been orphaned by nodeA
62       * (for example because it crashed) by virtue of scavenging performed by nodeB.
63       */
64      private Map<String, MutableLong> _sessionExpirations;
65  
66      private long _scavengePeriodMs = 30000;
67      private ScheduledExecutorService _scheduler;
68      private ScheduledFuture<?> _scavenger;
69  
70      public void doStart() throws Exception
71      {
72          super.doStart();
73  
74          _sessions = Collections.synchronizedMap(new HashMap<String, Session>());
75          _sessionDatas = newSharedMap("sessionData:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context));
76          _sessionExpirations = newSharedMap("sessionExpirations:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context));
77          _scheduler = Executors.newSingleThreadScheduledExecutor();
78          scheduleScavenging();
79      }
80  
81      private Map newSharedMap(String name)
82      {
83          // We want to partition the session data among contexts, so we need to have different roots for
84          // different contexts, and each root must have a different name, since roots with the same name are shared.
85          Lock.lock(name);
86          try
87          {
88              Map result = (Map)ManagerUtil.lookupOrCreateRootNoDepth(name, new Hashtable());
89              ((Manageable)result).__tc_managed().disableAutoLocking();
90              return result;
91          }
92          finally
93          {
94              Lock.unlock(name);
95          }
96      }
97  
98      private void scheduleScavenging()
99      {
100         if (_scavenger != null)
101         {
102             _scavenger.cancel(true);
103             _scavenger = null;
104         }
105         long scavengePeriod = getScavengePeriodMs();
106         if (scavengePeriod > 0 && _scheduler != null)
107             _scavenger = _scheduler.scheduleWithFixedDelay(this, scavengePeriod, scavengePeriod, TimeUnit.MILLISECONDS);
108     }
109 
110     public void doStop() throws Exception
111     {
112         if (_scavenger != null) _scavenger.cancel(true);
113         if (_scheduler != null) _scheduler.shutdownNow();
114         super.doStop();
115     }
116 
117     public void run()
118     {
119         scavenge();
120     }
121 
122     public void enter(Request request)
123     {
124         String requestedSessionId = request.getRequestedSessionId();
125         if (requestedSessionId == null) return;
126 
127         enter(getIdManager().getClusterId(requestedSessionId));
128     }
129 
130     protected void enter(String clusterId)
131     {
132         Lock.lock(newLockId(clusterId));
133     }
134 
135     public void exit(Request request)
136     {
137         String clusterId = null;
138         String requestedSessionId = request.getRequestedSessionId();
139         if (requestedSessionId == null)
140         {
141             HttpSession session = request.getSession(false);
142             if (session != null) clusterId = getIdManager().getClusterId(session.getId());
143         }
144         else
145         {
146             clusterId = getIdManager().getClusterId(requestedSessionId);
147         }
148 
149         if (clusterId != null) exit(clusterId);
150     }
151 
152     protected void exit(String clusterId)
153     {
154         Lock.unlock(newLockId(clusterId));
155     }
156 
157     @Override
158     protected void addSession(AbstractSessionManager.Session session, boolean created)
159     {
160         /**
161          * SESSION LOCKING
162          * This is an entry point for session locking.
163          * We enter here when a new session is requested via request.getSession(true).
164          */
165         if (created) enter(((Session)session).getClusterId());
166         super.addSession(session, created);
167     }
168 
169     protected void addSession(AbstractSessionManager.Session session)
170     {
171         /**
172          * SESSION LOCKING
173          * When this method is called, we already hold the session lock.
174          * See {@link #addSession(AbstractSessionManager.Session, boolean)}
175          */
176         String clusterId = getClusterId(session);
177         Session tcSession = (Session)session;
178         SessionData sessionData = tcSession.getSessionData();
179         _sessionExpirations.put(clusterId, sessionData._expiration);
180         _sessionDatas.put(clusterId, sessionData);
181         _sessions.put(clusterId, tcSession);
182         Log.debug("Added session {} with id {}", tcSession, clusterId);
183     }
184 
185     @Override
186     public Cookie access(HttpSession session, boolean secure)
187     {
188         Cookie cookie = super.access(session, secure);
189         Log.debug("Accessed session {} with id {}", session, session.getId());
190         return cookie;
191     }
192 
193     @Override
194     public void complete(HttpSession session)
195     {
196         super.complete(session);
197         Log.debug("Completed session {} with id {}", session, session.getId());
198     }
199 
200     protected void removeSession(String clusterId)
201     {
202         /**
203          * SESSION LOCKING
204          * When this method is called, we already hold the session lock.
205          * Either the scavenger acquired it, or the user invalidated
206          * the existing session and thus {@link #enter(String)} was called.
207          */
208 
209         // Remove locally cached session
210         Session session = _sessions.remove(clusterId);
211         Log.debug("Removed session {} with id {}", session, clusterId);
212 
213         // It may happen that one node removes its expired session data,
214         // so that when this node does the same, the session data is already gone
215         SessionData sessionData = _sessionDatas.remove(clusterId);
216         Log.debug("Removed session data {} with id {}", sessionData, clusterId);
217 
218         // Remove the expiration entry used in scavenging
219         _sessionExpirations.remove(clusterId);
220     }
221 
222     public void setScavengePeriodMs(long ms)
223     {
224         this._scavengePeriodMs = ms;
225         scheduleScavenging();
226     }
227 
228     public long getScavengePeriodMs()
229     {
230         return _scavengePeriodMs;
231     }
232 
233     public AbstractSessionManager.Session getSession(String clusterId)
234     {
235         Session result = null;
236 
237         /**
238          * SESSION LOCKING
239          * This is an entry point for session locking.
240          * We lookup the session given the id, and if it exist we hold the lock.
241          * We unlock on end of method, since this method can be called outside
242          * an {@link #enter(String)}/{@link #exit(String)} pair.
243          */
244         enter(clusterId);
245         try
246         {
247             // Need to synchronize because we use a get-then-put that must be atomic
248             // on the local session cache
249             synchronized (_sessions)
250             {
251                 result = _sessions.get(clusterId);
252                 if (result == null)
253                 {
254                     Log.debug("Session with id {} --> local cache miss", clusterId);
255 
256                     // Lookup the distributed shared sessionData object.
257                     // This will migrate the session data to this node from the Terracotta server
258                     // We have not grabbed the distributed lock associated with this session yet,
259                     // so another node can migrate the session data as well. This is no problem,
260                     // since just after this method returns the distributed lock will be grabbed by
261                     // one node, the session data will be changed and the lock released.
262                     // The second node contending for the distributed lock will then acquire it,
263                     // and the session data information will be migrated lazily by Terracotta means.
264                     // We are only interested in having a SessionData reference locally.
265                     Log.debug("Distributed session data with id {} --> lookup", clusterId);
266                     SessionData sessionData = _sessionDatas.get(clusterId);
267                     if (sessionData == null)
268                     {
269                         Log.debug("Distributed session data with id {} --> not found", clusterId);
270                     }
271                     else
272                     {
273                         Log.debug("Distributed session data with id {} --> found", clusterId);
274                         // Wrap the migrated session data and cache the Session object
275                         result = new Session(sessionData);
276                         _sessions.put(clusterId, result);
277                     }
278                 }
279                 else
280                 {
281                     Log.debug("Session with id {} --> local cache hit", clusterId);
282                     if (!_sessionExpirations.containsKey(clusterId))
283                     {
284                         // A session is present in the local cache, but it has been expired
285                         // or invalidated on another node, perform local clean up.
286                         _sessions.remove(clusterId);
287                         result = null;
288                         Log.debug("Session with id {} --> local cache stale");
289                     }
290                 }
291             }
292         }
293         finally
294         {
295             /**
296              * SESSION LOCKING
297              */
298             exit(clusterId);
299         }
300         return result;
301     }
302 
303     protected String newLockId(String clusterId)
304     {
305         StringBuilder builder = new StringBuilder(clusterId);
306         builder.append(":").append(canonicalize(_context.getContextPath()));
307         builder.append(":").append(virtualHostFrom(_context));
308         return builder.toString();
309     }
310 
311     // TODO: This method is not needed, only used for testing
312     public Map getSessionMap()
313     {
314         return Collections.unmodifiableMap(_sessions);
315     }
316 
317     // TODO: rename to getSessionsCount()
318     // TODO: also, not used if not by superclass for unused statistics data
319     public int getSessions()
320     {
321         return _sessions.size();
322     }
323 
324     protected Session newSession(HttpServletRequest request)
325     {
326         return new Session(request);
327     }
328 
329     protected void invalidateSessions()
330     {
331         // Do nothing.
332         // We don't want to remove and invalidate all the sessions,
333         // because this method is called from doStop(), and just
334         // because this context is stopping does not mean that we
335         // should remove the session from any other node (remember
336         // the session map is shared)
337     }
338 
339     private void scavenge()
340     {
341         Thread thread = Thread.currentThread();
342         ClassLoader old_loader = thread.getContextClassLoader();
343         if (_loader != null) thread.setContextClassLoader(_loader);
344         try
345         {
346             long now = System.currentTimeMillis();
347             Log.debug(this + " scavenging at {}, scavenge period {}", now, getScavengePeriodMs());
348 
349             // Detect the candidates that may have expired already, checking the estimated expiration time.
350             Set<String> candidates = new HashSet<String>();
351             String lockId = "scavenge:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context);
352             Lock.lock(lockId);
353             try
354             {
355                 for (Map.Entry<String, MutableLong> entry : _sessionExpirations.entrySet())
356                 {
357                     String sessionId = entry.getKey();
358                     long expirationTime = entry.getValue().value;
359                     Log.debug("Estimated expiration time {} for session {}", expirationTime, sessionId);
360                     if (expirationTime > 0 && expirationTime < now) candidates.add(sessionId);
361                 }
362             }
363             finally
364             {
365                 Lock.unlock(lockId);
366             }
367             Log.debug("Scavenging detected {} candidate sessions to expire", candidates.size());
368 
369             // Now validate that the candidates that do expire are really expired,
370             // grabbing the session lock for each candidate
371             for (String sessionId : candidates)
372             {
373                 Session candidate = (Session)getSession(sessionId);
374                 // Here we grab the lock to avoid anyone else interfering
375                 enter(sessionId);
376                 try
377                 {
378                     long maxInactiveTime = candidate.getMaxIdlePeriodMs();
379                     // Exclude sessions that never expire
380                     if (maxInactiveTime > 0)
381                     {
382                         // The lastAccessedTime is fetched from Terracotta, so we're sure it is up-to-date.
383                         long lastAccessedTime = candidate.getLastAccessedTime();
384                         // Since we write the shared lastAccessedTime every scavenge period,
385                         // take that in account before considering the session expired
386                         long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
387                         if (expirationTime < now)
388                         {
389                             Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
390                             // Calling timeout() result in calling removeSession(), that will clean the data structures
391                             candidate.timeout();
392                         }
393                         else
394                         {
395                             Log.debug("Scavenging skipping candidate session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
396                         }
397                     }
398                 }
399                 finally
400                 {
401                     exit(sessionId);
402                 }
403             }
404 
405             int sessionCount = getSessions();
406             if (sessionCount < _minSessions) _minSessions = sessionCount;
407             if (sessionCount > _maxSessions) _maxSessions = sessionCount;
408         }
409         finally
410         {
411             thread.setContextClassLoader(old_loader);
412         }
413     }
414 
415     private String canonicalize(String contextPath)
416     {
417         if (contextPath == null) return "";
418         return contextPath.replace('/', '_').replace('.', '_').replace('\\', '_');
419     }
420 
421     private String virtualHostFrom(ContextHandler.SContext context)
422     {
423         String result = "0.0.0.0";
424         if (context == null) return result;
425 
426         String[] vhosts = context.getContextHandler().getVirtualHosts();
427         if (vhosts == null || vhosts.length == 0 || vhosts[0] == null) return result;
428 
429         return vhosts[0];
430     }
431 
432     class Session extends AbstractSessionManager.Session
433     {
434         private static final long serialVersionUID = -2134521374206116367L;
435 
436         private final SessionData _sessionData;
437         private long _lastUpdate;
438 
439         protected Session(HttpServletRequest request)
440         {
441             super(request);
442             _sessionData = new SessionData(getClusterId(), _maxIdleMs);
443             _lastAccessed = _sessionData.getCreationTime();
444         }
445 
446         protected Session(SessionData sd)
447         {
448             super(sd.getCreationTime(), sd.getId());
449             _sessionData = sd;
450             _lastAccessed = getLastAccessedTime();
451             initValues();
452         }
453 
454         public SessionData getSessionData()
455         {
456             return _sessionData;
457         }
458 
459         @Override
460         public long getCookieSetTime()
461         {
462             return _sessionData.getCookieTime();
463         }
464 
465         @Override
466         protected void cookieSet()
467         {
468             _sessionData.setCookieTime(getLastAccessedTime());
469         }
470 
471         @Override
472         public long getLastAccessedTime()
473         {
474             if (!isValid()) throw new IllegalStateException();
475             return _sessionData.getPreviousAccessTime();
476         }
477 
478         @Override
479         public long getCreationTime() throws IllegalStateException
480         {
481             if (!isValid()) throw new IllegalStateException();
482             return _sessionData.getCreationTime();
483         }
484 
485         // Overridden for visibility
486         @Override
487         protected String getClusterId()
488         {
489             return super.getClusterId();
490         }
491 
492         protected Map newAttributeMap()
493         {
494             // It is important to never return a new attribute map here (as other Session implementations do),
495             // but always return the shared attributes map, so that a new session created on a different cluster
496             // node is immediately filled with the session data from Terracotta.
497             return _sessionData.getAttributeMap();
498         }
499 
500         @Override
501         protected void access(long time)
502         {
503             // The local previous access time is always updated via the super.access() call.
504             // If the requests are steady and within the scavenge period, the distributed shared access times
505             // are never updated. If only one node gets hits, other nodes reach the expiration time and the
506             // scavenging on other nodes will believe the session is expired, since the distributed shared
507             // access times have never been updated.
508             // Therefore we need to update the distributed shared access times once in a while, no matter what.
509             long previousAccessTime = getPreviousAccessTime();
510             if (time - previousAccessTime > getScavengePeriodMs())
511             {
512                 Log.debug("Out-of-date update of distributed access times: previous {} - current {}", previousAccessTime, time);
513                 updateAccessTimes(time);
514             }
515             else
516             {
517                 if (time - _lastUpdate > getScavengePeriodMs())
518                 {
519                     Log.debug("Periodic update of distributed access times: last update {} - current {}", _lastUpdate, time);
520                     updateAccessTimes(time);
521                 }
522                 else
523                 {
524                     Log.debug("Skipping update of distributed access times: previous {} - current {}", previousAccessTime, time);
525                 }
526             }
527             super.access(time);
528         }
529 
530         /**
531          * Updates the shared distributed access times that need to be updated
532          *
533          * @param time the update value
534          */
535         private void updateAccessTimes(long time)
536         {
537             _sessionData.setPreviousAccessTime(_accessed);
538             if (getMaxIdlePeriodMs() > 0) _sessionData.setExpirationTime(time + getMaxIdlePeriodMs());
539             _lastUpdate = time;
540         }
541 
542         // Overridden for visibility
543         @Override
544         protected void timeout()
545         {
546             super.timeout();
547             Log.debug("Timed out session {} with id {}", this, getClusterId());
548         }
549 
550         @Override
551         public void invalidate()
552         {
553             super.invalidate();
554             Log.debug("Invalidated session {} with id {}", this, getClusterId());
555         }
556 
557         private long getMaxIdlePeriodMs()
558         {
559             return _maxIdleMs;
560         }
561 
562         private long getPreviousAccessTime()
563         {
564             return super.getLastAccessedTime();
565         }
566     }
567 
568     /**
569      * The session data that is distributed to cluster nodes via Terracotta.
570      */
571     public static class SessionData
572     {
573         private final String _id;
574         private final Map _attributes;
575         private final long _creation;
576         private final MutableLong _expiration;
577         private long _previousAccess;
578         private long _cookieTime;
579 
580         public SessionData(String sessionId, long maxIdleMs)
581         {
582             _id = sessionId;
583             // Don't need synchronization, as we grab a distributed session id lock
584             // when this map is accessed.
585             _attributes = new HashMap();
586             _creation = System.currentTimeMillis();
587             _expiration = new MutableLong();
588             // Set expiration time to negative value if the session never expires
589             _expiration.value = maxIdleMs > 0 ? _creation + maxIdleMs : -1L;
590         }
591 
592         public String getId()
593         {
594             return _id;
595         }
596 
597         protected Map getAttributeMap()
598         {
599             return _attributes;
600         }
601 
602         public long getCreationTime()
603         {
604             return _creation;
605         }
606 
607         public long getExpirationTime()
608         {
609             return _expiration.value;
610         }
611 
612         public void setExpirationTime(long time)
613         {
614             _expiration.value = time;
615         }
616 
617         public long getCookieTime()
618         {
619             return _cookieTime;
620         }
621 
622         public void setCookieTime(long time)
623         {
624             _cookieTime = time;
625         }
626 
627         public long getPreviousAccessTime()
628         {
629             return _previousAccess;
630         }
631 
632         public void setPreviousAccessTime(long time)
633         {
634             _previousAccess = time;
635         }
636     }
637 
638     private static class Lock
639     {
640         private static final ThreadLocal<Map<String, Integer>> nestings = new ThreadLocal<Map<String, Integer>>()
641         {
642             @Override
643             protected Map<String, Integer> initialValue()
644             {
645                 return new HashMap<String, Integer>();
646             }
647         };
648 
649         private Lock()
650         {
651         }
652 
653         public static void lock(String lockId)
654         {
655             Integer nestingLevel = nestings.get().get(lockId);
656             if (nestingLevel == null) nestingLevel = 0;
657             if (nestingLevel < 0)
658                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + nestings.get());
659             if (nestingLevel == 0)
660             {
661                 ManagerUtil.beginLock(lockId, Manager.LOCK_TYPE_WRITE);
662                 Log.debug("Lock({}) acquired by thread {}", lockId, Thread.currentThread().getName());
663             }
664             nestings.get().put(lockId, nestingLevel + 1);
665             Log.debug("Lock({}) nestings {}", lockId, nestings.get());
666         }
667 
668         public static void unlock(String lockId)
669         {
670             Integer nestingLevel = nestings.get().get(lockId);
671             if (nestingLevel == null || nestingLevel < 1)
672                 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + nestings.get());
673             if (nestingLevel == 1)
674             {
675                 ManagerUtil.commitLock(lockId);
676                 Log.debug("Lock({}) released by thread {}", lockId, Thread.currentThread().getName());
677                 nestings.get().remove(lockId);
678             }
679             else
680             {
681                 nestings.get().put(lockId, nestingLevel - 1);
682             }
683             Log.debug("Lock({}) nestings {}", lockId, nestings.get());
684         }
685     }
686 
687     private static class MutableLong
688     {
689         private long value;
690     }
691 }