001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.catalina.ha.session;
018:
019: import java.io.IOException;
020:
021: import javax.servlet.ServletException;
022: import javax.servlet.http.Cookie;
023:
024: import org.apache.catalina.Container;
025: import org.apache.catalina.Context;
026: import org.apache.catalina.Engine;
027: import org.apache.catalina.Globals;
028: import org.apache.catalina.Host;
029: import org.apache.catalina.Lifecycle;
030: import org.apache.catalina.LifecycleException;
031: import org.apache.catalina.LifecycleListener;
032: import org.apache.catalina.Manager;
033: import org.apache.catalina.Session;
034: import org.apache.catalina.ha.CatalinaCluster;
035: import org.apache.catalina.ha.ClusterManager;
036: import org.apache.catalina.ha.ClusterMessage;
037: import org.apache.catalina.ha.ClusterValve;
038: import org.apache.catalina.connector.Request;
039: import org.apache.catalina.connector.Response;
040: import org.apache.catalina.session.ManagerBase;
041: import org.apache.catalina.util.LifecycleSupport;
042: import org.apache.catalina.util.StringManager;
043: import org.apache.catalina.valves.ValveBase;
044:
045: /**
046: * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node
047: * failure. After a node crashed the next request going to other cluster node.
048: * Now the answering from apache is slower ( make some error handshaking. Very
049: * bad with apache at my windows.). We rewrite now the cookie jsessionid
050: * information to the backup cluster node. After the next response all client
051: * request goes direct to the backup node. The change sessionid send also to all
052: * other cluster nodes. Well, now the session stickyness work directly to the
053: * backup node and traffic don't go back too restarted cluster nodes!
054: *
055: * At all cluster node you must configure the as ClusterListener since 5.5.10
056: * {@link org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener JvmRouteSessionIDBinderListener}
057: * or before with
058: * org.apache.catalina.ha.session.JvmRouteSessionIDBinderListenerLifecycle.
059: *
060: * Add this Valve to your host definition at conf/server.xml .
061: *
062: * Since 5.5.10 as direct cluster valve:<br/>
063: * <pre>
064: * <Cluster>
065: * <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
066: * </Cluster>
067: * </pre>
068: * <br />
069: * Before 5.5.10 as Host element:<br/>
070: * <pre>
071: * <Hostr>
072: * <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
073: * </Hostr>
074: * </pre>
075: *
076: * Trick:<br/>
077: * You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes!
078: * Set enable true on all JvmRouteBinderValve backups, disable worker at mod_jk
079: * and then drop node and restart it! Then enable mod_jk Worker and disable JvmRouteBinderValves again.
080: * This use case means that only requested session are migrated.
081: *
082: * @author Peter Rossbach
083: * @version $Revision: 467222 $ $Date: 2006-10-24 05:17:11 +0200 (mar., 24 oct. 2006) $
084: */
085: public class JvmRouteBinderValve extends ValveBase implements
086: ClusterValve, Lifecycle {
087:
088: /*--Static Variables----------------------------------------*/
089: public static org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory
090: .getLog(JvmRouteBinderValve.class);
091:
092: /**
093: * The descriptive information about this implementation.
094: */
095: protected static final String info = "org.apache.catalina.ha.session.JvmRouteBinderValve/1.2";
096:
097: /*--Instance Variables--------------------------------------*/
098:
099: /**
100: * the cluster
101: */
102: protected CatalinaCluster cluster;
103:
104: /**
105: * The string manager for this package.
106: */
107: protected StringManager sm = StringManager
108: .getManager(Constants.Package);
109:
110: /**
111: * Has this component been started yet?
112: */
113: protected boolean started = false;
114:
115: /**
116: * enabled this component
117: */
118: protected boolean enabled = true;
119:
120: /**
121: * number of session that no at this tomcat instanz hosted
122: */
123: protected long numberOfSessions = 0;
124:
125: protected String sessionIdAttribute = "org.apache.catalina.ha.session.JvmRouteOrignalSessionID";
126:
127: /**
128: * The lifecycle event support for this component.
129: */
130: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
131:
132: /*--Logic---------------------------------------------------*/
133:
134: /**
135: * Return descriptive information about this implementation.
136: */
137: public String getInfo() {
138:
139: return (info);
140:
141: }
142:
143: /**
144: * set session id attribute to failed node for request.
145: *
146: * @return Returns the sessionIdAttribute.
147: */
148: public String getSessionIdAttribute() {
149: return sessionIdAttribute;
150: }
151:
152: /**
153: * get name of failed reqeust session attribute
154: *
155: * @param sessionIdAttribute
156: * The sessionIdAttribute to set.
157: */
158: public void setSessionIdAttribute(String sessionIdAttribute) {
159: this .sessionIdAttribute = sessionIdAttribute;
160: }
161:
162: /**
163: * @return Returns the number of migrated sessions.
164: */
165: public long getNumberOfSessions() {
166: return numberOfSessions;
167: }
168:
169: /**
170: * @return Returns the enabled.
171: */
172: public boolean getEnabled() {
173: return enabled;
174: }
175:
176: /**
177: * @param enabled
178: * The enabled to set.
179: */
180: public void setEnabled(boolean enabled) {
181: this .enabled = enabled;
182: }
183:
184: /**
185: * Detect possible the JVMRoute change at cluster backup node..
186: *
187: * @param request
188: * tomcat request being processed
189: * @param response
190: * tomcat response being processed
191: * @exception IOException
192: * if an input/output error has occurred
193: * @exception ServletException
194: * if a servlet error has occurred
195: */
196: public void invoke(Request request, Response response)
197: throws IOException, ServletException {
198:
199: if (getEnabled() && getCluster() != null
200: && request.getContext() != null
201: && request.getContext().getDistributable()) {
202: // valve cluster can access manager - other cluster handle turnover
203: // at host level - hopefully!
204: Manager manager = request.getContext().getManager();
205: if (manager != null
206: && manager instanceof ClusterManager
207: && getCluster().getManager(
208: ((ClusterManager) manager).getName()) != null)
209: handlePossibleTurnover(request, response);
210: }
211: // Pass this request on to the next valve in our pipeline
212: getNext().invoke(request, response);
213: }
214:
215: /**
216: * handle possible session turn over.
217: *
218: * @see JvmRouteBinderValve#handleJvmRoute(Request, Response, String, String)
219: * @param request current request
220: * @param response current response
221: */
222: protected void handlePossibleTurnover(Request request,
223: Response response) {
224: Session session = request.getSessionInternal(false);
225: if (session != null) {
226: long t1 = System.currentTimeMillis();
227: String jvmRoute = getLocalJvmRoute(request);
228: if (jvmRoute == null) {
229: if (log.isDebugEnabled())
230: log
231: .debug(sm
232: .getString("jvmRoute.missingJvmRouteAttribute"));
233: return;
234: }
235: handleJvmRoute(request, response, session.getIdInternal(),
236: jvmRoute);
237: if (log.isDebugEnabled()) {
238: long t2 = System.currentTimeMillis();
239: long time = t2 - t1;
240: log.debug(sm.getString("jvmRoute.turnoverInfo",
241: new Long(time)));
242: }
243: }
244: }
245:
246: /**
247: * get jvmroute from engine
248: *
249: * @param request current request
250: * @return return jvmRoute from ManagerBase or null
251: */
252: protected String getLocalJvmRoute(Request request) {
253: Manager manager = getManager(request);
254: if (manager instanceof ManagerBase)
255: return ((ManagerBase) manager).getJvmRoute();
256: return null;
257: }
258:
259: /**
260: * get Cluster DeltaManager
261: *
262: * @param request current request
263: * @return manager or null
264: */
265: protected Manager getManager(Request request) {
266: Manager manager = request.getContext().getManager();
267: if (log.isDebugEnabled()) {
268: if (manager != null)
269: log.debug(sm.getString("jvmRoute.foundManager",
270: manager, request.getContext().getName()));
271: else
272: log.debug(sm.getString("jvmRoute.notFoundManager",
273: manager, request.getContext().getName()));
274: }
275: return manager;
276: }
277:
278: /**
279: * @return Returns the cluster.
280: */
281: public CatalinaCluster getCluster() {
282: return cluster;
283: }
284:
285: /**
286: * @param cluster The cluster to set.
287: */
288: public void setCluster(CatalinaCluster cluster) {
289: this .cluster = cluster;
290: }
291:
292: /**
293: * Handle jvmRoute stickyness after tomcat instance failed. After this
294: * correction a new Cookie send to client with new jvmRoute and the
295: * SessionID change propage to the other cluster nodes.
296: *
297: * @param request current request
298: * @param response
299: * Tomcat Response
300: * @param sessionId
301: * request SessionID from Cookie
302: * @param localJvmRoute
303: * local jvmRoute
304: */
305: protected void handleJvmRoute(Request request, Response response,
306: String sessionId, String localJvmRoute) {
307: // get requested jvmRoute.
308: String requestJvmRoute = null;
309: int index = sessionId.indexOf(".");
310: if (index > 0) {
311: requestJvmRoute = sessionId.substring(index + 1, sessionId
312: .length());
313: }
314: if (requestJvmRoute != null
315: && !requestJvmRoute.equals(localJvmRoute)) {
316: if (log.isDebugEnabled()) {
317: log.debug(sm.getString("jvmRoute.failover",
318: requestJvmRoute, localJvmRoute, sessionId));
319: }
320: // OK - turnover the session ?
321: String newSessionID = sessionId.substring(0, index) + "."
322: + localJvmRoute;
323: Session catalinaSession = null;
324: try {
325: catalinaSession = getManager(request).findSession(
326: sessionId);
327: } catch (IOException e) {
328: // Hups!
329: }
330: if (catalinaSession != null) {
331: changeSessionID(request, response, sessionId,
332: newSessionID, catalinaSession);
333: numberOfSessions++;
334: } else {
335: if (log.isDebugEnabled()) {
336: log.debug(sm.getString(
337: "jvmRoute.cannotFindSession", sessionId));
338: }
339: }
340: }
341: }
342:
343: /**
344: * change session id and send to all cluster nodes
345: *
346: * @param request current request
347: * @param response current response
348: * @param sessionId
349: * original session id
350: * @param newSessionID
351: * new session id for node migration
352: * @param catalinaSession
353: * current session with original session id
354: */
355: protected void changeSessionID(Request request, Response response,
356: String sessionId, String newSessionID,
357: Session catalinaSession) {
358: lifecycle.fireLifecycleEvent("Before session migration",
359: catalinaSession);
360: request.setRequestedSessionId(newSessionID);
361: catalinaSession.setId(newSessionID);
362: if (catalinaSession instanceof DeltaSession)
363: ((DeltaSession) catalinaSession).resetDeltaRequest();
364: if (request.isRequestedSessionIdFromCookie())
365: setNewSessionCookie(request, response, newSessionID);
366: // set orginal sessionid at request, to allow application detect the
367: // change
368: if (sessionIdAttribute != null
369: && !"".equals(sessionIdAttribute)) {
370: if (log.isDebugEnabled()) {
371: log.debug(sm.getString("jvmRoute.set.orignalsessionid",
372: sessionIdAttribute, sessionId));
373: }
374: request.setAttribute(sessionIdAttribute, sessionId);
375: }
376: // now sending the change to all other clusternode!
377: ClusterManager manager = (ClusterManager) catalinaSession
378: .getManager();
379: sendSessionIDClusterBackup(manager, request, sessionId,
380: newSessionID);
381: lifecycle.fireLifecycleEvent("After session migration",
382: catalinaSession);
383: if (log.isDebugEnabled()) {
384: log.debug(sm.getString("jvmRoute.changeSession", sessionId,
385: newSessionID));
386: }
387: }
388:
389: /**
390: * Send the changed Sessionid to all clusternodes.
391: *
392: * @see JvmRouteSessionIDBinderListener#messageReceived(ClusterMessage)
393: * @param manager
394: * ClusterManager
395: * @param sessionId
396: * current failed sessionid
397: * @param newSessionID
398: * new session id, bind to the new cluster node
399: */
400: protected void sendSessionIDClusterBackup(ClusterManager manager,
401: Request request, String sessionId, String newSessionID) {
402: SessionIDMessage msg = new SessionIDMessage();
403: msg.setOrignalSessionID(sessionId);
404: msg.setBackupSessionID(newSessionID);
405: Context context = request.getContext();
406: msg.setContextPath(context.getPath());
407: msg.setHost(context.getParent().getName());
408: if (manager.doDomainReplication())
409: cluster.sendClusterDomain(msg);
410: else
411: cluster.send(msg);
412: }
413:
414: /**
415: * Sets a new cookie for the given session id and response and see
416: * {@link org.apache.catalina.connector.Request#configureSessionCookie(javax.servlet.http.Cookie)}
417: *
418: * @param request current request
419: * @param response Tomcat Response
420: * @param sessionId The session id
421: */
422: protected void setNewSessionCookie(Request request,
423: Response response, String sessionId) {
424: if (response != null) {
425: Context context = request.getContext();
426: if (context.getCookies()) {
427: // set a new session cookie
428: Cookie newCookie = new Cookie(
429: Globals.SESSION_COOKIE_NAME, sessionId);
430: newCookie.setMaxAge(-1);
431: String contextPath = null;
432: if (!response.getConnector().getEmptySessionPath()
433: && (context != null)) {
434: contextPath = context.getEncodedPath();
435: }
436: if ((contextPath != null) && (contextPath.length() > 0)) {
437: newCookie.setPath(contextPath);
438: } else {
439: newCookie.setPath("/");
440: }
441: if (request.isSecure()) {
442: newCookie.setSecure(true);
443: }
444: if (log.isDebugEnabled()) {
445: log.debug(sm.getString("jvmRoute.newSessionCookie",
446: sessionId, Globals.SESSION_COOKIE_NAME,
447: newCookie.getPath(), new Boolean(newCookie
448: .getSecure())));
449: }
450: response.addCookie(newCookie);
451: }
452: }
453: }
454:
455: // ------------------------------------------------------ Lifecycle Methods
456:
457: /**
458: * Add a lifecycle event listener to this component.
459: *
460: * @param listener
461: * The listener to add
462: */
463: public void addLifecycleListener(LifecycleListener listener) {
464:
465: lifecycle.addLifecycleListener(listener);
466:
467: }
468:
469: /**
470: * Get the lifecycle listeners associated with this lifecycle. If this
471: * Lifecycle has no listeners registered, a zero-length array is returned.
472: */
473: public LifecycleListener[] findLifecycleListeners() {
474:
475: return lifecycle.findLifecycleListeners();
476:
477: }
478:
479: /**
480: * Remove a lifecycle event listener from this component.
481: *
482: * @param listener
483: * The listener to add
484: */
485: public void removeLifecycleListener(LifecycleListener listener) {
486:
487: lifecycle.removeLifecycleListener(listener);
488:
489: }
490:
491: /**
492: * Prepare for the beginning of active use of the public methods of this
493: * component. This method should be called after <code>configure()</code>,
494: * and before any of the public methods of the component are utilized.
495: *
496: * @exception LifecycleException
497: * if this component detects a fatal error that prevents this
498: * component from being used
499: */
500: public void start() throws LifecycleException {
501:
502: // Validate and update our current component state
503: if (started)
504: throw new LifecycleException(sm
505: .getString("jvmRoute.valve.alreadyStarted"));
506: lifecycle.fireLifecycleEvent(START_EVENT, null);
507: started = true;
508: if (cluster == null) {
509: Container hostContainer = getContainer();
510: // compatibility with JvmRouteBinderValve version 1.1
511: // ( setup at context.xml or context.xml.default )
512: if (!(hostContainer instanceof Host)) {
513: if (log.isWarnEnabled())
514: log.warn(sm.getString("jvmRoute.configure.warn"));
515: hostContainer = hostContainer.getParent();
516: }
517: if (hostContainer instanceof Host
518: && ((Host) hostContainer).getCluster() != null) {
519: cluster = (CatalinaCluster) ((Host) hostContainer)
520: .getCluster();
521: } else {
522: Container engine = hostContainer.getParent();
523: if (engine instanceof Engine
524: && ((Engine) engine).getCluster() != null) {
525: cluster = (CatalinaCluster) ((Engine) engine)
526: .getCluster();
527: }
528: }
529: }
530: if (cluster == null) {
531: throw new RuntimeException(
532: "No clustering support at container "
533: + container.getName());
534: }
535:
536: if (log.isInfoEnabled())
537: log.info(sm.getString("jvmRoute.valve.started"));
538:
539: }
540:
541: /**
542: * Gracefully terminate the active use of the public methods of this
543: * component. This method should be the last one called on a given instance
544: * of this component.
545: *
546: * @exception LifecycleException
547: * if this component detects a fatal error that needs to be
548: * reported
549: */
550: public void stop() throws LifecycleException {
551:
552: // Validate and update our current component state
553: if (!started)
554: throw new LifecycleException(sm
555: .getString("jvmRoute.valve.notStarted"));
556: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
557: started = false;
558: cluster = null;
559: numberOfSessions = 0;
560: if (log.isInfoEnabled())
561: log.info(sm.getString("jvmRoute.valve.stopped"));
562:
563: }
564:
565: }
|