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
335
336 synchronized (_sessions)
337 {
338 result = _sessions.get(clusterId);
339 if (result == null)
340 {
341 Log.debug("Session with id {} --> local cache miss", clusterId);
342
343
344
345
346
347
348
349
350
351
352 Log.debug("Distributed session data with id {} --> lookup", clusterId);
353 SessionData sessionData = _sessionDatas.get(clusterId);
354 if (sessionData == null)
355 {
356 Log.debug("Distributed session data with id {} --> not found", clusterId);
357 }
358 else
359 {
360 Log.debug("Distributed session data with id {} --> found", clusterId);
361
362 result = new Session(sessionData);
363 _sessions.put(clusterId, result);
364 }
365 }
366 else
367 {
368 Log.debug("Session with id {} --> local cache hit", clusterId);
369 if (!_sessionExpirations.containsKey(clusterId))
370 {
371
372
373 _sessions.remove(clusterId);
374 result = null;
375 Log.debug("Session with id {} --> local cache stale");
376 }
377 }
378 }
379 }
380 finally
381 {
382
383
384
385 exit(clusterId);
386 }
387 return result;
388 }
389
390 protected String newLockId(String clusterId)
391 {
392 StringBuilder builder = new StringBuilder(clusterId);
393 builder.append(":").append(_contextPath);
394 builder.append(":").append(_virtualHost);
395 return builder.toString();
396 }
397
398
399 public Map getSessionMap()
400 {
401 return Collections.unmodifiableMap(_sessions);
402 }
403
404
405
406 public int getSessions()
407 {
408 return _sessions.size();
409 }
410
411 protected Session newSession(HttpServletRequest request)
412 {
413
414
415
416
417
418
419 Session result = new Session(request);
420
421 String requestedSessionId = request.getRequestedSessionId();
422 if (requestedSessionId == null)
423 {
424
425 enter(result.getClusterId());
426 }
427 else
428 {
429 if (result.getClusterId().equals(getIdManager().getClusterId(requestedSessionId)))
430 {
431
432
433
434
435 }
436 else
437 {
438
439
440 enter(result.getClusterId());
441 }
442 }
443 return result;
444 }
445
446 protected void invalidateSessions()
447 {
448
449
450
451
452
453
454 }
455
456 private void scavenge()
457 {
458 Thread thread = Thread.currentThread();
459 ClassLoader old_loader = thread.getContextClassLoader();
460 if (_loader != null) thread.setContextClassLoader(_loader);
461 try
462 {
463 long now = System.currentTimeMillis();
464 Log.debug(this + " scavenging at {}, scavenge period {}", now, getScavengePeriodMs());
465
466
467 Set<String> candidates = new HashSet<String>();
468 String lockId = "scavenge:" + _contextPath + ":" + _virtualHost;
469 Lock.lock(lockId);
470 try
471 {
472
473
474
475
476
477
478 synchronized (_sessions)
479 {
480 synchronized (_sessionExpirations)
481 {
482 for (Map.Entry<String, MutableLong> entry : _sessionExpirations.entrySet())
483 {
484 String sessionId = entry.getKey();
485 long expirationTime = entry.getValue().value;
486 Log.debug("Estimated expiration time {} for session {}", expirationTime, sessionId);
487 if (expirationTime > 0 && expirationTime < now) candidates.add(sessionId);
488 }
489
490 _sessions.keySet().retainAll(_sessionExpirations.keySet());
491 }
492 }
493 }
494 finally
495 {
496 Lock.unlock(lockId);
497 }
498 Log.debug("Scavenging detected {} candidate sessions to expire", candidates.size());
499
500
501
502 for (String sessionId : candidates)
503 {
504 Session candidate = (Session)getSession(sessionId);
505
506 boolean entered = tryEnter(sessionId);
507 if (entered)
508 {
509 try
510 {
511 long maxInactiveTime = candidate.getMaxIdlePeriodMs();
512
513 if (maxInactiveTime > 0)
514 {
515
516 long lastAccessedTime = candidate.getLastAccessedTime();
517
518
519 long expirationTime = lastAccessedTime + maxInactiveTime + getScavengePeriodMs();
520 if (expirationTime < now)
521 {
522 Log.debug("Scavenging expired session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
523
524 candidate.timeout();
525 }
526 else
527 {
528 Log.debug("Scavenging skipping candidate session {}, expirationTime {}", candidate.getClusterId(), expirationTime);
529 }
530 }
531 }
532 finally
533 {
534 exit(sessionId);
535 }
536 }
537 }
538
539 int sessionCount = getSessions();
540 if (sessionCount < _minSessions) _minSessions = sessionCount;
541 if (sessionCount > _maxSessions) _maxSessions = sessionCount;
542 }
543 finally
544 {
545 thread.setContextClassLoader(old_loader);
546 }
547 }
548
549 private String canonicalize(String contextPath)
550 {
551 if (contextPath == null) return "";
552 return contextPath.replace('/', '_').replace('.', '_').replace('\\', '_');
553 }
554
555 private String virtualHostFrom(ContextHandler.SContext context)
556 {
557 String result = "0.0.0.0";
558 if (context == null) return result;
559
560 String[] vhosts = context.getContextHandler().getVirtualHosts();
561 if (vhosts == null || vhosts.length == 0 || vhosts[0] == null) return result;
562
563 return vhosts[0];
564 }
565
566 class Session extends AbstractSessionManager.Session
567 {
568 private static final long serialVersionUID = -2134521374206116367L;
569
570 private final SessionData _sessionData;
571 private long _lastUpdate;
572
573 protected Session(HttpServletRequest request)
574 {
575 super(request);
576 _sessionData = new SessionData(getClusterId(), _maxIdleMs);
577 _lastAccessed = _sessionData.getCreationTime();
578 }
579
580 protected Session(SessionData sd)
581 {
582 super(sd.getCreationTime(), sd.getId());
583 _sessionData = sd;
584 _lastAccessed = getLastAccessedTime();
585 initValues();
586 }
587
588 public SessionData getSessionData()
589 {
590 return _sessionData;
591 }
592
593 @Override
594 public long getCookieSetTime()
595 {
596 return _sessionData.getCookieTime();
597 }
598
599 @Override
600 protected void cookieSet()
601 {
602 _sessionData.setCookieTime(getLastAccessedTime());
603 }
604
605 @Override
606 public long getLastAccessedTime()
607 {
608 if (!isValid()) throw new IllegalStateException();
609 return _sessionData.getPreviousAccessTime();
610 }
611
612 @Override
613 public long getCreationTime() throws IllegalStateException
614 {
615 if (!isValid()) throw new IllegalStateException();
616 return _sessionData.getCreationTime();
617 }
618
619
620 @Override
621 protected String getClusterId()
622 {
623 return super.getClusterId();
624 }
625
626 protected Map newAttributeMap()
627 {
628
629
630
631 return _sessionData.getAttributeMap();
632 }
633
634 @Override
635 protected void access(long time)
636 {
637
638
639
640
641
642
643 long previousAccessTime = getPreviousAccessTime();
644 if (time - previousAccessTime > getScavengePeriodMs())
645 {
646 Log.debug("Out-of-date update of distributed access times: previous {} - current {}", previousAccessTime, time);
647 updateAccessTimes(time);
648 }
649 else
650 {
651 if (time - _lastUpdate > getScavengePeriodMs())
652 {
653 Log.debug("Periodic update of distributed access times: last update {} - current {}", _lastUpdate, time);
654 updateAccessTimes(time);
655 }
656 else
657 {
658 Log.debug("Skipping update of distributed access times: previous {} - current {}", previousAccessTime, time);
659 }
660 }
661 super.access(time);
662 }
663
664
665
666
667
668
669 private void updateAccessTimes(long time)
670 {
671 _sessionData.setPreviousAccessTime(_accessed);
672 if (getMaxIdlePeriodMs() > 0) _sessionData.setExpirationTime(time + getMaxIdlePeriodMs());
673 _lastUpdate = time;
674 }
675
676
677 @Override
678 protected void timeout()
679 {
680 super.timeout();
681 Log.debug("Timed out session {} with id {}", this, getClusterId());
682 }
683
684 @Override
685 public void invalidate()
686 {
687 super.invalidate();
688 Log.debug("Invalidated session {} with id {}", this, getClusterId());
689 }
690
691 private long getMaxIdlePeriodMs()
692 {
693 return _maxIdleMs;
694 }
695
696 private long getPreviousAccessTime()
697 {
698 return super.getLastAccessedTime();
699 }
700 }
701
702
703
704
705 public static class SessionData
706 {
707 private final String _id;
708 private final Map _attributes;
709 private final long _creation;
710 private final MutableLong _expiration;
711 private long _previousAccess;
712 private long _cookieTime;
713
714 public SessionData(String sessionId, long maxIdleMs)
715 {
716 _id = sessionId;
717
718
719 _attributes = new HashMap();
720 _creation = System.currentTimeMillis();
721 _expiration = new MutableLong();
722
723 _expiration.value = maxIdleMs > 0 ? _creation + maxIdleMs : -1L;
724 }
725
726 public String getId()
727 {
728 return _id;
729 }
730
731 protected Map getAttributeMap()
732 {
733 return _attributes;
734 }
735
736 public long getCreationTime()
737 {
738 return _creation;
739 }
740
741 public long getExpirationTime()
742 {
743 return _expiration.value;
744 }
745
746 public void setExpirationTime(long time)
747 {
748 _expiration.value = time;
749 }
750
751 public long getCookieTime()
752 {
753 return _cookieTime;
754 }
755
756 public void setCookieTime(long time)
757 {
758 _cookieTime = time;
759 }
760
761 public long getPreviousAccessTime()
762 {
763 return _previousAccess;
764 }
765
766 public void setPreviousAccessTime(long time)
767 {
768 _previousAccess = time;
769 }
770 }
771
772 protected static class Lock
773 {
774 private static final ThreadLocal<Map<String, Integer>> nestings = new ThreadLocal<Map<String, Integer>>()
775 {
776 @Override
777 protected Map<String, Integer> initialValue()
778 {
779 return new HashMap<String, Integer>();
780 }
781 };
782
783 private Lock()
784 {
785 }
786
787 public static void lock(String lockId)
788 {
789 Integer nestingLevel = nestings.get().get(lockId);
790 if (nestingLevel == null) nestingLevel = 0;
791 if (nestingLevel < 0)
792 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
793 if (nestingLevel == 0)
794 {
795 ManagerUtil.beginLock(lockId, Manager.LOCK_TYPE_WRITE);
796 Log.debug("Lock({}) acquired by thread {}", lockId, Thread.currentThread().getName());
797 }
798 nestings.get().put(lockId, nestingLevel + 1);
799 Log.debug("Lock({}) nestings {}", lockId, getLocks());
800 }
801
802 public static boolean tryLock(String lockId)
803 {
804 boolean result = ManagerUtil.tryBeginLock(lockId, Manager.LOCK_TYPE_WRITE);
805 Log.debug("Lock({}) tried and" + (result ? "" : " not") + " acquired by thread {}", lockId, Thread.currentThread().getName());
806 if (result)
807 {
808 Integer nestingLevel = nestings.get().get(lockId);
809 if (nestingLevel == null) nestingLevel = 0;
810 nestings.get().put(lockId, nestingLevel + 1);
811 Log.debug("Lock({}) nestings {}", lockId, getLocks());
812 }
813 return result;
814 }
815
816 public static void unlock(String lockId)
817 {
818 Integer nestingLevel = nestings.get().get(lockId);
819 if (nestingLevel == null) return;
820 if (nestingLevel < 1)
821 throw new AssertionError("Lock(" + lockId + ") nest level = " + nestingLevel + ", thread " + Thread.currentThread() + ": " + getLocks());
822 if (nestingLevel == 1)
823 {
824 ManagerUtil.commitLock(lockId);
825 Log.debug("Lock({}) released by thread {}", lockId, Thread.currentThread().getName());
826 nestings.get().remove(lockId);
827 }
828 else
829 {
830 nestings.get().put(lockId, nestingLevel - 1);
831 }
832 Log.debug("Lock({}) nestings {}", lockId, getLocks());
833 }
834
835
836
837
838
839 protected static Map<String, Integer> getLocks()
840 {
841 return Collections.unmodifiableMap(nestings.get());
842 }
843 }
844
845 private static class MutableLong
846 {
847 private long value;
848 }
849 }