001: /*
002: * All content copyright (c) 2003-2006 Terracotta, Inc., except as may otherwise be noted in a separate copyright
003: * notice. All rights reserved.
004: */
005: package com.terracotta.session;
006:
007: import com.tc.logging.TCLogger;
008: import com.tc.management.beans.sessions.SessionMonitorMBean;
009: import com.tc.management.beans.sessions.SessionMonitorMBean.SessionsComptroller;
010: import com.tc.object.bytecode.ManagerUtil;
011: import com.tc.object.bytecode.hook.impl.ClassProcessorHelper;
012: import com.terracotta.session.util.Assert;
013: import com.terracotta.session.util.ConfigProperties;
014: import com.terracotta.session.util.ContextMgr;
015: import com.terracotta.session.util.DefaultContextMgr;
016: import com.terracotta.session.util.LifecycleEventMgr;
017: import com.terracotta.session.util.Lock;
018: import com.terracotta.session.util.SessionCookieWriter;
019: import com.terracotta.session.util.SessionIdGenerator;
020: import com.terracotta.session.util.Timestamp;
021:
022: import java.util.Collections;
023: import java.util.Set;
024: import java.util.TreeSet;
025:
026: import javax.servlet.http.Cookie;
027: import javax.servlet.http.HttpServletRequest;
028: import javax.servlet.http.HttpServletResponse;
029:
030: public class TerracottaSessionManager implements SessionManager {
031:
032: private final SessionMonitorMBean mBean;
033: private final SessionIdGenerator idGenerator;
034: private final SessionCookieWriter cookieWriter;
035: private final SessionDataStore store;
036: private final LifecycleEventMgr eventMgr;
037: private final ContextMgr contextMgr;
038: private final boolean reqeustLogEnabled;
039: private final boolean invalidatorLogEnabled;
040: private final TCLogger logger;
041: private final RequestResponseFactory factory;
042: private final RequestTracker tracker;
043: private final boolean debugServerHops;
044: private final int debugServerHopsInterval;
045: private final boolean debugInvalidate;
046: private final boolean debugSessions;
047: private int serverHopsDetected = 0;
048:
049: private static final Set excludedVHosts = loadExcludedVHosts();
050:
051: public TerracottaSessionManager(SessionIdGenerator sig,
052: SessionCookieWriter scw, LifecycleEventMgr eventMgr,
053: ContextMgr contextMgr, RequestResponseFactory factory,
054: ConfigProperties cp) {
055:
056: Assert.pre(sig != null);
057: Assert.pre(scw != null);
058: Assert.pre(eventMgr != null);
059: Assert.pre(contextMgr != null);
060:
061: this .idGenerator = sig;
062: this .cookieWriter = scw;
063: this .eventMgr = eventMgr;
064: this .contextMgr = contextMgr;
065: this .factory = factory;
066: this .store = new SessionDataStore(contextMgr.getAppName(), cp
067: .getSessionTimeoutSeconds(), eventMgr, contextMgr, this );
068: this .logger = ManagerUtil.getLogger("com.tc.tcsession."
069: + contextMgr.getAppName());
070: this .reqeustLogEnabled = cp.getRequestLogBenchEnabled();
071: this .invalidatorLogEnabled = cp.getInvalidatorLogBenchEnabled();
072:
073: // XXX: If reasonable, we should move this out of the constructor -- leaking a reference to "this" to another thread
074: // within a constructor is a bad practice (note: although "this" isn't explicitly based as arg, it is available and
075: // accessed by the non-static inner class)
076: Thread invalidator = new Thread(new SessionInvalidator(cp
077: .getInvalidatorSleepSeconds()), "SessionInvalidator - "
078: + contextMgr.getAppName());
079: invalidator.setDaemon(true);
080: invalidator.start();
081: Assert.post(invalidator.isAlive());
082:
083: // This is disgusting, but right now we have to do this because we don't have an event
084: // management infrastructure to boot stuff up
085: mBean = ManagerUtil.getSessionMonitorMBean();
086:
087: mBean.registerSessionsController(new SessionsComptroller() {
088: public boolean killSession(final String browserSessionId) {
089: SessionId id = idGenerator
090: .makeInstanceFromBrowserId(browserSessionId);
091: if (id == null) {
092: // that, potentially, was not *browser* id, try to recover...
093: id = idGenerator
094: .makeInstanceFromInternalKey(browserSessionId);
095: }
096: expire(id);
097: return true;
098: }
099: });
100:
101: if (cp.isRequestTrackingEnabled()) {
102: tracker = new StuckRequestTracker(cp
103: .getRequestTrackerSleepMillis(), cp
104: .getRequestTrackerStuckThresholdMillis(), cp
105: .isDumpThreadsOnStuckRequests());
106: ((StuckRequestTracker) tracker).start();
107: } else {
108: tracker = new NullRequestTracker();
109: }
110:
111: this .debugServerHops = cp.isDebugServerHops();
112: this .debugServerHopsInterval = cp.getDebugServerHopsInterval();
113: this .debugInvalidate = cp.isDebugSessionInvalidate();
114: this .debugSessions = cp.isDebugSessions();
115: }
116:
117: private static Set loadExcludedVHosts() {
118: String list = ManagerUtil.getTCProperties().getProperty(
119: "session.vhosts.excluded", true);
120: list = (list == null) ? "" : list.replaceAll("\\s", "");
121:
122: Set set = new TreeSet();
123: String[] vhosts = list.split(",");
124: for (int i = 0; i < vhosts.length; i++) {
125: String vhost = vhosts[i];
126: if (vhost != null && vhost.length() > 0) {
127: set.add(vhost);
128: }
129: }
130:
131: if (set.size() > 0) {
132: ManagerUtil.getLogger("com.tc.TerracottaSessionManager")
133: .warn("Excluded vhosts for sessions: " + set);
134: }
135:
136: return Collections.unmodifiableSet(set);
137: }
138:
139: public TerracottaRequest preprocess(HttpServletRequest req,
140: HttpServletResponse res) {
141: tracker.begin(req);
142: TerracottaRequest terracottaRequest = basicPreprocess(req, res);
143: tracker.recordSessionId(terracottaRequest);
144: return terracottaRequest;
145: }
146:
147: private TerracottaRequest basicPreprocess(HttpServletRequest req,
148: HttpServletResponse res) {
149: Assert.pre(req != null);
150: Assert.pre(res != null);
151:
152: SessionId sessionId = findSessionId(req);
153:
154: if (debugServerHops) {
155: if (sessionId != null && sessionId.isServerHop()) {
156: synchronized (this ) {
157: serverHopsDetected++;
158: if ((serverHopsDetected % debugServerHopsInterval) == 0) {
159: logger.info(serverHopsDetected
160: + " server hops detected");
161: }
162: }
163: }
164: }
165:
166: TerracottaRequest rw = wrapRequest(sessionId, req, res);
167:
168: Assert.post(rw != null);
169: return rw;
170: }
171:
172: public TerracottaResponse createResponse(TerracottaRequest req,
173: HttpServletResponse res) {
174: return factory.createResponse(req, res);
175: }
176:
177: public void postprocess(TerracottaRequest req) {
178: try {
179: basicPostprocess(req);
180: } finally {
181: tracker.end();
182: }
183: }
184:
185: private void basicPostprocess(TerracottaRequest req) {
186: Assert.pre(req != null);
187:
188: // don't do anything for forwarded requests
189: if (req.isForwarded())
190: return;
191:
192: Assert.inv(!req.isForwarded());
193:
194: mBean.requestProcessed();
195:
196: try {
197: if (req.isSessionOwner())
198: postprocessSession(req);
199: } finally {
200: if (reqeustLogEnabled) {
201: logRequestBench(req);
202: }
203: }
204: }
205:
206: private void logRequestBench(TerracottaRequest req) {
207: final String msgPrefix = "REQUEST BENCH: url=["
208: + req.getRequestURL() + "]";
209: String sessionInfo = "";
210: if (req.isSessionOwner()) {
211: final SessionId id = req.getTerracottaSession(false)
212: .getSessionId();
213: sessionInfo = " sid=[" + id.getKey() + "]";
214: }
215: final String msg = msgPrefix
216: + sessionInfo
217: + " -> "
218: + (System.currentTimeMillis() - req
219: .getRequestStartMillis());
220: logger.info(msg);
221: }
222:
223: private void postprocessSession(TerracottaRequest req) {
224: Assert.pre(req != null);
225: Assert.pre(!req.isForwarded());
226: Assert.pre(req.isSessionOwner());
227: final Session session = req.getTerracottaSession(false);
228: session.clearRequest();
229: Assert.inv(session != null);
230: final SessionId id = session.getSessionId();
231: final SessionData sd = session.getSessionData();
232: try {
233: if (!session.isValid())
234: store.remove(id);
235: else {
236: sd.finishRequest();
237: store.updateTimestampIfNeeded(sd);
238: }
239: } finally {
240: id.commitLock();
241: }
242: }
243:
244: /**
245: * The only use for this method [currently] is by Struts' Include Tag, which can generate a nested request. In this
246: * case we have to release session lock, so that nested request (running, potentially, in another JVM) can acquire it.
247: * {@link TerracottaSessionManager#resumeRequest(Session)} method will re-aquire the lock.
248: */
249: public static void pauseRequest(final Session sess) {
250: Assert.pre(sess != null);
251: final SessionId id = sess.getSessionId();
252: final SessionData sd = sess.getSessionData();
253: sd.finishRequest();
254: id.commitLock();
255: }
256:
257: /**
258: * See {@link TerracottaSessionManager#resumeRequest(Session)} for details
259: */
260: public static void resumeRequest(final Session sess) {
261: Assert.pre(sess != null);
262: final SessionId id = sess.getSessionId();
263: final SessionData sd = sess.getSessionData();
264: id.getWriteLock();
265: sd.startRequest();
266: }
267:
268: private TerracottaRequest wrapRequest(SessionId sessionId,
269: HttpServletRequest req, HttpServletResponse res) {
270: return factory.createRequest(sessionId, req, res, this );
271: }
272:
273: /**
274: * This method always returns a valid session. If data for the requestedSessionId found and is valid, it is returned.
275: * Otherwise, we must create a new session id, a new session data, a new session, and cookie the response.
276: */
277: public Session getSession(final SessionId requestedSessionId,
278: final HttpServletRequest req, final HttpServletResponse res) {
279: Assert.pre(req != null);
280: Assert.pre(res != null);
281: Session rv = doGetSession(requestedSessionId, req, res);
282: Assert.post(rv != null);
283: return rv;
284: }
285:
286: public Session getSessionIfExists(SessionId requestedSessionId,
287: HttpServletRequest req, HttpServletResponse res) {
288: if (debugSessions) {
289: logger.info("getSessionIfExists called for "
290: + requestedSessionId);
291: }
292:
293: if (requestedSessionId == null)
294: return null;
295: SessionData sd = store.find(requestedSessionId);
296: if (sd == null) {
297: if (debugSessions) {
298: logger.info("No session found in store for "
299: + requestedSessionId);
300: }
301: return null;
302: }
303:
304: Assert.inv(sd.isValid());
305: writeCookieIfHop(req, res, requestedSessionId);
306:
307: return sd;
308: }
309:
310: private void writeCookieIfHop(HttpServletRequest req,
311: HttpServletResponse res, SessionId id) {
312: if (id.isServerHop()) {
313: Cookie cookie = cookieWriter.writeCookie(req, res, id);
314: if (debugSessions) {
315: logger.info("writing new cookie for hopped request: "
316: + getCookieDetails(cookie));
317: }
318: }
319: }
320:
321: private String getCookieDetails(Cookie c) {
322: if (c == null) {
323: return "<null cookie>";
324: }
325:
326: StringBuffer buf = new StringBuffer("Cookie(");
327: buf.append(c.getName()).append("=").append(c.getValue());
328: buf.append(", path=").append(c.getPath()).append(", maxAge=")
329: .append(c.getMaxAge()).append(", domain=").append(
330: c.getDomain());
331: buf.append(", secure=").append(c.getSecure()).append(
332: ", comment=").append(c.getComment()).append(
333: ", version=").append(c.getVersion());
334:
335: buf.append(")");
336: return buf.toString();
337: }
338:
339: public SessionCookieWriter getCookieWriter() {
340: return this .cookieWriter;
341: }
342:
343: private void expire(SessionId id) {
344: SessionData sd = null;
345: try {
346: sd = store.find(id);
347: if (sd != null) {
348: expire(id, sd);
349: }
350: } finally {
351: if (sd != null)
352: id.commitLock();
353: }
354: }
355:
356: public void remove(Session data, boolean unlock) {
357: if (debugInvalidate) {
358: logger.info("Session id: " + data.getSessionId().getKey()
359: + " being removed, unlock: " + unlock);
360: }
361:
362: store.remove(data.getSessionId());
363: mBean.sessionDestroyed();
364:
365: if (unlock) {
366: data.getSessionId().commitLock();
367: }
368: }
369:
370: private void expire(SessionId id, SessionData sd) {
371: try {
372: sd.invalidateIfNecessary();
373: } catch (Throwable t) {
374: logger.error(
375: "unhandled exception during invalidate() for session "
376: + id.getKey(), t);
377: }
378: }
379:
380: private Session doGetSession(final SessionId requestedSessionId,
381: final HttpServletRequest req, final HttpServletResponse res) {
382: Assert.pre(req != null);
383: Assert.pre(res != null);
384:
385: if (requestedSessionId == null) {
386: if (debugSessions) {
387: logger
388: .info("creating new session since requested id is null");
389: }
390: return createNewSession(req, res);
391: }
392: final SessionData sd = store.find(requestedSessionId);
393:
394: if (sd == null) {
395: if (debugSessions) {
396: logger
397: .info("creating new session since requested id is not in store: "
398: + requestedSessionId);
399: }
400: return createNewSession(req, res);
401: }
402:
403: if (debugSessions) {
404: logger.info("requested id found in store: "
405: + requestedSessionId);
406: }
407:
408: Assert.inv(sd.isValid());
409:
410: writeCookieIfHop(req, res, requestedSessionId);
411:
412: return sd;
413: }
414:
415: private Session createNewSession(HttpServletRequest req,
416: HttpServletResponse res) {
417: Assert.pre(req != null);
418: Assert.pre(res != null);
419:
420: SessionId id = idGenerator.generateNewId();
421: SessionData sd = store.createSessionData(id);
422: Cookie cookie = cookieWriter.writeCookie(req, res, id);
423: eventMgr.fireSessionCreatedEvent(sd);
424: mBean.sessionCreated();
425: Assert.post(sd != null);
426:
427: if (debugSessions) {
428: logger.info("new session created: " + id + " with cookie "
429: + getCookieDetails(cookie));
430: }
431:
432: return sd;
433: }
434:
435: private SessionId findSessionId(HttpServletRequest httpRequest) {
436: Assert.pre(httpRequest != null);
437:
438: String requestedSessionId = httpRequest.getRequestedSessionId();
439:
440: if (debugSessions) {
441: logger.info("requested session ID from http request: "
442: + requestedSessionId);
443: }
444:
445: if (requestedSessionId == null) {
446: return null;
447: }
448:
449: SessionId rv = idGenerator
450: .makeInstanceFromBrowserId(requestedSessionId);
451: if (debugSessions) {
452: logger.info("session ID generator returned " + rv);
453: }
454:
455: return rv;
456: }
457:
458: private class SessionInvalidator implements Runnable {
459:
460: private final long sleepMillis;
461:
462: public SessionInvalidator(final long sleepSeconds) {
463: this .sleepMillis = sleepSeconds * 1000L;
464: }
465:
466: public void run() {
467: final String invalidatorLock = "tc:session_invalidator_lock_"
468: + contextMgr.getAppName();
469:
470: while (true) {
471: sleep(sleepMillis);
472: if (Thread.interrupted()) {
473: break;
474: } else {
475: try {
476: final Lock lock = new Lock(invalidatorLock);
477: lock.tryWriteLock();
478: if (!lock.isLocked())
479: continue;
480: try {
481: invalidateSessions();
482: } finally {
483: lock.commitLock();
484: }
485: } catch (Throwable t) {
486: logger
487: .error(
488: "Unhandled exception occurred during session invalidation",
489: t);
490: }
491: }
492: }
493: }
494:
495: private void invalidateSessions() {
496: final long startMillis = System.currentTimeMillis();
497: final String keys[] = store.getAllKeys();
498: int totalCnt = 0;
499: int invalCnt = 0;
500: int evaled = 0;
501: int notEvaled = 0;
502: int errors = 0;
503:
504: if (invalidatorLogEnabled) {
505: logger.info("SESSION INVALIDATOR: started");
506: }
507:
508: for (int i = 0, n = keys.length; i < n; i++) {
509: final String key = keys[i];
510: try {
511: final SessionId id = idGenerator
512: .makeInstanceFromInternalKey(key);
513: final Timestamp dtm = store
514: .findTimestampUnlocked(id);
515: if (dtm == null)
516: continue;
517: totalCnt++;
518: if (dtm.getMillis() < System.currentTimeMillis()) {
519: evaled++;
520: if (evaluateSession(dtm, id))
521: invalCnt++;
522: } else {
523: notEvaled++;
524: }
525: } catch (Throwable t) {
526: errors++;
527: logger.error(
528: "Unhandled exception inspecting session "
529: + key + " for invalidation", t);
530: }
531: }
532: if (invalidatorLogEnabled) {
533: final String msg = "SESSION INVALIDATOR BENCH: "
534: + " -> total=" + totalCnt + ", evaled="
535: + evaled + ", notEvaled=" + notEvaled
536: + ", errors=" + errors + ", invalidated="
537: + invalCnt + " -> elapsed="
538: + (System.currentTimeMillis() - startMillis);
539: logger.info(msg);
540: }
541: }
542:
543: private boolean evaluateSession(final Timestamp dtm,
544: final SessionId id) {
545: Assert.pre(id != null);
546:
547: boolean rv = false;
548:
549: if (!id.tryWriteLock()) {
550: return rv;
551: }
552:
553: try {
554: final SessionData sd = store
555: .findSessionDataUnlocked(id);
556: if (sd == null)
557: return rv;
558: if (!sd.isValid()) {
559: expire(id, sd);
560: rv = true;
561: } else {
562: store.updateTimestampIfNeeded(sd);
563: }
564: } finally {
565: id.commitLock();
566: }
567: return rv;
568: }
569:
570: private void sleep(long l) {
571: try {
572: Thread.sleep(l);
573: } catch (InterruptedException ignore) {
574: // nothing to do
575: }
576: }
577: }
578:
579: // XXX: move this method?
580: public static boolean isDsoSessionApp(HttpServletRequest request) {
581: Assert.pre(request != null);
582:
583: if (excludedVHosts.contains(request.getServerName())) {
584: return false;
585: }
586:
587: String hostHeader = request.getHeader("Host");
588: if (hostHeader != null && excludedVHosts.contains(hostHeader)) {
589: return false;
590: }
591:
592: final String appName = DefaultContextMgr
593: .computeAppName(request);
594: return ClassProcessorHelper.isDSOSessions(appName);
595: }
596:
597: private static final class NullRequestTracker implements
598: RequestTracker {
599:
600: public final boolean end() {
601: return true;
602: }
603:
604: public final void begin(HttpServletRequest req) {
605: //
606: }
607:
608: public final void recordSessionId(
609: TerracottaRequest terracottaRequest) {
610: //
611: }
612:
613: }
614:
615: }
|