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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 public class TerracottaSessionManager extends AbstractSessionManager implements Runnable
85 {
86
87
88
89 private Map<String, Session> _sessions;
90
91
92
93
94
95
96 private Map<String, SessionData> _sessionDatas;
97
98
99
100
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
126
127 Lock.lock(name);
128 try
129 {
130
131
132
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
171
172
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
181
182
183 }
184 else
185 {
186
187
188
189
190
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
210
211
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
222 }
223 else
224 {
225
226 exit(getClusterId(session));
227 }
228 }
229 else
230 {
231
232 String requestedClusterId = getIdManager().getClusterId(requestedSessionId);
233 exit(requestedClusterId);
234
235 if (session != null)
236 {
237 if (!requestedClusterId.equals(getClusterId(session)))
238 {
239
240
241
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
258
259
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
289
290
291
292
293
294
295 Session session = _sessions.remove(clusterId);
296 Log.debug("Removed session {} with id {}", session, clusterId);
297
298
299
300 SessionData sessionData = _sessionDatas.remove(clusterId);
301 Log.debug("Removed session data {} with id {}", sessionData, clusterId);
302
303
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
324
325
326
327
328
329 enter(clusterId);
330 try
331 {
332
333
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
342
343
344
345
346
347
348
349
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
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
370
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
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
397 public Map getSessionMap()
398 {
399 return Collections.unmodifiableMap(_sessions);
400 }
401
402
403
404 public int getSessions()
405 {
406 return _sessions.size();
407 }
408
409 protected Session newSession(HttpServletRequest request)
410 {
411
412
413
414
415
416
417 Session result = new Session(request);
418
419 String requestedSessionId = request.getRequestedSessionId();
420 if (requestedSessionId == null)
421 {
422
423 enter(result.getClusterId());
424 }
425 else
426 {
427 if (result.getClusterId().equals(getIdManager().getClusterId(requestedSessionId)))
428 {
429
430
431
432
433 }
434 else
435 {
436
437
438 enter(result.getClusterId());
439 }
440 }
441 return result;
442 }
443
444 protected void invalidateSessions()
445 {
446
447
448
449
450
451
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
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
493
494 for (String sessionId : candidates)
495 {
496 Session candidate = (Session)getSession(sessionId);
497
498 boolean entered = tryEnter(sessionId);
499 if (entered)
500 {
501 try
502 {
503 long maxInactiveTime = candidate.getMaxIdlePeriodMs();
504
505 if (maxInactiveTime > 0)
506 {
507
508 long lastAccessedTime = candidate.getLastAccessedTime();
509
510
511 long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
512 if (expirationTime < now)
513 {
514 Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
515
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
612 @Override
613 protected String getClusterId()
614 {
615 return super.getClusterId();
616 }
617
618 protected Map newAttributeMap()
619 {
620
621
622
623 return _sessionData.getAttributeMap();
624 }
625
626 @Override
627 protected void access(long time)
628 {
629
630
631
632
633
634
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
658
659
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
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
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
710
711 _attributes = new HashMap();
712 _creation = System.currentTimeMillis();
713 _expiration = new MutableLong();
714
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
828
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 }