1
2
3
4
5
6
7
8
9
10
11
12
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
42
43
44
45 public class TerracottaSessionManager extends AbstractSessionManager implements Runnable
46 {
47
48
49
50 private Map<String, Session> _sessions;
51
52
53
54
55
56
57
58 private Map<String, SessionData> _sessionDatas;
59
60
61
62
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
84
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
162
163
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
173
174
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
204
205
206
207
208
209
210 Session session = _sessions.remove(clusterId);
211 Log.debug("Removed session {} with id {}", session, clusterId);
212
213
214
215 SessionData sessionData = _sessionDatas.remove(clusterId);
216 Log.debug("Removed session data {} with id {}", sessionData, clusterId);
217
218
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
239
240
241
242
243
244 enter(clusterId);
245 try
246 {
247
248
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
257
258
259
260
261
262
263
264
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
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
285
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
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
312 public Map getSessionMap()
313 {
314 return Collections.unmodifiableMap(_sessions);
315 }
316
317
318
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
332
333
334
335
336
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
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
370
371 for (String sessionId : candidates)
372 {
373 Session candidate = (Session)getSession(sessionId);
374
375 enter(sessionId);
376 try
377 {
378 long maxInactiveTime = candidate.getMaxIdlePeriodMs();
379
380 if (maxInactiveTime > 0)
381 {
382
383 long lastAccessedTime = candidate.getLastAccessedTime();
384
385
386 long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
387 if (expirationTime < now)
388 {
389 Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
390
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
486 @Override
487 protected String getClusterId()
488 {
489 return super.getClusterId();
490 }
491
492 protected Map newAttributeMap()
493 {
494
495
496
497 return _sessionData.getAttributeMap();
498 }
499
500 @Override
501 protected void access(long time)
502 {
503
504
505
506
507
508
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
532
533
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
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
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
584
585 _attributes = new HashMap();
586 _creation = System.currentTimeMillis();
587 _expiration = new MutableLong();
588
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 }