| /* |
| * 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.ha.session; |
| |
| import java.io.IOException; |
| |
| import javax.servlet.ServletException; |
| |
| import org.apache.catalina.Cluster; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.Manager; |
| import org.apache.catalina.Session; |
| import org.apache.catalina.connector.Request; |
| import org.apache.catalina.connector.Response; |
| import org.apache.catalina.ha.CatalinaCluster; |
| import org.apache.catalina.ha.ClusterManager; |
| import org.apache.catalina.ha.ClusterValve; |
| import org.apache.catalina.session.ManagerBase; |
| import org.apache.catalina.session.PersistentManager; |
| import org.apache.catalina.valves.ValveBase; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node |
| * failure. After a node crashes, subsequent requests go to other cluster nodes. |
| * That incurs a drop in performance. When this Valve is enabled on a backup |
| * node and sees a request, which was intended for another (thus failed) node, |
| * it will rewrite the cookie jsessionid information to use the route to this |
| * backup cluster node, that answered the request. After the response is |
| * delivered to the client, all subsequent client requests will go directly to |
| * the backup node. The change of sessionid is also sent to all other cluster |
| * nodes. After all that, the session stickiness will work directly to the |
| * backup node and the traffic will not go back to the failed node after it is |
| * restarted! |
| * |
| * <p> |
| * Add this Valve to your cluster definition at conf/server.xml . |
| * |
| * <pre> |
| * <Cluster> |
| * <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" /> |
| * </Cluster> |
| * </pre> |
| * |
| * <em>A Trick:</em><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 sessions are migrated. |
| * |
| * @author Peter Rossbach |
| */ |
| public class JvmRouteBinderValve extends ValveBase implements ClusterValve { |
| |
| /*--Static Variables----------------------------------------*/ |
| public static final org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory |
| .getLog(JvmRouteBinderValve.class); |
| |
| //------------------------------------------------------ Constructor |
| public JvmRouteBinderValve() { |
| super(true); |
| } |
| |
| /*--Instance Variables--------------------------------------*/ |
| |
| /** |
| * the cluster |
| */ |
| protected CatalinaCluster cluster; |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected static final StringManager sm = StringManager.getManager(Constants.Package); |
| |
| /** |
| * enabled this component |
| */ |
| protected boolean enabled = true; |
| |
| /** |
| * number of session that no at this tomcat instance hosted |
| */ |
| protected long numberOfSessions = 0; |
| |
| protected String sessionIdAttribute = "org.apache.catalina.ha.session.JvmRouteOrignalSessionID"; |
| |
| |
| /*--Logic---------------------------------------------------*/ |
| |
| /** |
| * set session id attribute to failed node for request. |
| * |
| * @return Returns the sessionIdAttribute. |
| */ |
| public String getSessionIdAttribute() { |
| return sessionIdAttribute; |
| } |
| |
| /** |
| * get name of failed request 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 |
| */ |
| @Override |
| public void invoke(Request request, Response response) throws IOException, |
| ServletException { |
| |
| if (getEnabled() && |
| request.getContext() != null && |
| request.getContext().getDistributable() && |
| !request.isAsyncDispatching()) { |
| // 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() != null |
| && getCluster().getManager(((ClusterManager)manager).getName()) != null) |
| || |
| (manager instanceof PersistentManager))) { |
| handlePossibleTurnover(request); |
| } |
| } |
| // Pass this request on to the next valve in our pipeline |
| getNext().invoke(request, response); |
| } |
| |
| /** |
| * handle possible session turn over. |
| * |
| * @see JvmRouteBinderValve#handleJvmRoute(Request, String, String) |
| * @param request current request |
| */ |
| protected void handlePossibleTurnover(Request request) { |
| String sessionID = request.getRequestedSessionId() ; |
| if (sessionID != null) { |
| long t1 = System.currentTimeMillis(); |
| String jvmRoute = getLocalJvmRoute(request); |
| if (jvmRoute == null) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.missingJvmRouteAttribute")); |
| } |
| return; |
| } |
| handleJvmRoute( request, sessionID, jvmRoute); |
| if (log.isDebugEnabled()) { |
| long t2 = System.currentTimeMillis(); |
| long time = t2 - t1; |
| log.debug(sm.getString("jvmRoute.turnoverInfo", Long.valueOf(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 ClusterManager |
| * |
| * @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", request.getContext().getName())); |
| } |
| } |
| return manager; |
| } |
| |
| /** |
| * @return Returns the cluster. |
| */ |
| @Override |
| public CatalinaCluster getCluster() { |
| return cluster; |
| } |
| |
| /** |
| * @param cluster The cluster to set. |
| */ |
| @Override |
| public void setCluster(CatalinaCluster cluster) { |
| this.cluster = cluster; |
| } |
| |
| /** |
| * Handle jvmRoute stickiness after tomcat instance failed. After this |
| * correction a new Cookie send to client with new jvmRoute and the |
| * SessionID change propagate to the other cluster nodes. |
| * |
| * @param request current request |
| * @param sessionId |
| * request SessionID from Cookie |
| * @param localJvmRoute |
| * local jvmRoute |
| */ |
| protected void handleJvmRoute( |
| Request request, 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)); |
| } |
| Session catalinaSession = null; |
| try { |
| catalinaSession = getManager(request).findSession(sessionId); |
| } catch (IOException e) { |
| // Hups! |
| } |
| String id = sessionId.substring(0, index); |
| String newSessionID = id + "." + localJvmRoute; |
| // OK - turnover the session and inform other cluster nodes |
| if (catalinaSession != null) { |
| changeSessionID(request, sessionId, newSessionID, |
| catalinaSession); |
| numberOfSessions++; |
| } else { |
| try { |
| catalinaSession = getManager(request).findSession(newSessionID); |
| } catch (IOException e) { |
| // Hups! |
| } |
| if (catalinaSession != null) { |
| // session is rewrite at other request, rewrite this also |
| changeRequestSessionID(request, sessionId, newSessionID); |
| } 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 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, String sessionId, |
| String newSessionID, Session catalinaSession) { |
| fireLifecycleEvent("Before session migration", catalinaSession); |
| catalinaSession.getManager().changeSessionId(catalinaSession, newSessionID); |
| changeRequestSessionID(request, sessionId, newSessionID); |
| fireLifecycleEvent("After session migration", catalinaSession); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jvmRoute.changeSession", sessionId, |
| newSessionID)); |
| } |
| } |
| |
| /** |
| * Change Request Session id |
| * @param request current request |
| * @param sessionId |
| * original session id |
| * @param newSessionID |
| * new session id for node migration |
| */ |
| protected void changeRequestSessionID(Request request, String sessionId, String newSessionID) { |
| request.changeSessionId(newSessionID); |
| |
| // set original 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); |
| } |
| } |
| |
| |
| /** |
| * Start this component and implement the requirements |
| * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. |
| * |
| * @exception LifecycleException if this component detects a fatal error |
| * that prevents this component from being used |
| */ |
| @Override |
| protected synchronized void startInternal() throws LifecycleException { |
| |
| if (cluster == null) { |
| Cluster containerCluster = getContainer().getCluster(); |
| if (containerCluster instanceof CatalinaCluster) { |
| setCluster((CatalinaCluster)containerCluster); |
| } |
| } |
| |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("jvmRoute.valve.started")); |
| if (cluster == null) { |
| log.info(sm.getString("jvmRoute.noCluster")); |
| } |
| } |
| |
| super.startInternal(); |
| } |
| |
| |
| /** |
| * Stop this component and implement the requirements |
| * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. |
| * |
| * @exception LifecycleException if this component detects a fatal error |
| * that prevents this component from being used |
| */ |
| @Override |
| protected synchronized void stopInternal() throws LifecycleException { |
| |
| super.stopInternal(); |
| |
| cluster = null; |
| numberOfSessions = 0; |
| if (log.isInfoEnabled()) { |
| log.info(sm.getString("jvmRoute.valve.stopped")); |
| } |
| |
| } |
| |
| } |