| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.cluster.session; |
| |
| import java.io.IOException; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.Cookie; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Engine; |
| import org.apache.catalina.Globals; |
| import org.apache.catalina.Host; |
| import org.apache.catalina.Lifecycle; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.LifecycleListener; |
| import org.apache.catalina.Manager; |
| import org.apache.catalina.Session; |
| import org.apache.catalina.cluster.CatalinaCluster; |
| import org.apache.catalina.cluster.ClusterManager; |
| import org.apache.catalina.cluster.ClusterMessage; |
| import org.apache.catalina.cluster.ClusterValve; |
| import org.apache.catalina.connector.Request; |
| import org.apache.catalina.connector.Response; |
| import org.apache.catalina.session.ManagerBase; |
| import org.apache.catalina.util.LifecycleSupport; |
| import org.apache.catalina.util.StringManager; |
| import org.apache.catalina.valves.ValveBase; |
| |
| /** |
| * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node |
| * failure. After a node crashed the next request going to other cluster node. |
| * Now the answering from apache is slower ( make some error handshaking. Very |
| * bad with apache at my windows.). We rewrite now the jsessionid |
| * information to the backup cluster node. After the next response all client |
| * request goes direct to the backup node. The change sessionid send also to all |
| * other cluster nodes. Well, now the session stickyness work directly to the |
| * backup node and traffic don't go back too restarted cluster nodes! |
| * As jsessionid was created by cookie, the change JSESSIONID cookie resend with next response. |
| * |
| * At all cluster node you must configure the as ClusterListener since 5.5.10 |
| * {@link org.apache.catalina.cluster.session.JvmRouteSessionIDBinderListener JvmRouteSessionIDBinderListener} |
| * or before with |
| * org.apache.catalina.cluster.session.JvmRouteSessionIDBinderListenerLifecycle. |
| * |
| * Add this Valve to your host definition at conf/server.xml . |
| * |
| * Since 5.5.10 as direct cluster valve:<br/> |
| * <pre> |
| * <Cluster> |
| * <Valve className="org.apache.catalina.cluster.session.JvmRouteBinderValve" /> |
| * </Cluster> |
| * </pre> |
| * <br /> |
| * Before 5.5.10 as Host element:<br/> |
| * <pre> |
| * <Hostr> |
| * <Valve className="org.apache.catalina.cluster.session.JvmRouteBinderValve" /> |
| * </Hostr> |
| * </pre> |
| * |
| * Trick:<br/> |
| * You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes! |
| * Set enable true on all JvmRouteBinderValve backups, disable worker at mod_jk |
| * and then drop node and restart it! Then enable mod_jk Worker and disable JvmRouteBinderValves again. |
| * This use case means that only requested session are migrated. |
| * |
| * @author Peter Rossbach |
| * @version $Revision$ $Date$ |
| */ |
| public class JvmRouteBinderValve extends ValveBase implements ClusterValve, Lifecycle { |
| |
| /*--Static Variables----------------------------------------*/ |
| public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory |
| .getLog(JvmRouteBinderValve.class); |
| |
| /** |
| * The descriptive information about this implementation. |
| */ |
| protected static final String info = "org.apache.catalina.cluster.session.JvmRouteBinderValve/1.3"; |
| |
| /*--Instance Variables--------------------------------------*/ |
| |
| /** |
| * the cluster |
| */ |
| protected CatalinaCluster cluster; |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected StringManager sm = StringManager.getManager(Constants.Package); |
| |
| /** |
| * Has this component been started yet? |
| */ |
| protected boolean started = false; |
| |
| /** |
| * enabled this component |
| */ |
| protected boolean enabled = true; |
| |
| /** |
| * number of session that no at this tomcat instanz hosted |
| */ |
| protected long numberOfSessions = 0; |
| |
| protected String sessionIdAttribute = "org.apache.catalina.cluster.session.JvmRouteOrignalSessionID"; |
| |
| /** |
| * The lifecycle event support for this component. |
| */ |
| protected LifecycleSupport lifecycle = new LifecycleSupport(this); |
| |
| /*--Logic---------------------------------------------------*/ |
| |
| /** |
| * Return descriptive information about this implementation. |
| */ |
| public String getInfo() { |
| |
| return (info); |
| |
| } |
| |
| /** |
| * set session id attribute to failed node for request. |
| * |
| * @return Returns the sessionIdAttribute. |
| */ |
| public String getSessionIdAttribute() { |
| return sessionIdAttribute; |
| } |
| |
| /** |
| * get name of failed reqeust session attribute |
| * |
| * @param sessionIdAttribute |
| * The sessionIdAttribute to set. |
| */ |
| public void setSessionIdAttribute(String sessionIdAttribute) { |
| this.sessionIdAttribute = sessionIdAttribute; |
| } |
| |
| /** |
| * @return Returns the number of migrated sessions. |
| */ |
| public long getNumberOfSessions() { |
| return numberOfSessions; |
| } |
| |
| /** |
| * @return Returns the enabled. |
| */ |
| public boolean getEnabled() { |
| return enabled; |
| } |
| |
| /** |
| * @param enabled |
| * The enabled to set. |
| */ |
| public void setEnabled(boolean enabled) { |
| this.enabled = enabled; |
| } |
| |
| /** |
| * Detect possible the JVMRoute change at cluster backup node.. |
| * |
| * @param request |
| * tomcat request being processed |
| * @param response |
| * tomcat response being processed |
| * @exception IOException |
| * if an input/output error has occurred |
| * @exception ServletException |
| * if a servlet error has occurred |
| */ |
| public void invoke(Request request, Response response) throws IOException, |
| ServletException { |
| |
| if (getEnabled() |
| && getCluster() != null |
| && request.getContext() != null |
| && request.getContext().getDistributable() ) { |
| // valve cluster can access manager - other cluster handle turnover |
| // at host level - hopefully! |
| Manager manager = request.getContext().getManager(); |
| if (manager != null && manager instanceof ClusterManager |
| && getCluster().getManager(((ClusterManager)manager).getName()) != null) |
| handlePossibleTurnover(request, response); |
| } |
| // Pass this request on to the next valve in our pipeline |
| getNext().invoke(request, response); |
| } |
| |
| /** |
| * handle possible session turn over. |
| * |
| * @see JvmRouteBinderValve#handleJvmRoute(Request, Response, String, String) |
| * @param request current request |
| * @param response current response |
| */ |
| protected void handlePossibleTurnover(Request request, Response response) { |
| Session session = request.getSessionInternal(false); |
| if (session != null) { |
| long t1 = System.currentTimeMillis(); |
| String jvmRoute = getLocalJvmRoute(request); |
| if (jvmRoute == null) { |
| if (log.isWarnEnabled()) |
| log.warn(sm.getString("jvmRoute.missingJvmRouteAttribute")); |
| return; |
| } |
| handleJvmRoute( request, response,session.getIdInternal(), jvmRoute); |
| if (log.isDebugEnabled()) { |
| long t2 = System.currentTimeMillis(); |
| long time = t2 - t1; |
| log.debug(sm.getString("jvmRoute.turnoverInfo", new Long(time))); |
| } |
| } |
| } |
| |
| /** |
| * get jvmroute from engine |
| * |
| * @param request current request |
| * @return return jvmRoute from ManagerBase or null |
| */ |
| protected String getLocalJvmRoute(Request request) { |
| Manager manager = getManager(request); |
| if(manager instanceof ManagerBase) |
| return ((ManagerBase) manager).getJvmRoute(); |
| return null ; |
| } |
| |
| /** |
| * get Cluster DeltaManager |
| * |
| * @param request current request |
| * @return manager or null |
| */ |
| protected Manager getManager(Request request) { |
| Manager manager = request.getContext().getManager(); |
| if (log.isDebugEnabled()) { |
| if(manager != null) |
| log.debug(sm.getString("jvmRoute.foundManager", manager, request.getContext().getName())); |
| else |
| log.debug(sm.getString("jvmRoute.notFoundManager", manager, request.getContext().getName())); |
| } |
| return manager; |
| } |
| |
| /** |
| * @return Returns the cluster. |
| */ |
| public CatalinaCluster getCluster() { |
| return cluster; |
| } |
| |
| /** |
| * @param cluster The cluster to set. |
| */ |
| public void setCluster(CatalinaCluster cluster) { |
| this.cluster = cluster; |
| } |
| |
| /** |
| * Handle jvmRoute stickyness after tomcat instance failed. After this |
| * correction a new Cookie send to client with new jvmRoute and the |
| * SessionID change propage to the other cluster nodes. |
| * |
| * @param request current request |
| * @param response |
| * Tomcat Response |
| * @param sessionId |
| * request SessionID from Cookie |
| * @param localJvmRoute |
| * local jvmRoute |
| */ |
| protected void handleJvmRoute( |
| Request request, Response response,String sessionId, String localJvmRoute) { |
| // get requested jvmRoute. |
| String requestJvmRoute = null; |
| int index = sessionId.indexOf("."); |
| if (index > 0) { |
| requestJvmRoute = sessionId |
| .substring(index + 1, sessionId.length()); |
| } |
| if (requestJvmRoute != null && !requestJvmRoute.equals(localJvmRoute)) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.failover", requestJvmRoute, |
| localJvmRoute, sessionId)); |
| } |
| // OK - turnover the session ? |
| String newSessionID = sessionId.substring(0, index) + "." |
| + localJvmRoute; |
| Session catalinaSession = null; |
| try { |
| catalinaSession = getManager(request).findSession(sessionId); |
| } catch (IOException e) { |
| // Hups! |
| } |
| if (catalinaSession != null) { |
| changeSessionID(request, response, sessionId, newSessionID, |
| catalinaSession); |
| numberOfSessions++; |
| } else { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.cannotFindSession", |
| sessionId)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * change session id and send to all cluster nodes |
| * |
| * @param request current request |
| * @param response current response |
| * @param sessionId |
| * original session id |
| * @param newSessionID |
| * new session id for node migration |
| * @param catalinaSession |
| * current session with original session id |
| */ |
| protected void changeSessionID(Request request, |
| Response response, String sessionId, String newSessionID, Session catalinaSession) { |
| lifecycle.fireLifecycleEvent("Before session migration", |
| catalinaSession); |
| request.setRequestedSessionId(newSessionID); |
| catalinaSession.setId(newSessionID); |
| if (catalinaSession instanceof DeltaSession) |
| ((DeltaSession) catalinaSession).resetDeltaRequest(); |
| if(request.isRequestedSessionIdFromCookie()) |
| setNewSessionCookie(request, response,newSessionID); |
| // set orginal sessionid at request, to allow application detect the |
| // change |
| if (sessionIdAttribute != null && !"".equals(sessionIdAttribute)) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.set.orignalsessionid",sessionIdAttribute,sessionId)); |
| } |
| request.setAttribute(sessionIdAttribute, sessionId); |
| } |
| // now sending the change to all other clusternode! |
| ClusterManager manager = (ClusterManager)catalinaSession.getManager(); |
| sendSessionIDClusterBackup(manager,request,sessionId, newSessionID); |
| lifecycle |
| .fireLifecycleEvent("After session migration", catalinaSession); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.changeSession", sessionId, |
| newSessionID)); |
| } |
| } |
| |
| /** |
| * Send the changed Sessionid to all clusternodes. |
| * |
| * @see JvmRouteSessionIDBinderListener#messageReceived(ClusterMessage) |
| * @param manager |
| * ClusterManager |
| * @param sessionId |
| * current failed sessionid |
| * @param newSessionID |
| * new session id, bind to the new cluster node |
| */ |
| protected void sendSessionIDClusterBackup(ClusterManager manager,Request request,String sessionId, |
| String newSessionID) { |
| SessionIDMessage msg = new SessionIDMessage(); |
| msg.setOrignalSessionID(sessionId); |
| msg.setBackupSessionID(newSessionID); |
| Context context = request.getContext(); |
| msg.setContextPath(context.getPath()); |
| msg.setHost(context.getParent().getName()); |
| if(manager.isSendClusterDomainOnly()) |
| cluster.sendClusterDomain(msg); |
| else |
| cluster.send(msg); |
| } |
| |
| /** |
| * Sets a new cookie for the given session id and response and see |
| * {@link org.apache.catalina.connector.Request#configureSessionCookie(javax.servlet.http.Cookie)} |
| * |
| * @param request current request |
| * @param response Tomcat Response |
| * @param sessionId The session id |
| */ |
| protected void setNewSessionCookie(Request request, |
| Response response, String sessionId) { |
| if (response != null) { |
| Context context = request.getContext(); |
| if (context.getCookies()) { |
| // set a new session cookie |
| Cookie newCookie = new Cookie(Globals.SESSION_COOKIE_NAME, |
| sessionId); |
| newCookie.setMaxAge(-1); |
| String contextPath = null; |
| if (!response.getConnector().getEmptySessionPath() |
| && (context != null)) { |
| contextPath = context.getEncodedPath(); |
| } |
| if ((contextPath != null) && (contextPath.length() > 0)) { |
| newCookie.setPath(contextPath); |
| } else { |
| newCookie.setPath("/"); |
| } |
| if (request.isSecure()) { |
| newCookie.setSecure(true); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.newSessionCookie", |
| sessionId, Globals.SESSION_COOKIE_NAME, newCookie |
| .getPath(), new Boolean(newCookie |
| .getSecure()))); |
| } |
| response.addCookie(newCookie); |
| } |
| } |
| } |
| |
| // ------------------------------------------------------ Lifecycle Methods |
| |
| /** |
| * Add a lifecycle event listener to this component. |
| * |
| * @param listener |
| * The listener to add |
| */ |
| public void addLifecycleListener(LifecycleListener listener) { |
| |
| lifecycle.addLifecycleListener(listener); |
| |
| } |
| |
| /** |
| * Get the lifecycle listeners associated with this lifecycle. If this |
| * Lifecycle has no listeners registered, a zero-length array is returned. |
| */ |
| public LifecycleListener[] findLifecycleListeners() { |
| |
| return lifecycle.findLifecycleListeners(); |
| |
| } |
| |
| /** |
| * Remove a lifecycle event listener from this component. |
| * |
| * @param listener |
| * The listener to add |
| */ |
| public void removeLifecycleListener(LifecycleListener listener) { |
| |
| lifecycle.removeLifecycleListener(listener); |
| |
| } |
| |
| /** |
| * Prepare for the beginning of active use of the public methods of this |
| * component. This method should be called after <code>configure()</code>, |
| * and before any of the public methods of the component are utilized. |
| * |
| * @exception LifecycleException |
| * if this component detects a fatal error that prevents this |
| * component from being used |
| */ |
| public void start() throws LifecycleException { |
| |
| // Validate and update our current component state |
| if (started) |
| throw new LifecycleException(sm |
| .getString("jvmRoute.valve.alreadyStarted")); |
| lifecycle.fireLifecycleEvent(START_EVENT, null); |
| started = true; |
| if (cluster == null) { |
| Container hostContainer = getContainer(); |
| // compatibility with JvmRouteBinderValve version 1.1 |
| // ( setup at context.xml or context.xml.default ) |
| if (!(hostContainer instanceof Host)) { |
| if (log.isWarnEnabled()) |
| log.warn(sm.getString("jvmRoute.configure.warn")); |
| hostContainer = hostContainer.getParent(); |
| } |
| if (hostContainer instanceof Host |
| && ((Host) hostContainer).getCluster() != null) { |
| cluster = (CatalinaCluster) ((Host) hostContainer).getCluster(); |
| } else { |
| Container engine = hostContainer.getParent() ; |
| if (engine instanceof Engine |
| && ((Engine) engine).getCluster() != null) { |
| cluster = (CatalinaCluster) ((Engine) engine).getCluster(); |
| } |
| } |
| } |
| if (cluster == null) { |
| throw new RuntimeException("No clustering support at container " |
| + container.getName()); |
| } |
| |
| if (log.isInfoEnabled()) |
| log.info(sm.getString("jvmRoute.valve.started")); |
| |
| } |
| |
| /** |
| * Gracefully terminate the active use of the public methods of this |
| * component. This method should be the last one called on a given instance |
| * of this component. |
| * |
| * @exception LifecycleException |
| * if this component detects a fatal error that needs to be |
| * reported |
| */ |
| public void stop() throws LifecycleException { |
| |
| // Validate and update our current component state |
| if (!started) |
| throw new LifecycleException(sm |
| .getString("jvmRoute.valve.notStarted")); |
| lifecycle.fireLifecycleEvent(STOP_EVENT, null); |
| started = false; |
| cluster = null; |
| numberOfSessions = 0; |
| if (log.isInfoEnabled()) |
| log.info(sm.getString("jvmRoute.valve.stopped")); |
| |
| } |
| |
| } |