Hi Chris,
On Wed, Jun 21, 2017 at 9:05 PM, <schu...@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-unsubscr...@tomcat.apache.org > For additional commands, e-mail: dev-h...@tomcat.apache.org > >