You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by sc...@apache.org on 2017/06/21 19:05:38 UTC

svn commit: r1799498 - in /tomcat/trunk: java/org/apache/catalina/valves/LoadBalancerDrainingValve.java test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java webapps/docs/changelog.xml webapps/docs/config/valve.xml

Author: schultz
Date: Wed Jun 21 19:05:38 2017
New Revision: 1799498

URL: http://svn.apache.org/viewvc?rev=1799498&view=rev
Log:
Add LoadBalancerDrainingValve.

Added:
    tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java   (with props)
    tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java   (with props)
Modified:
    tomcat/trunk/webapps/docs/changelog.xml
    tomcat/trunk/webapps/docs/config/valve.xml

Added: tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java?rev=1799498&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java (added)
+++ tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java Wed Jun 21 19:05:38 2017
@@ -0,0 +1,277 @@
+/*
+ * 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.valves;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.util.SessionConfig;
+import org.apache.juli.logging.Log;
+
+/**
+ * <p>A Valve to detect situations where a load-balanced node receiving a
+ * request has been deactivated by the load balancer (JK_LB_ACTIVATION=DIS)
+ * and the incoming request has no valid session.</p>
+ *
+ * <p>In these cases, the user's session cookie should be removed if it exists,
+ * any ";jsessionid" parameter should be removed from the request URI,
+ * and the client should be redirected to the same URI. This will cause the
+ * load-balanced to re-balance the client to another server.</p>
+ *
+ * <p>A request parameter is added to the redirect URI in order to avoid
+ * repeated redirects in the event of an error or misconfiguration.</p>
+ *
+ * <p>All this work is required because when the activation state of a node is
+ * DISABLED, the load-balancer will still send requests to the node if they
+ * appear to have a session on that node. Since mod_jk doesn't actually know
+ * whether the session id is valid, it will send the request blindly to
+ * the disabled node, which makes it take much longer to drain the node
+ * than strictly necessary.</p>
+ *
+ * <p>For testing purposes, a special cookie can be configured and used
+ * by a client to ignore the normal behavior of this Valve and allow
+ * a client to get a new session on a DISABLED node. See
+ * {@link #setIgnoreCookieName} and {@link #setIgnoreCookieValue}
+ * to configure those values.</p>
+ *
+ * <p>This Valve should be installed earlier in the Valve pipeline than any
+ * authentication valves, as the redirection should take place before an
+ * authentication valve would save a request to a protected resource.</p>
+ *
+ * @see http://tomcat.apache.org/connectors-doc/generic_howto/loadbalancers.html
+ */
+public class LoadBalancerDrainingValve
+    extends ValveBase
+{
+    /**
+     * The request attribute key where the load-balancer's activation state
+     * can be found.
+     */
+    static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION = "JK_LB_ACTIVATION";
+
+    /**
+     * The HTTP response code that will be used to redirect the request
+     * back to the load-balancer for re-balancing. Defaults to 307
+     * (TEMPORARY_REDIRECT).
+     *
+     * HTTP status code 305 (USE_PROXY) might be an option, here. too.
+     */
+    private int _redirectStatusCode = HttpServletResponse.SC_TEMPORARY_REDIRECT;
+
+    /**
+     * The name of the cookie which can be set to ignore the "draining" action
+     * of this Filter. This will allow a client to contact the server without
+     * being re-balanced to another server. The expected cookie value can be set
+     * in the {@link #_ignoreCookieValue}. The cookie name and value must match
+     * to avoid being re-balanced.
+     */
+    private String _ignoreCookieName;
+
+    /**
+     * The value of the cookie which can be set to ignore the "draining" action
+     * of this Filter. This will allow a client to contact the server without
+     * being re-balanced to another server. The expected cookie name can be set
+     * in the {@link #_ignoreCookieValue}. The cookie name and value must match
+     * to avoid being re-balanced.
+     */
+    private String _ignoreCookieValue;
+
+    /**
+     * Local reference to the container log.
+     */
+    protected Log containerLog = null;
+
+    public LoadBalancerDrainingValve()
+    {
+        super(true); // Supports async
+    }
+
+    //
+    // Configuration parameters
+    //
+
+    /**
+     * Sets the HTTP response code that will be used to redirect the request
+     * back to the load-balancer for re-balancing. Defaults to 307
+     * (TEMPORARY_REDIRECT).
+     */
+    public void setRedirectStatusCode(int code) {
+        _redirectStatusCode = code;
+    }
+
+    /**
+     * Gets the name of the cookie that can be used to override the
+     * re-balancing behavior of this Valve when the current node is
+     * in the DISABLED activation state.
+     *
+     * @return The cookie name used to ignore normal processing rules.
+     *
+     * @see #setIgnoreCookieValue
+     */
+    public String getIgnoreCookieName() {
+        return _ignoreCookieName;
+    }
+
+    /**
+     * Sets the name of the cookie that can be used to override the
+     * re-balancing behavior of this Valve when the current node is
+     * in the DISABLED activation state.
+     *
+     * There is no default value for this setting: the ability to override
+     * the re-balancing behavior of this Valve is <i>disabled</i> by default.
+     *
+     * @param cookieName The cookie name to use to ignore normal
+     *                   processing rules.
+     *
+     * @see #getIgnoreCookieValue
+     */
+    public void setIgnoreCookieName(String cookieName) {
+        _ignoreCookieName = cookieName;
+    }
+
+    /**
+     * Gets the expected value of the cookie that can be used to override the
+     * re-balancing behavior of this Valve when the current node is
+     * in the DISABLED activation state.
+     *
+     * @return The cookie value used to ignore normal processing rules.
+     *
+     * @see #setIgnoreCookieValue
+     */
+    public String getIgnoreCookieValue() {
+        return _ignoreCookieValue;
+    }
+
+    /**
+     * Sets the expected value of the cookie that can be used to override the
+     * re-balancing behavior of this Valve when the current node is
+     * in the DISABLED activation state. The "ignore" cookie's value
+     * <b>must</b> be exactly equal to this value in order to allow
+     * the client to override the re-balancing behavior.
+     *
+     * @param cookieValue The cookie value to use to ignore normal
+     *                    processing rules.
+     *
+     * @see #getIgnoreCookieValue
+     */
+    public void setIgnoreCookieValue(String cookieValue) {
+        _ignoreCookieValue = cookieValue;
+    }
+
+    @Override
+    public void initInternal()
+        throws LifecycleException
+    {
+        super.initInternal();
+
+        containerLog = getContainer().getLogger();
+    }
+
+    @Override
+    public void invoke(Request request, Response response) throws IOException, ServletException {
+        if("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_LB_ACTIVATION))
+           && !request.isRequestedSessionIdValid()) {
+
+            if(containerLog.isDebugEnabled())
+                containerLog.debug("Load-balancer is in DISABLED state; draining this node");
+
+            boolean ignoreRebalance = false; // Allow certain clients
+            Cookie sessionCookie = null;
+
+            // Kill any session cookie present
+            final Cookie[] cookies = request.getCookies();
+
+            final String sessionCookieName = request.getServletContext().getSessionCookieConfig().getName();
+
+            // Kill any session cookie present
+            if(null != cookies) {
+                for(Cookie cookie : cookies) {
+                    final String cookieName = cookie.getName();
+                    if(containerLog.isTraceEnabled())
+                        containerLog.trace("Checking cookie " + cookieName + "=" + cookie.getValue());
+
+                    if(sessionCookieName.equals(cookieName)
+                       && request.getRequestedSessionId().equals(cookie.getValue())) {
+                        sessionCookie = cookie;
+                    } else
+                    // Is the client presenting a valid ignore-cookie value?
+                    if(null != _ignoreCookieName
+                            && _ignoreCookieName.equals(cookieName)
+                            && null != _ignoreCookieValue
+                            && _ignoreCookieValue.equals(cookie.getValue())) {
+                        ignoreRebalance = true;
+                    }
+                }
+            }
+
+            if(ignoreRebalance) {
+                if(containerLog.isDebugEnabled())
+                    containerLog.debug("Client is presenting a valid " + _ignoreCookieName
+                                 + " cookie, re-balancing is being skipped");
+
+                getNext().invoke(request, response);
+
+                return;
+            }
+
+            // Kill any session cookie that was found
+            // TODO: Consider implications of SSO cookies
+            if(null != sessionCookie) {
+                String cookiePath = request.getServletContext().getSessionCookieConfig().getPath();
+
+                if(request.getContext().getSessionCookiePathUsesTrailingSlash()) {
+                    // Handle special case of ROOT context where cookies require a path of
+                    // '/' but the servlet spec uses an empty string
+                    // Also ensure the cookies for a context with a path of /foo don't get
+                    // sent for requests with a path of /foobar
+                    if (!cookiePath.endsWith("/"))
+                        cookiePath = cookiePath + "/";
+
+                    sessionCookie.setPath(cookiePath);
+                    sessionCookie.setMaxAge(0); // Delete
+                    sessionCookie.setValue(""); // Purge the cookie's value
+                    response.addCookie(sessionCookie);
+                }
+            }
+
+            // Re-write the URI if it contains a ;jsessionid parameter
+            String uri = request.getRequestURI();
+            String sessionURIParamName = "jsessionid";
+            SessionConfig.getSessionUriParamName(request.getContext());
+            if(uri.contains(";" + sessionURIParamName + "="))
+                uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", "");
+
+            String queryString = request.getQueryString();
+
+            if(null != queryString)
+                uri = uri + "?" + queryString;
+
+            // NOTE: Do not call response.encodeRedirectURL or the bad
+            // sessionid will be restored
+            response.setHeader("Location", uri);
+            response.setStatus(_redirectStatusCode);
+        }
+        else
+            getNext().invoke(request, response);
+    }
+}

Propchange: tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java?rev=1799498&view=auto
==============================================================================
--- tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java (added)
+++ tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java Wed Jun 21 19:05:38 2017
@@ -0,0 +1,257 @@
+/* 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.valves;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.http.Cookie;
+
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Valve;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.core.StandardPipeline;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+
+public class TestLoadBalancerDrainingValve {
+
+    static class MockResponse extends Response {
+        private List<Cookie> cookies;
+        @Override
+        public boolean isCommitted() {
+            return false;
+        }
+        @Override
+        public void addCookie(Cookie cookie)
+        {
+            if(null == cookies)
+                cookies = new ArrayList<Cookie>(1);
+            cookies.add(cookie);
+        }
+        public List<Cookie> getCookies() {
+            return cookies;
+        }
+    }
+
+    static class CookieConfig implements SessionCookieConfig {
+
+        private String name;
+        private String domain;
+        private String path;
+        private String comment;
+        private boolean httpOnly;
+        private boolean secure;
+        private int maxAge;
+
+        @Override
+        public String getName() {
+            return name;
+        }
+        @Override
+        public void setName(String name) {
+            this.name = name;
+        }
+        @Override
+        public String getDomain() {
+            return domain;
+        }
+        @Override
+        public void setDomain(String domain) {
+            this.domain = domain;
+        }
+        @Override
+        public String getPath() {
+            return path;
+        }
+        @Override
+        public void setPath(String path) {
+            this.path = path;
+        }
+        @Override
+        public String getComment() {
+            return comment;
+        }
+        @Override
+        public void setComment(String comment) {
+            this.comment = comment;
+        }
+        @Override
+        public boolean isHttpOnly() {
+            return httpOnly;
+        }
+        @Override
+        public void setHttpOnly(boolean httpOnly) {
+            this.httpOnly = httpOnly;
+        }
+        @Override
+        public boolean isSecure() {
+            return secure;
+        }
+        @Override
+        public void setSecure(boolean secure) {
+            this.secure = secure;
+        }
+        @Override
+        public int getMaxAge() {
+            return maxAge;
+        }
+        @Override
+        public void setMaxAge(int maxAge) {
+            this.maxAge = maxAge;
+        }
+    }
+
+    // A Cookie subclass that knows how to compare itself to other Cookie objects
+    static class MyCookie extends Cookie {
+      public MyCookie(String name, String value) { super(name, value); }
+
+      @Override 
+      public boolean equals(Object o) {
+        if(null == o) return false;
+        MyCookie mc = (MyCookie)o;
+
+        return mc.getName().equals(this.getName())
+            && mc.getPath().equals(this.getPath())
+            && mc.getValue().equals(this.getValue())
+            && mc.getMaxAge() == this.getMaxAge();
+      }
+
+      @Override
+      public String toString() {
+          return "Cookie { name=" + getName() + ", value=" + getValue() + ", path=" + getPath() + ", maxAge=" + getMaxAge() + " }";
+      }
+    }
+
+    @Test
+    public void testNormalRequest() throws Exception {
+        runValve("ACT", true, true, false, null);
+    }
+
+    @Test
+    public void testDisabledValidSession() throws Exception {
+        runValve("DIS", true, true, false, null);
+    }
+
+    @Test
+    public void testDisabledInvalidSession() throws Exception {
+        runValve("DIS", false, false, false, "foo=bar");
+    }
+
+    @Test
+    public void testDisabledInvalidSessionWithIgnore() throws Exception {
+        runValve("DIS", false, true, true, "foo=bar");
+    }
+
+    private void runValve(String jkActivation,
+                          boolean validSessionId,
+                          boolean expectInvokeNext,
+                          boolean enableIgnore,
+                          String queryString) throws Exception {
+        IMocksControl control = EasyMock.createControl();
+        ServletContext servletContext = control.createMock(ServletContext.class);
+        Context ctx = control.createMock(Context.class);
+        Request request = control.createMock(Request.class);
+        Response response = control.createMock(Response.class);
+
+        String sessionCookieName = "JSESSIONID";
+        String sessionId = "cafebabe";
+        String requestURI = "/test/path";
+        SessionCookieConfig cookieConfig = new CookieConfig();
+        cookieConfig.setDomain("example.com");
+        cookieConfig.setName(sessionCookieName);
+        cookieConfig.setPath("/");
+
+        // Valve.init requires all of this stuff
+        EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn("");
+        EasyMock.expect(ctx.getName()).andStubReturn("");
+        EasyMock.expect(ctx.getPipeline()).andStubReturn(new StandardPipeline());
+        EasyMock.expect(ctx.getDomain()).andStubReturn("foo");
+        EasyMock.expect(ctx.getLogger()).andStubReturn(org.apache.juli.logging.LogFactory.getLog(LoadBalancerDrainingValve.class));
+        EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext);
+
+        // Set up the actual test
+        EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBUTE_KEY_JK_LB_ACTIVATION)).andStubReturn(jkActivation);
+        EasyMock.expect(request.isRequestedSessionIdValid()).andStubReturn(validSessionId);
+
+        ArrayList<Cookie> cookies = new ArrayList<Cookie>();
+        if(enableIgnore) {
+            cookies.add(new Cookie("ignore", "true"));
+        }
+
+        if(!validSessionId) {
+            MyCookie cookie = new MyCookie(cookieConfig.getName(), sessionId);
+            cookie.setPath(cookieConfig.getPath());
+            cookie.setValue(sessionId);
+
+            cookies.add(cookie);
+
+            EasyMock.expect(request.getRequestedSessionId()).andStubReturn(sessionId);
+            EasyMock.expect(request.getRequestURI()).andStubReturn(requestURI);
+            EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(new Cookie[cookies.size()]));
+            EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(cookieConfig);
+            EasyMock.expect(request.getServletContext()).andStubReturn(servletContext);
+            EasyMock.expect(request.getContext()).andStubReturn(ctx);
+            EasyMock.expect(ctx.getSessionCookiePathUsesTrailingSlash()).andStubReturn(true);
+            EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(cookieConfig);
+            EasyMock.expect(request.getQueryString()).andStubReturn(queryString);
+
+           if(!enableIgnore) {
+                // Response will have cookie deleted
+                MyCookie expectedCookie = new MyCookie(cookieConfig.getName(), "");
+                expectedCookie.setPath(cookieConfig.getPath());
+                expectedCookie.setMaxAge(0);
+
+                // These two lines just mean EasyMock.expect(response.addCookie) but for a void method
+                response.addCookie(expectedCookie);
+                EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieName); // Indirect call
+                String expectedRequestURI = requestURI;
+                if(null != queryString)
+                    expectedRequestURI = expectedRequestURI + '?' + queryString;
+                response.setHeader("Location", expectedRequestURI);
+                response.setStatus(307);
+            }
+        }
+
+        Valve next = control.createMock(Valve.class);
+
+        if(expectInvokeNext) {
+            // Expect the "next" Valve to fire
+            // Next 2 lines are basically EasyMock.expect(next.invoke(req,res)) but for a void method
+            next.invoke(request, response);
+            EasyMock.expectLastCall();
+        }
+
+        // Get set to actually test
+        control.replay();
+
+        LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve();
+        valve.setContainer(ctx);
+        valve.init();
+        valve.setNext(next);
+        valve.setIgnoreCookieName("ignore");
+        valve.setIgnoreCookieValue("true");
+
+        valve.invoke(request, response);
+
+        control.verify();
+    }
+}

Propchange: tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1799498&r1=1799497&r2=1799498&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jun 21 19:05:38 2017
@@ -138,6 +138,10 @@
         variable for CGI executables is populated in a consistent way regardless
         of how the CGI servlet is mapped to a request. (markt)
       </fix>
+      <add>
+        Add LoadBalancerDrainingValve, a Valve designed to reduce the amount of
+        time required for a node to drain its authenticated users. (schultz)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">

Modified: tomcat/trunk/webapps/docs/config/valve.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xml?rev=1799498&r1=1799497&r2=1799498&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/config/valve.xml (original)
+++ tomcat/trunk/webapps/docs/config/valve.xml Wed Jun 21 19:05:38 2017
@@ -700,6 +700,81 @@
 
 
 <section name="Proxies Support">
+  <subsection name="Load Balancer Draining Valve">
+    <subsection name="Introduction">
+      <p>
+        When using mod_jk or mod_proxy_ajp, the client's session id is used to
+        determine which back-end server will be used to serve the request. If the
+        target node is being "drained" (in mod_jk, this is the <i>DISABLED</i>
+        state; in mod_proxy_ajp, this is the <i>Drain (N)</i> state), requests
+        for expired sessions can actually cause the draining node to fail to
+        drain.
+      </p>
+      <p>
+        Unfortunately, AJP-based load-balancers cannot prove whether the
+        client-provided session id is valid or not and therefore will send any
+        requests for a session that appears to be targeted to that node to the
+        disabled (or "draining") node, causing the "draining" process to take
+        longer than necessary.
+      </p>
+      <p>
+        This Valve detects requests for invalid sessions, strips the session
+        information from the request, and redirects back to the same URL, where
+        the load-balancer should choose a different (active)  node to handle the
+        request. This will accelerate the "draining" process for the disabled
+        node(s).
+      </p>
+
+      <p>
+        The activation state of the node is sent by the load-balancer in the
+        request, so no state change on the node being disabled is necessary. Simply
+        configure this Valve in your valve pipeline and it will take action when
+        the activation state is set to "disabled".
+      </p>
+
+      <p>
+        You should take care to register this Valve earlier in the Valve pipeline
+        than any authentication Valves, because this Valve should be able to
+        redirect a request before any authentication Valve saves a request to a
+        protected resource. If this happens, a new session will be created and
+        the draining process will stall because a new, valid session will be
+        established.
+      </p>
+    </subsection><!-- / Introduction -->
+
+    <subsection name="Attributes">
+      <p>The <strong>Load Balancer Draining Valve</strong> supports the
+      following configuration attributes:</p>
+
+      <attributes>
+        <attribute name="className" required="true">
+          <p>Java class name of the implementation to use. This MUST be set to
+          <strong>org.apache.catalina.valves.LoadBalancerDrainingValve</strong>.
+          </p>
+        </attribute>
+
+        <attribute name="redirectStatusCode" required="false">
+          <p>Allows setting a custom redirect code to be used when the client
+          is redirected to be re-balanced by the load-balancer. The default is
+          307 TEMPORARY_REDIRECT.</p>
+        </attribute>
+
+        <attribute name="ignoreCookieName" required="false">
+          <p>When used with <code>ignoreCookieValue</code>, a client can present
+          this cookie (and accompanying value) that will cause this Valve to
+          do nothing. This will allow you to probe your <i>disabled</i> node
+          before re-enabling it to make sure that it is working as expected.</p>
+        </attribute>
+
+        <attribute name="ignoreCookieValue" required="false">
+          <p>When used with <code>ignoreCookieName</code>, a client can present
+          a cookie (and accompanying value) that will cause this Valve to
+          do nothing. This will allow you to probe your <i>disabled</i> node
+          before re-enabling it to make sure that it is working as expected.</p>
+        </attribute>
+      </attributes>
+    </subsection><!-- /Attributes -->
+  </subsection><!-- /Load Balancer Draining Valve -->
 
 <subsection name="Remote IP Valve">
 



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: svn commit: r1799498 - in /tomcat/trunk: java/org/apache/catalina/valves/LoadBalancerDrainingValve.java test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java webapps/docs/changelog.xml webapps/docs/config/valve.xml

Posted by Mark Thomas <ma...@apache.org>.
On 21/06/17 20:05, schultz@apache.org wrote:
> Author: schultz
> Date: Wed Jun 21 19:05:38 2017
> New Revision: 1799498
> 
> URL: http://svn.apache.org/viewvc?rev=1799498&view=rev
> Log:
> Add LoadBalancerDrainingValve.
> 
> Added:
>     tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java   (with props)
>     tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java   (with props)
> Modified:
>     tomcat/trunk/webapps/docs/changelog.xml
>     tomcat/trunk/webapps/docs/config/valve.xml

<snip/>

> Modified: tomcat/trunk/webapps/docs/changelog.xml
> URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1799498&r1=1799497&r2=1799498&view=diff
> ==============================================================================
> --- tomcat/trunk/webapps/docs/changelog.xml (original)
> +++ tomcat/trunk/webapps/docs/changelog.xml Wed Jun 21 19:05:38 2017
> @@ -138,6 +138,10 @@
>          variable for CGI executables is populated in a consistent way regardless
>          of how the CGI servlet is mapped to a request. (markt)
>        </fix>
> +      <add>
> +        Add LoadBalancerDrainingValve, a Valve designed to reduce the amount of
> +        time required for a node to drain its authenticated users. (schultz)
> +      </add>
>      </changelog>
>    </subsection>
>    <subsection name="Coyote">

Wrong version again.

Mark

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: svn commit: r1799498 - in /tomcat/trunk: java/org/apache/catalina/valves/LoadBalancerDrainingValve.java test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java webapps/docs/changelog.xml webapps/docs/config/valve.xml

Posted by Christopher Schultz <ch...@christopherschultz.net>.
Martin,

On 6/22/17 3:04 AM, Martin Grigorov wrote:
> On Wed, Jun 21, 2017 at 9:05 PM, <sc...@apache.org> wrote:
>
>> +    /**
>> +     * The request attribute key where the load-balancer's activation
>> state
>> +     * can be found.
>> +     */
>> +    static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION =
>> "JK_LB_ACTIVATION";
>>
> > Any objection to make this constant public and visible from outside
> ? I find it useful to be able to refer the constant by name than its
> value when integrating.

No objections. It looks like I'll have a follow-on patch shortly, so I
can change this at the same time.

>> +            // Kill any session cookie present
>> +            if(null != cookies) {
>> +                for(Cookie cookie : cookies) {
>> +                    final String cookieName = cookie.getName();
>> +                    if(containerLog.isTraceEnabled())
>> +                        containerLog.trace("Checking cookie " +
>> cookieName + "=" + cookie.getValue());
>> +
>> +                    if(sessionCookieName.equals(cookieName)
>> +                       &&
request.getRequestedSessionId().equals(cookie.getValue()))
>> {
>> +                        sessionCookie = cookie;
>>
>
> Is it a good idea to 'break' here ?
> Do you expect more than one session cookies ?!

No, but I do expect that there may be more interesting cookies later on
in the list...

>> +                    } else
>> +                    // Is the client presenting a valid ignore-cookie
>> value?
>> +                    if(null != _ignoreCookieName
>> +                            && _ignoreCookieName.equals(cookieName)
>> +                            && null != _ignoreCookieValue
>> +                            &&
_ignoreCookieValue.equals(cookie.getValue()))
>> {
>> +                        ignoreRebalance = true;
>> +                    }
>> +                }

Like here ^^^^^.

>> +            // Re-write the URI if it contains a ;jsessionid parameter
>> +            String uri = request.getRequestURI();
>> +            String sessionURIParamName = "jsessionid";
>> +            SessionConfig.getSessionUriParamName(request.getContext());
>>
>
> It seems this bug has been inroduced during testing/debugging.
> The return value of
> "SessionConfig.getSessionUriParamName(request.getContext());"
> is ignored and the sessionURIParamName is always "jsessionid".

+1

I'll get that fixed. This Valve is a port of a Filter that I wrote
earlier and evidently that got lost in the shuffle.

Thanks for the review.

-chris


Re: svn commit: r1799498 - in /tomcat/trunk: java/org/apache/catalina/valves/LoadBalancerDrainingValve.java test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java webapps/docs/changelog.xml webapps/docs/config/valve.xml

Posted by Martin Grigorov <mg...@apache.org>.
Hi Chris,


On Wed, Jun 21, 2017 at 9:05 PM, <sc...@apache.org> wrote:

> Author: schultz
> Date: Wed Jun 21 19:05:38 2017
> New Revision: 1799498
>
> URL: http://svn.apache.org/viewvc?rev=1799498&view=rev
> Log:
> Add LoadBalancerDrainingValve.
>
> Added:
>     tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java
>  (with props)
>     tomcat/trunk/test/org/apache/catalina/valves/
> TestLoadBalancerDrainingValve.java   (with props)
> Modified:
>     tomcat/trunk/webapps/docs/changelog.xml
>     tomcat/trunk/webapps/docs/config/valve.xml
>
> Added: tomcat/trunk/java/org/apache/catalina/valves/
> LoadBalancerDrainingValve.java
> URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/
> catalina/valves/LoadBalancerDrainingValve.java?rev=1799498&view=auto
> ============================================================
> ==================
> --- tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java
> (added)
> +++ tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java
> Wed Jun 21 19:05:38 2017
> @@ -0,0 +1,277 @@
> +/*
> + * 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.valves;
> +
> +import java.io.IOException;
> +
> +import javax.servlet.ServletException;
> +import javax.servlet.http.Cookie;
> +import javax.servlet.http.HttpServletResponse;
> +
> +import org.apache.catalina.LifecycleException;
> +import org.apache.catalina.connector.Request;
> +import org.apache.catalina.connector.Response;
> +import org.apache.catalina.util.SessionConfig;
> +import org.apache.juli.logging.Log;
> +
> +/**
> + * <p>A Valve to detect situations where a load-balanced node receiving a
> + * request has been deactivated by the load balancer
> (JK_LB_ACTIVATION=DIS)
> + * and the incoming request has no valid session.</p>
> + *
> + * <p>In these cases, the user's session cookie should be removed if it
> exists,
> + * any ";jsessionid" parameter should be removed from the request URI,
> + * and the client should be redirected to the same URI. This will cause
> the
> + * load-balanced to re-balance the client to another server.</p>
> + *
> + * <p>A request parameter is added to the redirect URI in order to avoid
> + * repeated redirects in the event of an error or misconfiguration.</p>
> + *
> + * <p>All this work is required because when the activation state of a
> node is
> + * DISABLED, the load-balancer will still send requests to the node if
> they
> + * appear to have a session on that node. Since mod_jk doesn't actually
> know
> + * whether the session id is valid, it will send the request blindly to
> + * the disabled node, which makes it take much longer to drain the node
> + * than strictly necessary.</p>
> + *
> + * <p>For testing purposes, a special cookie can be configured and used
> + * by a client to ignore the normal behavior of this Valve and allow
> + * a client to get a new session on a DISABLED node. See
> + * {@link #setIgnoreCookieName} and {@link #setIgnoreCookieValue}
> + * to configure those values.</p>
> + *
> + * <p>This Valve should be installed earlier in the Valve pipeline than
> any
> + * authentication valves, as the redirection should take place before an
> + * authentication valve would save a request to a protected resource.</p>
> + *
> + * @see http://tomcat.apache.org/connectors-doc/generic_howto/
> loadbalancers.html
> + */
> +public class LoadBalancerDrainingValve
> +    extends ValveBase
> +{
> +    /**
> +     * The request attribute key where the load-balancer's activation
> state
> +     * can be found.
> +     */
> +    static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION =
> "JK_LB_ACTIVATION";
>

Any objection to make this constant public and visible from outside ?
I find it useful to be able to refer the constant by name than its value
when integrating.


> +
> +    /**
> +     * The HTTP response code that will be used to redirect the request
> +     * back to the load-balancer for re-balancing. Defaults to 307
> +     * (TEMPORARY_REDIRECT).
> +     *
> +     * HTTP status code 305 (USE_PROXY) might be an option, here. too.
> +     */
> +    private int _redirectStatusCode = HttpServletResponse.SC_
> TEMPORARY_REDIRECT;
> +
> +    /**
> +     * The name of the cookie which can be set to ignore the "draining"
> action
> +     * of this Filter. This will allow a client to contact the server
> without
> +     * being re-balanced to another server. The expected cookie value can
> be set
> +     * in the {@link #_ignoreCookieValue}. The cookie name and value must
> match
> +     * to avoid being re-balanced.
> +     */
> +    private String _ignoreCookieName;
> +
> +    /**
> +     * The value of the cookie which can be set to ignore the "draining"
> action
> +     * of this Filter. This will allow a client to contact the server
> without
> +     * being re-balanced to another server. The expected cookie name can
> be set
> +     * in the {@link #_ignoreCookieValue}. The cookie name and value must
> match
> +     * to avoid being re-balanced.
> +     */
> +    private String _ignoreCookieValue;
> +
> +    /**
> +     * Local reference to the container log.
> +     */
> +    protected Log containerLog = null;
> +
> +    public LoadBalancerDrainingValve()
> +    {
> +        super(true); // Supports async
> +    }
> +
> +    //
> +    // Configuration parameters
> +    //
> +
> +    /**
> +     * Sets the HTTP response code that will be used to redirect the
> request
> +     * back to the load-balancer for re-balancing. Defaults to 307
> +     * (TEMPORARY_REDIRECT).
> +     */
> +    public void setRedirectStatusCode(int code) {
> +        _redirectStatusCode = code;
> +    }
> +
> +    /**
> +     * Gets the name of the cookie that can be used to override the
> +     * re-balancing behavior of this Valve when the current node is
> +     * in the DISABLED activation state.
> +     *
> +     * @return The cookie name used to ignore normal processing rules.
> +     *
> +     * @see #setIgnoreCookieValue
> +     */
> +    public String getIgnoreCookieName() {
> +        return _ignoreCookieName;
> +    }
> +
> +    /**
> +     * Sets the name of the cookie that can be used to override the
> +     * re-balancing behavior of this Valve when the current node is
> +     * in the DISABLED activation state.
> +     *
> +     * There is no default value for this setting: the ability to override
> +     * the re-balancing behavior of this Valve is <i>disabled</i> by
> default.
> +     *
> +     * @param cookieName The cookie name to use to ignore normal
> +     *                   processing rules.
> +     *
> +     * @see #getIgnoreCookieValue
> +     */
> +    public void setIgnoreCookieName(String cookieName) {
> +        _ignoreCookieName = cookieName;
> +    }
> +
> +    /**
> +     * Gets the expected value of the cookie that can be used to override
> the
> +     * re-balancing behavior of this Valve when the current node is
> +     * in the DISABLED activation state.
> +     *
> +     * @return The cookie value used to ignore normal processing rules.
> +     *
> +     * @see #setIgnoreCookieValue
> +     */
> +    public String getIgnoreCookieValue() {
> +        return _ignoreCookieValue;
> +    }
> +
> +    /**
> +     * Sets the expected value of the cookie that can be used to override
> the
> +     * re-balancing behavior of this Valve when the current node is
> +     * in the DISABLED activation state. The "ignore" cookie's value
> +     * <b>must</b> be exactly equal to this value in order to allow
> +     * the client to override the re-balancing behavior.
> +     *
> +     * @param cookieValue The cookie value to use to ignore normal
> +     *                    processing rules.
> +     *
> +     * @see #getIgnoreCookieValue
> +     */
> +    public void setIgnoreCookieValue(String cookieValue) {
> +        _ignoreCookieValue = cookieValue;
> +    }
> +
> +    @Override
> +    public void initInternal()
> +        throws LifecycleException
> +    {
> +        super.initInternal();
> +
> +        containerLog = getContainer().getLogger();
> +    }
> +
> +    @Override
> +    public void invoke(Request request, Response response) throws
> IOException, ServletException {
> +        if("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_
> LB_ACTIVATION))
> +           && !request.isRequestedSessionIdValid()) {
> +
> +            if(containerLog.isDebugEnabled())
> +                containerLog.debug("Load-balancer is in DISABLED state;
> draining this node");
> +
> +            boolean ignoreRebalance = false; // Allow certain clients
> +            Cookie sessionCookie = null;
> +
> +            // Kill any session cookie present
> +            final Cookie[] cookies = request.getCookies();
> +
> +            final String sessionCookieName = request.getServletContext().
> getSessionCookieConfig().getName();
> +
> +            // Kill any session cookie present
> +            if(null != cookies) {
> +                for(Cookie cookie : cookies) {
> +                    final String cookieName = cookie.getName();
> +                    if(containerLog.isTraceEnabled())
> +                        containerLog.trace("Checking cookie " +
> cookieName + "=" + cookie.getValue());
> +
> +                    if(sessionCookieName.equals(cookieName)
> +                       && request.getRequestedSessionId().equals(cookie.getValue()))
> {
> +                        sessionCookie = cookie;
>

Is it a good idea to 'break' here ?
Do you expect more than one session cookies ?!


> +                    } else
> +                    // Is the client presenting a valid ignore-cookie
> value?
> +                    if(null != _ignoreCookieName
> +                            && _ignoreCookieName.equals(cookieName)
> +                            && null != _ignoreCookieValue
> +                            && _ignoreCookieValue.equals(cookie.getValue()))
> {
> +                        ignoreRebalance = true;
> +                    }
> +                }
> +            }
> +
> +            if(ignoreRebalance) {
> +                if(containerLog.isDebugEnabled())
> +                    containerLog.debug("Client is presenting a valid " +
> _ignoreCookieName
> +                                 + " cookie, re-balancing is being
> skipped");
> +
> +                getNext().invoke(request, response);
> +
> +                return;
> +            }
> +
> +            // Kill any session cookie that was found
> +            // TODO: Consider implications of SSO cookies
> +            if(null != sessionCookie) {
> +                String cookiePath = request.getServletContext().
> getSessionCookieConfig().getPath();
> +
> +                if(request.getContext().getSessionCookiePathUsesTrailingSlash())
> {
> +                    // Handle special case of ROOT context where cookies
> require a path of
> +                    // '/' but the servlet spec uses an empty string
> +                    // Also ensure the cookies for a context with a path
> of /foo don't get
> +                    // sent for requests with a path of /foobar
> +                    if (!cookiePath.endsWith("/"))
> +                        cookiePath = cookiePath + "/";
> +
> +                    sessionCookie.setPath(cookiePath);
> +                    sessionCookie.setMaxAge(0); // Delete
> +                    sessionCookie.setValue(""); // Purge the cookie's
> value
> +                    response.addCookie(sessionCookie);
> +                }
> +            }
> +
> +            // Re-write the URI if it contains a ;jsessionid parameter
> +            String uri = request.getRequestURI();
> +            String sessionURIParamName = "jsessionid";
> +            SessionConfig.getSessionUriParamName(request.getContext());
>

It seems this bug has been inroduced during testing/debugging.
The return value of
"SessionConfig.getSessionUriParamName(request.getContext());"
is ignored and the sessionURIParamName is always "jsessionid".


> +            if(uri.contains(";" + sessionURIParamName + "="))
> +                uri = uri.replaceFirst(";" + sessionURIParamName +
> "=[^&?]*", "");
> +
> +            String queryString = request.getQueryString();
> +
> +            if(null != queryString)
> +                uri = uri + "?" + queryString;
> +
> +            // NOTE: Do not call response.encodeRedirectURL or the bad
> +            // sessionid will be restored
> +            response.setHeader("Location", uri);
> +            response.setStatus(_redirectStatusCode);
> +        }
> +        else
> +            getNext().invoke(request, response);
> +    }
> +}
>
> Propchange: tomcat/trunk/java/org/apache/catalina/valves/
> LoadBalancerDrainingValve.java
> ------------------------------------------------------------
> ------------------
>     svn:eol-style = native
>
> Added: tomcat/trunk/test/org/apache/catalina/valves/
> TestLoadBalancerDrainingValve.java
> URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/
> catalina/valves/TestLoadBalancerDrainingValve.java?rev=1799498&view=auto
> ============================================================
> ==================
> --- tomcat/trunk/test/org/apache/catalina/valves/
> TestLoadBalancerDrainingValve.java (added)
> +++ tomcat/trunk/test/org/apache/catalina/valves/
> TestLoadBalancerDrainingValve.java Wed Jun 21 19:05:38 2017
> @@ -0,0 +1,257 @@
> +/* 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.valves;
> +
> +import java.util.ArrayList;
> +import java.util.List;
> +
> +import javax.servlet.ServletContext;
> +import javax.servlet.SessionCookieConfig;
> +import javax.servlet.http.Cookie;
> +
> +import org.junit.Test;
> +
> +import org.apache.catalina.Context;
> +import org.apache.catalina.Valve;
> +import org.apache.catalina.connector.Request;
> +import org.apache.catalina.connector.Response;
> +import org.apache.catalina.core.StandardPipeline;
> +import org.easymock.EasyMock;
> +import org.easymock.IMocksControl;
> +
> +public class TestLoadBalancerDrainingValve {
> +
> +    static class MockResponse extends Response {
> +        private List<Cookie> cookies;
> +        @Override
> +        public boolean isCommitted() {
> +            return false;
> +        }
> +        @Override
> +        public void addCookie(Cookie cookie)
> +        {
> +            if(null == cookies)
> +                cookies = new ArrayList<Cookie>(1);
> +            cookies.add(cookie);
> +        }
> +        public List<Cookie> getCookies() {
> +            return cookies;
> +        }
> +    }
> +
> +    static class CookieConfig implements SessionCookieConfig {
> +
> +        private String name;
> +        private String domain;
> +        private String path;
> +        private String comment;
> +        private boolean httpOnly;
> +        private boolean secure;
> +        private int maxAge;
> +
> +        @Override
> +        public String getName() {
> +            return name;
> +        }
> +        @Override
> +        public void setName(String name) {
> +            this.name = name;
> +        }
> +        @Override
> +        public String getDomain() {
> +            return domain;
> +        }
> +        @Override
> +        public void setDomain(String domain) {
> +            this.domain = domain;
> +        }
> +        @Override
> +        public String getPath() {
> +            return path;
> +        }
> +        @Override
> +        public void setPath(String path) {
> +            this.path = path;
> +        }
> +        @Override
> +        public String getComment() {
> +            return comment;
> +        }
> +        @Override
> +        public void setComment(String comment) {
> +            this.comment = comment;
> +        }
> +        @Override
> +        public boolean isHttpOnly() {
> +            return httpOnly;
> +        }
> +        @Override
> +        public void setHttpOnly(boolean httpOnly) {
> +            this.httpOnly = httpOnly;
> +        }
> +        @Override
> +        public boolean isSecure() {
> +            return secure;
> +        }
> +        @Override
> +        public void setSecure(boolean secure) {
> +            this.secure = secure;
> +        }
> +        @Override
> +        public int getMaxAge() {
> +            return maxAge;
> +        }
> +        @Override
> +        public void setMaxAge(int maxAge) {
> +            this.maxAge = maxAge;
> +        }
> +    }
> +
> +    // A Cookie subclass that knows how to compare itself to other Cookie
> objects
> +    static class MyCookie extends Cookie {
> +      public MyCookie(String name, String value) { super(name, value); }
> +
> +      @Override
> +      public boolean equals(Object o) {
> +        if(null == o) return false;
> +        MyCookie mc = (MyCookie)o;
> +
> +        return mc.getName().equals(this.getName())
> +            && mc.getPath().equals(this.getPath())
> +            && mc.getValue().equals(this.getValue())
> +            && mc.getMaxAge() == this.getMaxAge();
> +      }
> +
> +      @Override
> +      public String toString() {
> +          return "Cookie { name=" + getName() + ", value=" + getValue() +
> ", path=" + getPath() + ", maxAge=" + getMaxAge() + " }";
> +      }
> +    }
> +
> +    @Test
> +    public void testNormalRequest() throws Exception {
> +        runValve("ACT", true, true, false, null);
> +    }
> +
> +    @Test
> +    public void testDisabledValidSession() throws Exception {
> +        runValve("DIS", true, true, false, null);
> +    }
> +
> +    @Test
> +    public void testDisabledInvalidSession() throws Exception {
> +        runValve("DIS", false, false, false, "foo=bar");
> +    }
> +
> +    @Test
> +    public void testDisabledInvalidSessionWithIgnore() throws Exception {
> +        runValve("DIS", false, true, true, "foo=bar");
> +    }
> +
> +    private void runValve(String jkActivation,
> +                          boolean validSessionId,
> +                          boolean expectInvokeNext,
> +                          boolean enableIgnore,
> +                          String queryString) throws Exception {
> +        IMocksControl control = EasyMock.createControl();
> +        ServletContext servletContext = control.createMock(
> ServletContext.class);
> +        Context ctx = control.createMock(Context.class);
> +        Request request = control.createMock(Request.class);
> +        Response response = control.createMock(Response.class);
> +
> +        String sessionCookieName = "JSESSIONID";
> +        String sessionId = "cafebabe";
> +        String requestURI = "/test/path";
> +        SessionCookieConfig cookieConfig = new CookieConfig();
> +        cookieConfig.setDomain("example.com");
> +        cookieConfig.setName(sessionCookieName);
> +        cookieConfig.setPath("/");
> +
> +        // Valve.init requires all of this stuff
> +        EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn("");
> +        EasyMock.expect(ctx.getName()).andStubReturn("");
> +        EasyMock.expect(ctx.getPipeline()).andStubReturn(new
> StandardPipeline());
> +        EasyMock.expect(ctx.getDomain()).andStubReturn("foo");
> +        EasyMock.expect(ctx.getLogger()).andStubReturn(org.apache.
> juli.logging.LogFactory.getLog(LoadBalancerDrainingValve.class));
> +        EasyMock.expect(ctx.getServletContext()).
> andStubReturn(servletContext);
> +
> +        // Set up the actual test
> +        EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.
> ATTRIBUTE_KEY_JK_LB_ACTIVATION)).andStubReturn(jkActivation);
> +        EasyMock.expect(request.isRequestedSessionIdValid()).
> andStubReturn(validSessionId);
> +
> +        ArrayList<Cookie> cookies = new ArrayList<Cookie>();
> +        if(enableIgnore) {
> +            cookies.add(new Cookie("ignore", "true"));
> +        }
> +
> +        if(!validSessionId) {
> +            MyCookie cookie = new MyCookie(cookieConfig.getName(),
> sessionId);
> +            cookie.setPath(cookieConfig.getPath());
> +            cookie.setValue(sessionId);
> +
> +            cookies.add(cookie);
> +
> +            EasyMock.expect(request.getRequestedSessionId()).
> andStubReturn(sessionId);
> +            EasyMock.expect(request.getRequestURI()).
> andStubReturn(requestURI);
> +            EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(new
> Cookie[cookies.size()]));
> +            EasyMock.expect(servletContext.getSessionCookieConfig()).
> andStubReturn(cookieConfig);
> +            EasyMock.expect(request.getServletContext()).
> andStubReturn(servletContext);
> +            EasyMock.expect(request.getContext()).andStubReturn(ctx);
> +            EasyMock.expect(ctx.getSessionCookiePathUsesTraili
> ngSlash()).andStubReturn(true);
> +            EasyMock.expect(servletContext.getSessionCookieConfig()).
> andStubReturn(cookieConfig);
> +            EasyMock.expect(request.getQueryString()).
> andStubReturn(queryString);
> +
> +           if(!enableIgnore) {
> +                // Response will have cookie deleted
> +                MyCookie expectedCookie = new
> MyCookie(cookieConfig.getName(), "");
> +                expectedCookie.setPath(cookieConfig.getPath());
> +                expectedCookie.setMaxAge(0);
> +
> +                // These two lines just mean EasyMock.expect(response.addCookie)
> but for a void method
> +                response.addCookie(expectedCookie);
> +                EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieName);
> // Indirect call
> +                String expectedRequestURI = requestURI;
> +                if(null != queryString)
> +                    expectedRequestURI = expectedRequestURI + '?' +
> queryString;
> +                response.setHeader("Location", expectedRequestURI);
> +                response.setStatus(307);
> +            }
> +        }
> +
> +        Valve next = control.createMock(Valve.class);
> +
> +        if(expectInvokeNext) {
> +            // Expect the "next" Valve to fire
> +            // Next 2 lines are basically EasyMock.expect(next.invoke(req,res))
> but for a void method
> +            next.invoke(request, response);
> +            EasyMock.expectLastCall();
> +        }
> +
> +        // Get set to actually test
> +        control.replay();
> +
> +        LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve();
> +        valve.setContainer(ctx);
> +        valve.init();
> +        valve.setNext(next);
> +        valve.setIgnoreCookieName("ignore");
> +        valve.setIgnoreCookieValue("true");
> +
> +        valve.invoke(request, response);
> +
> +        control.verify();
> +    }
> +}
>
> Propchange: tomcat/trunk/test/org/apache/catalina/valves/
> TestLoadBalancerDrainingValve.java
> ------------------------------------------------------------
> ------------------
>     svn:eol-style = native
>
> Modified: tomcat/trunk/webapps/docs/changelog.xml
> URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/
> changelog.xml?rev=1799498&r1=1799497&r2=1799498&view=diff
> ============================================================
> ==================
> --- tomcat/trunk/webapps/docs/changelog.xml (original)
> +++ tomcat/trunk/webapps/docs/changelog.xml Wed Jun 21 19:05:38 2017
> @@ -138,6 +138,10 @@
>          variable for CGI executables is populated in a consistent way
> regardless
>          of how the CGI servlet is mapped to a request. (markt)
>        </fix>
> +      <add>
> +        Add LoadBalancerDrainingValve, a Valve designed to reduce the
> amount of
> +        time required for a node to drain its authenticated users.
> (schultz)
> +      </add>
>      </changelog>
>    </subsection>
>    <subsection name="Coyote">
>
> Modified: tomcat/trunk/webapps/docs/config/valve.xml
> URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/
> config/valve.xml?rev=1799498&r1=1799497&r2=1799498&view=diff
> ============================================================
> ==================
> --- tomcat/trunk/webapps/docs/config/valve.xml (original)
> +++ tomcat/trunk/webapps/docs/config/valve.xml Wed Jun 21 19:05:38 2017
> @@ -700,6 +700,81 @@
>
>
>  <section name="Proxies Support">
> +  <subsection name="Load Balancer Draining Valve">
> +    <subsection name="Introduction">
> +      <p>
> +        When using mod_jk or mod_proxy_ajp, the client's session id is
> used to
> +        determine which back-end server will be used to serve the
> request. If the
> +        target node is being "drained" (in mod_jk, this is the
> <i>DISABLED</i>
> +        state; in mod_proxy_ajp, this is the <i>Drain (N)</i> state),
> requests
> +        for expired sessions can actually cause the draining node to fail
> to
> +        drain.
> +      </p>
> +      <p>
> +        Unfortunately, AJP-based load-balancers cannot prove whether the
> +        client-provided session id is valid or not and therefore will
> send any
> +        requests for a session that appears to be targeted to that node
> to the
> +        disabled (or "draining") node, causing the "draining" process to
> take
> +        longer than necessary.
> +      </p>
> +      <p>
> +        This Valve detects requests for invalid sessions, strips the
> session
> +        information from the request, and redirects back to the same URL,
> where
> +        the load-balancer should choose a different (active)  node to
> handle the
> +        request. This will accelerate the "draining" process for the
> disabled
> +        node(s).
> +      </p>
> +
> +      <p>
> +        The activation state of the node is sent by the load-balancer in
> the
> +        request, so no state change on the node being disabled is
> necessary. Simply
> +        configure this Valve in your valve pipeline and it will take
> action when
> +        the activation state is set to "disabled".
> +      </p>
> +
> +      <p>
> +        You should take care to register this Valve earlier in the Valve
> pipeline
> +        than any authentication Valves, because this Valve should be able
> to
> +        redirect a request before any authentication Valve saves a
> request to a
> +        protected resource. If this happens, a new session will be
> created and
> +        the draining process will stall because a new, valid session will
> be
> +        established.
> +      </p>
> +    </subsection><!-- / Introduction -->
> +
> +    <subsection name="Attributes">
> +      <p>The <strong>Load Balancer Draining Valve</strong> supports the
> +      following configuration attributes:</p>
> +
> +      <attributes>
> +        <attribute name="className" required="true">
> +          <p>Java class name of the implementation to use. This MUST be
> set to
> +          <strong>org.apache.catalina.valves.LoadBalancerDrainingValve</
> strong>.
> +          </p>
> +        </attribute>
> +
> +        <attribute name="redirectStatusCode" required="false">
> +          <p>Allows setting a custom redirect code to be used when the
> client
> +          is redirected to be re-balanced by the load-balancer. The
> default is
> +          307 TEMPORARY_REDIRECT.</p>
> +        </attribute>
> +
> +        <attribute name="ignoreCookieName" required="false">
> +          <p>When used with <code>ignoreCookieValue</code>, a client can
> present
> +          this cookie (and accompanying value) that will cause this Valve
> to
> +          do nothing. This will allow you to probe your <i>disabled</i>
> node
> +          before re-enabling it to make sure that it is working as
> expected.</p>
> +        </attribute>
> +
> +        <attribute name="ignoreCookieValue" required="false">
> +          <p>When used with <code>ignoreCookieName</code>, a client can
> present
> +          a cookie (and accompanying value) that will cause this Valve to
> +          do nothing. This will allow you to probe your <i>disabled</i>
> node
> +          before re-enabling it to make sure that it is working as
> expected.</p>
> +        </attribute>
> +      </attributes>
> +    </subsection><!-- /Attributes -->
> +  </subsection><!-- /Load Balancer Draining Valve -->
>
>  <subsection name="Remote IP Valve">
>
>
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
>
>

Re: svn commit: r1799498 - in /tomcat/trunk: java/org/apache/catalina/valves/LoadBalancerDrainingValve.java test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java webapps/docs/changelog.xml webapps/docs/config/valve.xml

Posted by Christopher Schultz <ch...@christopherschultz.net>.
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

All,

Any objections to me back-porting this (and r1799677 as well) at least
back to 8.0?

Thanks,
- -chris

On 6/21/17 3:05 PM, schultz@apache.org wrote:
> Author: schultz Date: Wed Jun 21 19:05:38 2017 New Revision:
> 1799498
> 
> URL: http://svn.apache.org/viewvc?rev=1799498&view=rev Log: Add
> LoadBalancerDrainingValve.
> 
> Added: 
> tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve
.java
> (with props) 
> tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV
alve.java
> (with props) Modified: tomcat/trunk/webapps/docs/changelog.xml 
> tomcat/trunk/webapps/docs/config/valve.xml
> 
> Added:
> tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve
.java
>
> 
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valve
s/LoadBalancerDrainingValve.java?rev=1799498&view=auto
> ======================================================================
========
>
> 
- ---
tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.j
ava
(added)
> +++
> tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve
.java
> Wed Jun 21 19:05:38 2017 @@ -0,0 +1,277 @@ +/* + * 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.valves; + +import
> java.io.IOException; + +import javax.servlet.ServletException; 
> +import javax.servlet.http.Cookie; +import
> javax.servlet.http.HttpServletResponse; + +import
> org.apache.catalina.LifecycleException; +import
> org.apache.catalina.connector.Request; +import
> org.apache.catalina.connector.Response; +import
> org.apache.catalina.util.SessionConfig; +import
> org.apache.juli.logging.Log; + +/** + * <p>A Valve to detect
> situations where a load-balanced node receiving a + * request has
> been deactivated by the load balancer (JK_LB_ACTIVATION=DIS) + *
> and the incoming request has no valid session.</p> + * + * <p>In
> these cases, the user's session cookie should be removed if it
> exists, + * any ";jsessionid" parameter should be removed from the
> request URI, + * and the client should be redirected to the same
> URI. This will cause the + * load-balanced to re-balance the client
> to another server.</p> + * + * <p>A request parameter is added to
> the redirect URI in order to avoid + * repeated redirects in the
> event of an error or misconfiguration.</p> + * + * <p>All this work
> is required because when the activation state of a node is + *
> DISABLED, the load-balancer will still send requests to the node if
> they + * appear to have a session on that node. Since mod_jk
> doesn't actually know + * whether the session id is valid, it will
> send the request blindly to + * the disabled node, which makes it
> take much longer to drain the node + * than strictly
> necessary.</p> + * + * <p>For testing purposes, a special cookie
> can be configured and used + * by a client to ignore the normal
> behavior of this Valve and allow + * a client to get a new session
> on a DISABLED node. See + * {@link #setIgnoreCookieName} and {@link
> #setIgnoreCookieValue} + * to configure those values.</p> + * + *
> <p>This Valve should be installed earlier in the Valve pipeline
> than any + * authentication valves, as the redirection should take
> place before an + * authentication valve would save a request to a
> protected resource.</p> + * + * @see
> http://tomcat.apache.org/connectors-doc/generic_howto/loadbalancers.ht
ml
>
> 
+ */
> +public class LoadBalancerDrainingValve +    extends ValveBase +{ +
> /** +     * The request attribute key where the load-balancer's
> activation state +     * can be found. +     */ +    static final
> String ATTRIBUTE_KEY_JK_LB_ACTIVATION = "JK_LB_ACTIVATION"; + +
> /** +     * The HTTP response code that will be used to redirect
> the request +     * back to the load-balancer for re-balancing.
> Defaults to 307 +     * (TEMPORARY_REDIRECT). +     * +     * HTTP
> status code 305 (USE_PROXY) might be an option, here. too. +
> */ +    private int _redirectStatusCode =
> HttpServletResponse.SC_TEMPORARY_REDIRECT; + +    /** +     * The
> name of the cookie which can be set to ignore the "draining"
> action +     * of this Filter. This will allow a client to contact
> the server without +     * being re-balanced to another server. The
> expected cookie value can be set +     * in the {@link
> #_ignoreCookieValue}. The cookie name and value must match +     *
> to avoid being re-balanced. +     */ +    private String
> _ignoreCookieName; + +    /** +     * The value of the cookie which
> can be set to ignore the "draining" action +     * of this Filter.
> This will allow a client to contact the server without +     *
> being re-balanced to another server. The expected cookie name can
> be set +     * in the {@link #_ignoreCookieValue}. The cookie name
> and value must match +     * to avoid being re-balanced. +     */ +
> private String _ignoreCookieValue; + +    /** +     * Local
> reference to the container log. +     */ +    protected Log
> containerLog = null; + +    public LoadBalancerDrainingValve() +
> { +        super(true); // Supports async +    } + +    // +    //
> Configuration parameters +    // + +    /** +     * Sets the HTTP
> response code that will be used to redirect the request +     *
> back to the load-balancer for re-balancing. Defaults to 307 +     *
> (TEMPORARY_REDIRECT). +     */ +    public void
> setRedirectStatusCode(int code) { +        _redirectStatusCode =
> code; +    } + +    /** +     * Gets the name of the cookie that
> can be used to override the +     * re-balancing behavior of this
> Valve when the current node is +     * in the DISABLED activation
> state. +     * +     * @return The cookie name used to ignore
> normal processing rules. +     * +     * @see
> #setIgnoreCookieValue +     */ +    public String
> getIgnoreCookieName() { +        return _ignoreCookieName; +    } 
> + +    /** +     * Sets the name of the cookie that can be used to
> override the +     * re-balancing behavior of this Valve when the
> current node is +     * in the DISABLED activation state. +     * +
> * There is no default value for this setting: the ability to
> override +     * the re-balancing behavior of this Valve is
> <i>disabled</i> by default. +     * +     * @param cookieName The
> cookie name to use to ignore normal +     *
> processing rules. +     * +     * @see #getIgnoreCookieValue +
> */ +    public void setIgnoreCookieName(String cookieName) { +
> _ignoreCookieName = cookieName; +    } + +    /** +     * Gets the
> expected value of the cookie that can be used to override the +
> * re-balancing behavior of this Valve when the current node is +
> * in the DISABLED activation state. +     * +     * @return The
> cookie value used to ignore normal processing rules. +     * +
> * @see #setIgnoreCookieValue +     */ +    public String
> getIgnoreCookieValue() { +        return _ignoreCookieValue; +
> } + +    /** +     * Sets the expected value of the cookie that can
> be used to override the +     * re-balancing behavior of this Valve
> when the current node is +     * in the DISABLED activation state.
> The "ignore" cookie's value +     * <b>must</b> be exactly equal to
> this value in order to allow +     * the client to override the
> re-balancing behavior. +     * +     * @param cookieValue The
> cookie value to use to ignore normal +     *
> processing rules. +     * +     * @see #getIgnoreCookieValue +
> */ +    public void setIgnoreCookieValue(String cookieValue) { +
> _ignoreCookieValue = cookieValue; +    } + +    @Override +
> public void initInternal() +        throws LifecycleException +
> { +        super.initInternal(); + +        containerLog =
> getContainer().getLogger(); +    } + +    @Override +    public
> void invoke(Request request, Response response) throws IOException,
> ServletException { +
> if("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_LB_ACTIVATION))
>
> 
+           && !request.isRequestedSessionIdValid()) {
> + +            if(containerLog.isDebugEnabled()) +
> containerLog.debug("Load-balancer is in DISABLED state; draining
> this node"); + +            boolean ignoreRebalance = false; //
> Allow certain clients +            Cookie sessionCookie = null; + +
> // Kill any session cookie present +            final Cookie[]
> cookies = request.getCookies(); + +            final String
> sessionCookieName =
> request.getServletContext().getSessionCookieConfig().getName(); + +
> // Kill any session cookie present +            if(null != cookies)
> { +                for(Cookie cookie : cookies) { +
> final String cookieName = cookie.getName(); +
> if(containerLog.isTraceEnabled()) +
> containerLog.trace("Checking cookie " + cookieName + "=" +
> cookie.getValue()); + +
> if(sessionCookieName.equals(cookieName) +                       &&
> request.getRequestedSessionId().equals(cookie.getValue())) { +
> sessionCookie = cookie; +                    } else +
> // Is the client presenting a valid ignore-cookie value? +
> if(null != _ignoreCookieName +                            &&
> _ignoreCookieName.equals(cookieName) +
> && null != _ignoreCookieValue +                            &&
> _ignoreCookieValue.equals(cookie.getValue())) { +
> ignoreRebalance = true; +                    } +                } +
> } + +            if(ignoreRebalance) { +
> if(containerLog.isDebugEnabled()) +
> containerLog.debug("Client is presenting a valid " +
> _ignoreCookieName +                                 + " cookie,
> re-balancing is being skipped"); + +
> getNext().invoke(request, response); + +                return; +
> } + +            // Kill any session cookie that was found +
> // TODO: Consider implications of SSO cookies +            if(null
> != sessionCookie) { +                String cookiePath =
> request.getServletContext().getSessionCookieConfig().getPath(); + +
> if(request.getContext().getSessionCookiePathUsesTrailingSlash()) { 
> +                    // Handle special case of ROOT context where
> cookies require a path of +                    // '/' but the
> servlet spec uses an empty string +                    // Also
> ensure the cookies for a context with a path of /foo don't get +
> // sent for requests with a path of /foobar +                    if
> (!cookiePath.endsWith("/")) +                        cookiePath =
> cookiePath + "/"; + +
> sessionCookie.setPath(cookiePath); +
> sessionCookie.setMaxAge(0); // Delete +
> sessionCookie.setValue(""); // Purge the cookie's value +
> response.addCookie(sessionCookie); +                } +
> } + +            // Re-write the URI if it contains a ;jsessionid
> parameter +            String uri = request.getRequestURI(); +
> String sessionURIParamName = "jsessionid"; +
> SessionConfig.getSessionUriParamName(request.getContext()); +
> if(uri.contains(";" + sessionURIParamName + "=")) +
> uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", ""); 
> + +            String queryString = request.getQueryString(); + +
> if(null != queryString) +                uri = uri + "?" +
> queryString; + +            // NOTE: Do not call
> response.encodeRedirectURL or the bad +            // sessionid
> will be restored +            response.setHeader("Location", uri); 
> +            response.setStatus(_redirectStatusCode); +        } +
> else +            getNext().invoke(request, response); +    } +}
> 
> Propchange:
> tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve
.java
>
> 
- ------------------------------------------------------------------------
- ------
> svn:eol-style = native
> 
> Added:
> tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV
alve.java
>
> 
URL:
http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/valve
s/TestLoadBalancerDrainingValve.java?rev=1799498&view=auto
> ======================================================================
========
>
> 
- ---
tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingVal
ve.java
(added)
> +++
> tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV
alve.java
> Wed Jun 21 19:05:38 2017 @@ -0,0 +1,257 @@ +/* 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.valves; + +import
> java.util.ArrayList; +import java.util.List; + +import
> javax.servlet.ServletContext; +import
> javax.servlet.SessionCookieConfig; +import
> javax.servlet.http.Cookie; + +import org.junit.Test; + +import
> org.apache.catalina.Context; +import org.apache.catalina.Valve; 
> +import org.apache.catalina.connector.Request; +import
> org.apache.catalina.connector.Response; +import
> org.apache.catalina.core.StandardPipeline; +import
> org.easymock.EasyMock; +import org.easymock.IMocksControl; + 
> +public class TestLoadBalancerDrainingValve { + +    static class
> MockResponse extends Response { +        private List<Cookie>
> cookies; +        @Override +        public boolean isCommitted()
> { +            return false; +        } +        @Override +
> public void addCookie(Cookie cookie) +        { +
> if(null == cookies) +                cookies = new
> ArrayList<Cookie>(1); +            cookies.add(cookie); +        } 
> +        public List<Cookie> getCookies() { +            return
> cookies; +        } +    } + +    static class CookieConfig
> implements SessionCookieConfig { + +        private String name; +
> private String domain; +        private String path; +
> private String comment; +        private boolean httpOnly; +
> private boolean secure; +        private int maxAge; + +
> @Override +        public String getName() { +            return
> name; +        } +        @Override +        public void
> setName(String name) { +            this.name = name; +        } +
> @Override +        public String getDomain() { +            return
> domain; +        } +        @Override +        public void
> setDomain(String domain) { +            this.domain = domain; +
> } +        @Override +        public String getPath() { +
> return path; +        } +        @Override +        public void
> setPath(String path) { +            this.path = path; +        } +
> @Override +        public String getComment() { +            return
> comment; +        } +        @Override +        public void
> setComment(String comment) { +            this.comment = comment; +
> } +        @Override +        public boolean isHttpOnly() { +
> return httpOnly; +        } +        @Override +        public void
> setHttpOnly(boolean httpOnly) { +            this.httpOnly =
> httpOnly; +        } +        @Override +        public boolean
> isSecure() { +            return secure; +        } +
> @Override +        public void setSecure(boolean secure) { +
> this.secure = secure; +        } +        @Override +        public
> int getMaxAge() { +            return maxAge; +        } +
> @Override +        public void setMaxAge(int maxAge) { +
> this.maxAge = maxAge; +        } +    } + +    // A Cookie subclass
> that knows how to compare itself to other Cookie objects +
> static class MyCookie extends Cookie { +      public
> MyCookie(String name, String value) { super(name, value); } + +
> @Override +      public boolean equals(Object o) { +        if(null
> == o) return false; +        MyCookie mc = (MyCookie)o; + +
> return mc.getName().equals(this.getName()) +            &&
> mc.getPath().equals(this.getPath()) +            &&
> mc.getValue().equals(this.getValue()) +            &&
> mc.getMaxAge() == this.getMaxAge(); +      } + +      @Override +
> public String toString() { +          return "Cookie { name=" +
> getName() + ", value=" + getValue() + ", path=" + getPath() + ",
> maxAge=" + getMaxAge() + " }"; +      } +    } + +    @Test +
> public void testNormalRequest() throws Exception { +
> runValve("ACT", true, true, false, null); +    } + +    @Test +
> public void testDisabledValidSession() throws Exception { +
> runValve("DIS", true, true, false, null); +    } + +    @Test +
> public void testDisabledInvalidSession() throws Exception { +
> runValve("DIS", false, false, false, "foo=bar"); +    } + +
> @Test +    public void testDisabledInvalidSessionWithIgnore()
> throws Exception { +        runValve("DIS", false, true, true,
> "foo=bar"); +    } + +    private void runValve(String
> jkActivation, +                          boolean validSessionId, +
> boolean expectInvokeNext, +                          boolean
> enableIgnore, +                          String queryString) throws
> Exception { +        IMocksControl control =
> EasyMock.createControl(); +        ServletContext servletContext =
> control.createMock(ServletContext.class); +        Context ctx =
> control.createMock(Context.class); +        Request request =
> control.createMock(Request.class); +        Response response =
> control.createMock(Response.class); + +        String
> sessionCookieName = "JSESSIONID"; +        String sessionId =
> "cafebabe"; +        String requestURI = "/test/path"; +
> SessionCookieConfig cookieConfig = new CookieConfig(); +
> cookieConfig.setDomain("example.com"); +
> cookieConfig.setName(sessionCookieName); +
> cookieConfig.setPath("/"); + +        // Valve.init requires all of
> this stuff +
> EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn(""); +
> EasyMock.expect(ctx.getName()).andStubReturn(""); +
> EasyMock.expect(ctx.getPipeline()).andStubReturn(new
> StandardPipeline()); +
> EasyMock.expect(ctx.getDomain()).andStubReturn("foo"); +
> EasyMock.expect(ctx.getLogger()).andStubReturn(org.apache.juli.logging
.LogFactory.getLog(LoadBalancerDrainingValve.class));
>
> 
+
EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext);
> + +        // Set up the actual test +
> EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBU
TE_KEY_JK_LB_ACTIVATION)).andStubReturn(jkActivation);
>
> 
+
EasyMock.expect(request.isRequestedSessionIdValid()).andStubReturn(valid
SessionId);
> + +        ArrayList<Cookie> cookies = new ArrayList<Cookie>(); +
> if(enableIgnore) { +            cookies.add(new Cookie("ignore",
> "true")); +        } + +        if(!validSessionId) { +
> MyCookie cookie = new MyCookie(cookieConfig.getName(), sessionId); 
> +            cookie.setPath(cookieConfig.getPath()); +
> cookie.setValue(sessionId); + +            cookies.add(cookie); + +
> EasyMock.expect(request.getRequestedSessionId()).andStubReturn(session
Id);
>
> 
+
EasyMock.expect(request.getRequestURI()).andStubReturn(requestURI);
> +
> EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(ne
w
> Cookie[cookies.size()])); +
> EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn
(cookieConfig);
>
> 
+
EasyMock.expect(request.getServletContext()).andStubReturn(servletContex
t);
> +
> EasyMock.expect(request.getContext()).andStubReturn(ctx); +
> EasyMock.expect(ctx.getSessionCookiePathUsesTrailingSlash()).andStubRe
turn(true);
>
> 
+
EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(c
ookieConfig);
> +
> EasyMock.expect(request.getQueryString()).andStubReturn(queryString);
>
> 
+
> +           if(!enableIgnore) { +                // Response will
> have cookie deleted +                MyCookie expectedCookie = new
> MyCookie(cookieConfig.getName(), ""); +
> expectedCookie.setPath(cookieConfig.getPath()); +
> expectedCookie.setMaxAge(0); + +                // These two lines
> just mean EasyMock.expect(response.addCookie) but for a void
> method +                response.addCookie(expectedCookie); +
> EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieNam
e);
> // Indirect call +                String expectedRequestURI =
> requestURI; +                if(null != queryString) +
> expectedRequestURI = expectedRequestURI + '?' + queryString; +
> response.setHeader("Location", expectedRequestURI); +
> response.setStatus(307); +            } +        } + +        Valve
> next = control.createMock(Valve.class); + +
> if(expectInvokeNext) { +            // Expect the "next" Valve to
> fire +            // Next 2 lines are basically
> EasyMock.expect(next.invoke(req,res)) but for a void method +
> next.invoke(request, response); +
> EasyMock.expectLastCall(); +        } + +        // Get set to
> actually test +        control.replay(); + +
> LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve(); 
> +        valve.setContainer(ctx); +        valve.init(); +
> valve.setNext(next); +        valve.setIgnoreCookieName("ignore"); 
> +        valve.setIgnoreCookieValue("true"); + +
> valve.invoke(request, response); + +        control.verify(); +
> } +}
> 
> Propchange:
> tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV
alve.java
>
> 
- ------------------------------------------------------------------------
- ------
> svn:eol-style = native
> 
> Modified: tomcat/trunk/webapps/docs/changelog.xml URL:
> http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?r
ev=1799498&r1=1799497&r2=1799498&view=diff
>
> 
========================================================================
======
> --- tomcat/trunk/webapps/docs/changelog.xml (original) +++
> tomcat/trunk/webapps/docs/changelog.xml Wed Jun 21 19:05:38 2017 @@
> -138,6 +138,10 @@ variable for CGI executables is populated in a
> consistent way regardless of how the CGI servlet is mapped to a
> request. (markt) </fix> +      <add> +        Add
> LoadBalancerDrainingValve, a Valve designed to reduce the amount
> of +        time required for a node to drain its authenticated
> users. (schultz) +      </add> </changelog> </subsection> 
> <subsection name="Coyote">
> 
> Modified: tomcat/trunk/webapps/docs/config/valve.xml URL:
> http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xm
l?rev=1799498&r1=1799497&r2=1799498&view=diff
>
> 
========================================================================
======
> --- tomcat/trunk/webapps/docs/config/valve.xml (original) +++
> tomcat/trunk/webapps/docs/config/valve.xml Wed Jun 21 19:05:38
> 2017 @@ -700,6 +700,81 @@
> 
> 
> <section name="Proxies Support"> +  <subsection name="Load Balancer
> Draining Valve"> +    <subsection name="Introduction"> +      <p> +
> When using mod_jk or mod_proxy_ajp, the client's session id is used
> to +        determine which back-end server will be used to serve
> the request. If the +        target node is being "drained" (in
> mod_jk, this is the <i>DISABLED</i> +        state; in
> mod_proxy_ajp, this is the <i>Drain (N)</i> state), requests +
> for expired sessions can actually cause the draining node to fail
> to +        drain. +      </p> +      <p> +        Unfortunately,
> AJP-based load-balancers cannot prove whether the +
> client-provided session id is valid or not and therefore will send
> any +        requests for a session that appears to be targeted to
> that node to the +        disabled (or "draining") node, causing
> the "draining" process to take +        longer than necessary. +
> </p> +      <p> +        This Valve detects requests for invalid
> sessions, strips the session +        information from the request,
> and redirects back to the same URL, where +        the
> load-balancer should choose a different (active)  node to handle
> the +        request. This will accelerate the "draining" process
> for the disabled +        node(s). +      </p> + +      <p> +
> The activation state of the node is sent by the load-balancer in
> the +        request, so no state change on the node being disabled
> is necessary. Simply +        configure this Valve in your valve
> pipeline and it will take action when +        the activation state
> is set to "disabled". +      </p> + +      <p> +        You should
> take care to register this Valve earlier in the Valve pipeline +
> than any authentication Valves, because this Valve should be able
> to +        redirect a request before any authentication Valve
> saves a request to a +        protected resource. If this happens,
> a new session will be created and +        the draining process
> will stall because a new, valid session will be +
> established. +      </p> +    </subsection><!-- / Introduction --> 
> + +    <subsection name="Attributes"> +      <p>The <strong>Load
> Balancer Draining Valve</strong> supports the +      following
> configuration attributes:</p> + +      <attributes> +
> <attribute name="className" required="true"> +          <p>Java
> class name of the implementation to use. This MUST be set to +
> <strong>org.apache.catalina.valves.LoadBalancerDrainingValve</strong>.
>
> 
+          </p>
> +        </attribute> + +        <attribute
> name="redirectStatusCode" required="false"> +          <p>Allows
> setting a custom redirect code to be used when the client +
> is redirected to be re-balanced by the load-balancer. The default
> is +          307 TEMPORARY_REDIRECT.</p> +        </attribute> + +
> <attribute name="ignoreCookieName" required="false"> +
> <p>When used with <code>ignoreCookieValue</code>, a client can
> present +          this cookie (and accompanying value) that will
> cause this Valve to +          do nothing. This will allow you to
> probe your <i>disabled</i> node +          before re-enabling it to
> make sure that it is working as expected.</p> +
> </attribute> + +        <attribute name="ignoreCookieValue"
> required="false"> +          <p>When used with
> <code>ignoreCookieName</code>, a client can present +          a
> cookie (and accompanying value) that will cause this Valve to +
> do nothing. This will allow you to probe your <i>disabled</i> node 
> +          before re-enabling it to make sure that it is working as
> expected.</p> +        </attribute> +      </attributes> +
> </subsection><!-- /Attributes --> +  </subsection><!-- /Load
> Balancer Draining Valve -->
> 
> <subsection name="Remote IP Valve">
> 
> 
> 
> 
> ---------------------------------------------------------------------
>
> 
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
> For additional commands, e-mail: dev-help@tomcat.apache.org
> 
-----BEGIN PGP SIGNATURE-----
Comment: GPGTools - http://gpgtools.org
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iQIcBAEBCAAGBQJZUAAeAAoJEBzwKT+lPKRYdlcP/Avpl70k41yWB64rqlwbBwrB
ISxrnU3YLAOJXu8Lc0+/QRIhl2VXKt73ETblCPmVLYluPfu4MLjFvTouX9ZElL6r
UX36tNPTiWCaiocQ9P7jWLHvBdeX48ck+MbC8EE3bKfJf1MQvXL62RuIfuLnoj6P
r1/SsKXu63Y1CekWm68TlKPVqIhqk0sEzSG7X12w5QxVCOuDh2KA68Glkue86I5F
z6mS+eTetF8+inRbiB8EXBrCxThwpq7NACBOcBlOVOlGZHF7JWj/V5djBDDFRuKv
pFO5kbOeDaG5ruGNgD9UDvzlIikLIncLGFr01kYDnbIlfhf6sNJLtbd6m8dE2hAp
UYciBxj7RDjaCLhW6ltLlldP8kGdfKiUWkH6SELH6FVPlSvXp+7RqZoY2cqCFYEV
Xy1/QXAWbTlUPbLiC6z9YCHI0i9vMQL8JH3bfKVX9qIM6mGLpgZsCiRu+CD2PK0j
XXCb+F/X0Qq6v1sJFUGab8JYAOQz8pXNYjGde5xtHX6TUpnz8pgKsOe2wuxH0LP4
7oRXVDKYs57XjwPF7jZzEkE72JCYu2jgmLSk+1CXMmDTfs2/RPFaY8/rDpoWv67I
ZB6R+4zFBmL13DWRREHKWzZ3qUuCPy5+Pyzso4olrL3Gw/vvXt63Gatrg+hXm6RS
LAPDvxZ3I5pO6ceuNnle
=GX3V
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org