View Javadoc

1   package org.mortbay.terracotta.servlet;
2   
3   import java.util.Collections;
4   import java.util.HashMap;
5   import java.util.HashSet;
6   import java.util.Map;
7   import java.util.Set;
8   import java.util.concurrent.Executors;
9   import java.util.concurrent.ScheduledExecutorService;
10  import java.util.concurrent.ScheduledFuture;
11  import java.util.concurrent.TimeUnit;
12  
13  import javax.servlet.http.HttpServletRequest;
14  
15  import com.tc.object.bytecode.Manager;
16  import com.tc.object.bytecode.ManagerUtil;
17  import org.mortbay.jetty.handler.ContextHandler;
18  import org.mortbay.jetty.servlet.AbstractSessionManager;
19  import org.mortbay.log.Log;
20  
21  /**
22   * TODO: handle sessions that do not expire
23   * A specialized SessionManager to be used with <a href="http://www.terracotta.org">Terracotta</a>.
24   *
25   * @see TerracottaSessionIdManager
26   */
27  public class TerracottaSessionManager extends AbstractSessionManager implements Runnable
28  {
29      /**
30       * The local cache of session objects.
31       */
32      private Map<String, Session> _sessions;
33      /**
34       * The distributed shared SessionData map.
35       * Putting objects into the map result in the objects being sent to Terracotta, and any change
36       * to the objects are also replicated, recursively.
37       * Getting objects from the map result in the objects being fetched from Terracotta.
38       * The locking of this object in the cluster is automatically handled by Terracotta.
39       */
40      private Map<String, SessionData> _sessionData;
41      /**
42       * The distributed shared session expirations map, needed for scavenging.
43       * In particular it supports removal of sessions that have been orphaned by nodeA
44       * (for example because it crashed) by virtue of scavenging performed by nodeB.
45       */
46      private Map<String, MutableLong> _sessionExpirations;
47  
48      private long _scavengePeriodMs = 30000;
49      private ScheduledExecutorService _scheduler;
50      private ScheduledFuture<?> _scavenger;
51  
52      public void doStart() throws Exception
53      {
54          super.doStart();
55  
56          _sessions = Collections.synchronizedMap(new HashMap<String, Session>());
57          _sessionData = newSharedMap("sessionData:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context));
58          _sessionExpirations = newSharedMap("sessionExpirations:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context));
59          _scheduler = Executors.newSingleThreadScheduledExecutor();
60          scheduleScavenging();
61      }
62  
63      private Map newSharedMap(String name)
64      {
65          // We want to partition the session data among contexts, so we need to have different roots for
66          // different contexts, and each root must have a different name, since roots with the same name are shared.
67          final Lock lock = new Lock(name);
68          lock.lock();
69          try
70          {
71              return (Map)ManagerUtil.lookupOrCreateRootNoDepth(name, Collections.synchronizedMap(new HashMap()));
72          }
73          finally
74          {
75              lock.unlock();
76          }
77      }
78  
79      private void scheduleScavenging()
80      {
81          if (_scavenger != null)
82          {
83              _scavenger.cancel(true);
84              _scavenger = null;
85          }
86          long scavengePeriod = getScavengePeriodMs();
87          if (scavengePeriod > 0 && _scheduler != null)
88              _scavenger = _scheduler.scheduleWithFixedDelay(this, scavengePeriod, scavengePeriod, TimeUnit.MILLISECONDS);
89      }
90  
91      public void doStop() throws Exception
92      {
93          if (_scavenger != null) _scavenger.cancel(true);
94          if (_scheduler != null) _scheduler.shutdownNow();
95          super.doStop();
96      }
97  
98      public void run()
99      {
100         scavenge();
101     }
102 
103     protected void addSession(AbstractSessionManager.Session session)
104     {
105         String clusterId = getClusterId(session);
106         Session tcSession = (Session)session;
107         SessionData sessionData = tcSession.getSessionData();
108         _sessionExpirations.put(clusterId, sessionData._expiration);
109         _sessionData.put(clusterId, sessionData);
110         _sessions.put(clusterId, tcSession);
111         Log.debug("Added session {} with id {}", tcSession, clusterId);
112     }
113 
114     protected void removeSession(String clusterId)
115     {
116         // Remove locally cached session
117         Object o = _sessions.remove(clusterId);
118         Log.debug("Removed session {} with id {}", o, clusterId);
119         // It may happen that one node removes its expired session data,
120         // so that when this node does the same, the session data is already gone
121         o = _sessionData.remove(clusterId);
122         _sessionExpirations.remove(clusterId);
123         Log.debug("Removed session data {} with id {}", o, clusterId);
124     }
125 
126     public void setScavengePeriodMs(long ms)
127     {
128         this._scavengePeriodMs = ms;
129         scheduleScavenging();
130     }
131 
132     public long getScavengePeriodMs()
133     {
134         return _scavengePeriodMs;
135     }
136 
137     public AbstractSessionManager.Session getSession(String clusterId)
138     {
139         // Need to synchronize because we use a get-then-put that must be atomic.
140         synchronized (_sessions)
141         {
142             Session session = _sessions.get(clusterId);
143             if (session == null)
144             {
145                 Log.debug("Session with id {} --> local cache miss", clusterId);
146 
147                 // Lookup the distributed shared sessionData object.
148                 // This will migrate the session data to this node from the Terracotta server
149                 // We have not grabbed the distributed lock associated with this session yet,
150                 // so another node can migrate the session data as well. This is no problem,
151                 // since just after this method returns the distributed lock will be grabbed by
152                 // one node, the session data will be changed and the lock released.
153                 // The second node contending for the distributed lock will then acquire it,
154                 // and the session data information will be migrated lazily by Terracotta means.
155                 // We are only interested in having a SessionData reference locally.
156                 Log.debug("Distributed session data with id {} --> lookup", clusterId);
157                 SessionData sessionData = _sessionData.get(clusterId);
158                 if (sessionData == null)
159                 {
160                     Log.debug("Distributed session data with id {} --> not found", clusterId);
161                     return null;
162                 }
163                 else
164                 {
165                     Log.debug("Distributed session data with id {} --> found", clusterId);
166                     // Wrap the migrated session data and cache the Session object
167                     session = new Session(sessionData);
168                     _sessions.put(clusterId, session);
169                 }
170             }
171             else
172             {
173                 Log.debug("Session with id {} --> local cache hit", clusterId);
174             }
175             return session;
176         }
177     }
178 
179     // TODO: This method is not needed, only used for testing
180     public Map getSessionMap()
181     {
182         return Collections.unmodifiableMap(_sessions);
183     }
184 
185     // TODO: rename to getSessionsCount()
186     // TODO: also, not used if not by superclass for unused statistics data
187     public int getSessions()
188     {
189         return _sessions.size();
190     }
191 
192     protected Session newSession(HttpServletRequest request)
193     {
194         return new Session(request);
195     }
196 
197     protected void invalidateSessions()
198     {
199         // Do nothing.
200         // We don't want to remove and invalidate all the sessions,
201         // because this method is called from doStop(), and just
202         // because this context is stopping does not mean that we
203         // should remove the session from any other node (remember
204         // the session map is shared)
205     }
206 
207     private void scavenge()
208     {
209         Thread thread = Thread.currentThread();
210         ClassLoader old_loader = thread.getContextClassLoader();
211         if (_loader != null) thread.setContextClassLoader(_loader);
212         try
213         {
214             long now = System.currentTimeMillis();
215             Log.debug(this + " scavenging at {}, scavenge period {}", now, getScavengePeriodMs());
216 
217             // Detect the candidates that may have expired already, checking the estimated expiration time.
218             Set<String> candidates = new HashSet<String>();
219             final Lock lock = new Lock("scavenge:" + canonicalize(_context.getContextPath()) + ":" + virtualHostFrom(_context));
220             lock.lock();
221             try
222             {
223                 for (Map.Entry<String, MutableLong> entry : _sessionExpirations.entrySet())
224                 {
225                     String sessionId = entry.getKey();
226                     long expirationTime = entry.getValue().value;
227                     Log.debug("Estimated expiration time {} for session {}", expirationTime, sessionId);
228                     if (expirationTime > 0 && expirationTime < now) candidates.add(sessionId);
229                 }
230             }
231             finally
232             {
233                 lock.unlock();
234             }
235             Log.debug("Scavenging detected {} candidate sessions to expire", candidates.size());
236 
237             // Now validate that the candidates that do expire are really expired,
238             // grabbing the session lock for each candidate
239             for (String sessionId : candidates)
240             {
241                 Session candidate = (Session)getSession(sessionId);
242                 // We will work on the session, grab the lock to avoid anyone else interfering
243                 candidate.lock();
244                 try
245                 {
246                     long maxInactiveTime = candidate.getMaxIdlePeriodMs();
247                     // Exclude sessions that never expire
248                     if (maxInactiveTime > 0)
249                     {
250                         // The lastAccessedTime is fetched from Terracotta, so we're sure it is up-to-date.
251                         long lastAccessedTime = candidate.getLastAccessedTime();
252                         // Since we write the shared lastAccessedTime every scavenge period,
253                         // take that in account before considering the session expired
254                         long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
255                         if (expirationTime < now)
256                         {
257                             Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
258                             // Calling timeout() result in calling removeSession(), that will clean the data structures
259                             candidate.timeout();
260                         }
261                         else
262                         {
263                             Log.debug("Scavenging skipping candidate session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
264                         }
265                     }
266                 }
267                 finally
268                 {
269                     candidate.unlock();
270                 }
271             }
272 
273             int sessionCount = getSessions();
274             if (sessionCount < _minSessions) _minSessions = sessionCount;
275             if (sessionCount > _maxSessions) _maxSessions = sessionCount;
276         }
277         finally
278         {
279             thread.setContextClassLoader(old_loader);
280         }
281     }
282 
283     private String canonicalize(String contextPath)
284     {
285         if (contextPath == null) return "";
286         return contextPath.replace('/', '_').replace('.', '_').replace('\\', '_');
287     }
288 
289     private String virtualHostFrom(ContextHandler.SContext context)
290     {
291         String result = "0.0.0.0";
292         if (context == null) return result;
293 
294         String[] vhosts = context.getContextHandler().getVirtualHosts();
295         if (vhosts==null || vhosts.length == 0 || vhosts[0] == null) return result;
296 
297         return vhosts[0];
298     }
299 
300     class Session extends AbstractSessionManager.Session
301     {
302         private static final long serialVersionUID = -2134521374206116367L;
303 
304         private final SessionData _sessionData;
305         private final Lock _lock;
306         private long _lastUpdate;
307 
308         protected Session(HttpServletRequest request)
309         {
310             super(request);
311             String lockId = new StringBuilder(getClusterId()).
312                     append(":").
313                     append(canonicalize(_context.getContextPath())).
314                     append(":").
315                     append(virtualHostFrom(_context)).
316                     toString();
317             _sessionData = new SessionData(getClusterId(), lockId, _maxIdleMs);
318             _lastAccessed = _sessionData.getCreationTime();
319             _lock = new Lock(_sessionData.getLockId());
320             // This is a bit asymmetric, but the first time a session is created,
321             // access() is not called (though complete() will be). So we have to lock() here.
322             lock();
323         }
324 
325         protected Session(SessionData sd)
326         {
327             super(sd.getCreationTime(), sd.getId());
328             _sessionData = sd;
329             _lastAccessed = getLastAccessedTime();
330             _lock = new Lock(sd.getLockId());
331             initValues();
332         }
333 
334         public SessionData getSessionData()
335         {
336             return _sessionData;
337         }
338 
339         public long getCookieSetTime()
340         {
341             return _sessionData.getCookieTime();
342         }
343 
344         protected void cookieSet()
345         {
346             _sessionData.setCookieTime(getLastAccessedTime());
347         }
348 
349         public long getLastAccessedTime()
350         {
351             if (!isValid()) throw new IllegalStateException();
352             return _sessionData.getPreviousAccessTime();
353         }
354 
355         public long getCreationTime() throws IllegalStateException
356         {
357             if (!isValid()) throw new IllegalStateException();
358             return _sessionData.getCreationTime();
359         }
360 
361         // Overridden for visibility
362         protected String getClusterId()
363         {
364             return super.getClusterId();
365         }
366 
367         protected Map newAttributeMap()
368         {
369             // It is important to never return a new attribute map here (as other Session implementations do),
370             // but always return the shared attributes map, so that a new session created on a different cluster
371             // node is immediately filled with the session data from Terracotta.
372             return _sessionData.getAttributeMap();
373         }
374 
375         protected void access(long time)
376         {
377             // This method is called once at the beginning of every request.
378             // We grab the distributed lock, so that now this node owns the session data.
379             lock();
380 
381             // The local previous access time is always updated via the super.access() call.
382             // If the requests are steady and within the scavenge period, the distributed shared access times
383             // are never updated. If only one node gets hits, other nodes reach the expiration time and the
384             // scavenging on other nodes will believe the session is expired, since the distributed shared
385             // access times have never been updated.
386             // Therefore we need to update the distributed shared access times once in a while, no matter what.
387             long previousAccessTime = getPreviousAccessTime();
388             if (time - previousAccessTime > getScavengePeriodMs())
389             {
390                 Log.debug("Out-of-date update of distributed access times: previous {} - current {}", previousAccessTime, time);
391                 updateAccessTimes(time);
392             }
393             else
394             {
395                 if (time - _lastUpdate > getScavengePeriodMs())
396                 {
397                     Log.debug("Periodic update of distributed access times: last update {} - current {}", _lastUpdate, time);
398                     updateAccessTimes(time);
399                 }
400                 else
401                 {
402                     Log.debug("Skipping update of distributed access times: previous {} - current {}", previousAccessTime, time);
403                 }
404             }
405             super.access(time);
406         }
407 
408         protected void complete()
409         {
410             // This method is called once at the end of every request.
411             // We release the distributed lock
412             super.complete();
413             unlock();
414         }
415 
416         /**
417          * Updates the shared distributed access times that need to be updated
418          * @param time the update value
419          */
420         private void updateAccessTimes(long time)
421         {
422             _sessionData.setPreviousAccessTime(_accessed);
423             if (getMaxIdlePeriodMs() > 0) _sessionData.setExpirationTime(time + getMaxIdlePeriodMs());
424             _lastUpdate = time;
425         }
426 
427         // Overridden for visibility
428         protected void timeout()
429         {
430             super.timeout();
431         }
432 
433         protected void lock()
434         {
435             _lock.lock();
436         }
437 
438         protected void unlock()
439         {
440             _lock.unlock();
441         }
442 
443         private long getMaxIdlePeriodMs()
444         {
445             return _maxIdleMs;
446         }
447 
448         private long getPreviousAccessTime()
449         {
450             return super.getLastAccessedTime();
451         }
452     }
453 
454     /**
455      * The session data that is distributed to cluster nodes via Terracotta.
456      */
457     public static class SessionData
458     {
459         private final String _id;
460         private final String _lockId;
461         private final Map _attributes;
462         private final long _creation;
463         private final MutableLong _expiration;
464         private long _previousAccess;
465         private long _cookieTime;
466 
467         public SessionData(String sessionId, String lockId, long maxIdleMs)
468         {
469             _id = sessionId;
470             _lockId = lockId;
471             // Don't need synchronization, as we grab a distributed session id lock
472             // when this map is accessed.
473             _attributes = new HashMap();
474             _creation = System.currentTimeMillis();
475             _expiration = new MutableLong();
476             // Set expiration time to negative value if the session never expires
477             _expiration.value = maxIdleMs > 0 ? _creation + maxIdleMs : -1L;
478         }
479 
480         public String getId()
481         {
482             return _id;
483         }
484 
485         public String getLockId()
486         {
487             return _lockId;
488         }
489 
490         protected Map getAttributeMap()
491         {
492             return _attributes;
493         }
494 
495         public long getCreationTime()
496         {
497             return _creation;
498         }
499 
500         public long getExpirationTime()
501         {
502             return _expiration.value;
503         }
504 
505         public void setExpirationTime(long time)
506         {
507             _expiration.value = time;
508         }
509 
510         public long getCookieTime()
511         {
512             return _cookieTime;
513         }
514 
515         public void setCookieTime(long time)
516         {
517             _cookieTime = time;
518         }
519 
520         public long getPreviousAccessTime()
521         {
522             return _previousAccess;
523         }
524 
525         public void setPreviousAccessTime(long time)
526         {
527             _previousAccess = time;
528         }
529     }
530 
531     private static class Lock
532     {
533         private final String _id;
534 
535         public Lock(String id)
536         {
537             this._id = id;
538         }
539 
540         public void lock()
541         {
542             Log.debug("Locking lock({}) by thread {}", _id, Thread.currentThread().getName());
543             ManagerUtil.beginLock(_id, Manager.LOCK_TYPE_WRITE);
544         }
545 
546         public void unlock()
547         {
548             Log.debug("Unlocking lock({}) by thread {}", _id, Thread.currentThread().getName());
549             ManagerUtil.commitLock(_id);
550         }
551     }
552 
553     private static class MutableLong
554     {
555         private long value;
556     }
557 }