Fix BZ 64080 - graceful close
https://bz.apache.org/bugzilla/show_bug.cgi?id=64080
diff --git a/java/org/apache/catalina/core/StandardService.java b/java/org/apache/catalina/core/StandardService.java
index 05965ca..5597cb0 100644
--- a/java/org/apache/catalina/core/StandardService.java
+++ b/java/org/apache/catalina/core/StandardService.java
@@ -105,8 +105,21 @@
protected final MapperListener mapperListener = new MapperListener(this);
+ private long gracefulStopAwaitMillis = 0;
+
+
// ------------------------------------------------------------- Properties
+ public long getGracefulStopAwaitMillis() {
+ return gracefulStopAwaitMillis;
+ }
+
+
+ public void setGracefulStopAwaitMillis(long gracefulStopAwaitMillis) {
+ this.gracefulStopAwaitMillis = gracefulStopAwaitMillis;
+ }
+
+
@Override
public Mapper getMapper() {
return mapper;
@@ -453,13 +466,25 @@
@Override
protected void stopInternal() throws LifecycleException {
- // Pause connectors first
synchronized (connectorsLock) {
+ // Initiate a graceful stop for each connector
+ // This will only work if the bindOnInit==false which is not the
+ // default.
+ for (Connector connector: connectors) {
+ connector.getProtocolHandler().closeServerSocketGraceful();
+ }
+
+ // Wait for the graceful shutdown to complete
+ long waitMillis = gracefulStopAwaitMillis;
+ if (waitMillis > 0) {
+ for (Connector connector: connectors) {
+ waitMillis = connector.getProtocolHandler().awaitConnectionsClose(waitMillis);
+ }
+ }
+
+ // Pause the connectors
for (Connector connector: connectors) {
connector.pause();
- // Close server socket if bound on start
- // Note: test is in AbstractEndpoint
- connector.getProtocolHandler().closeServerSocketGraceful();
}
}
@@ -467,7 +492,7 @@
log.info(sm.getString("standardService.stop.name", this.name));
setState(LifecycleState.STOPPING);
- // Stop our defined Container second
+ // Stop our defined Container once the Connectors are all paused
if (engine != null) {
synchronized (engine) {
engine.stop();
diff --git a/java/org/apache/coyote/AbstractProtocol.java b/java/org/apache/coyote/AbstractProtocol.java
index e6fbaed..921d78b 100644
--- a/java/org/apache/coyote/AbstractProtocol.java
+++ b/java/org/apache/coyote/AbstractProtocol.java
@@ -711,6 +711,14 @@
}
+ @Override
+ public long awaitConnectionsClose(long waitMillis) {
+ getLog().info(sm.getString("abstractProtocol.closeConnectionsAwait",
+ Long.valueOf(waitMillis), getName()));
+ return endpoint.awaitConnectionsClose(waitMillis);
+ }
+
+
private void logPortOffset() {
if (getPort() != getPortWithOffset()) {
getLog().info(sm.getString("abstractProtocolHandler.portOffset", getName(),
diff --git a/java/org/apache/coyote/LocalStrings.properties b/java/org/apache/coyote/LocalStrings.properties
index 3009a36..595cfb2 100644
--- a/java/org/apache/coyote/LocalStrings.properties
+++ b/java/org/apache/coyote/LocalStrings.properties
@@ -34,6 +34,7 @@
abstractProcessor.setErrorState=Error state [{0}] reported while processing request
abstractProcessor.socket.ssl=Exception getting SSL attributes
+abstractProtocol.closeConnectionsAwait=Waiting [{0}] milliseconds for existing connections to [{1}] to complete and close.
abstractProtocol.mbeanDeregistrationFailed=Failed to deregister MBean named [{0}] from MBean server [{1}]
abstractProtocol.processorRegisterError=Error registering request processor
abstractProtocol.processorUnregisterError=Error unregistering request processor
diff --git a/java/org/apache/coyote/ProtocolHandler.java b/java/org/apache/coyote/ProtocolHandler.java
index c6e1565..dfa5a25 100644
--- a/java/org/apache/coyote/ProtocolHandler.java
+++ b/java/org/apache/coyote/ProtocolHandler.java
@@ -136,6 +136,19 @@
/**
+ * Wait for the client connections to the server to close gracefully. The
+ * method will return when all of the client connections have closed or the
+ * method has been waiting for {@code waitTimeMillis}.
+ *
+ * @param waitMillis The maximum time to wait in milliseconds for the
+ * client connections to close.
+ *
+ * @return The wait time, if any remaining when the method returned
+ */
+ public long awaitConnectionsClose(long waitMillis);
+
+
+ /**
* Requires APR/native library
*
* @return <code>true</code> if this Protocol Handler requires the
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index 23a2c16..1dcdb2a 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -124,7 +124,20 @@
}
protected enum BindState {
- UNBOUND, BOUND_ON_INIT, BOUND_ON_START, SOCKET_CLOSED_ON_STOP
+ UNBOUND(false),
+ BOUND_ON_INIT(true),
+ BOUND_ON_START(true),
+ SOCKET_CLOSED_ON_STOP(false);
+
+ private final boolean bound;
+
+ private BindState(boolean bound) {
+ this.bound = bound;
+ }
+
+ public boolean isBound() {
+ return bound;
+ }
}
@@ -709,7 +722,12 @@
*/
private int maxKeepAliveRequests=100; // as in Apache HTTPD server
public int getMaxKeepAliveRequests() {
- return maxKeepAliveRequests;
+ // Disable keep-alive if the server socket is not bound
+ if (bindState.isBound()) {
+ return maxKeepAliveRequests;
+ } else {
+ return 1;
+ }
}
public void setMaxKeepAliveRequests(int maxKeepAliveRequests) {
this.maxKeepAliveRequests = maxKeepAliveRequests;
@@ -1302,6 +1320,16 @@
*/
public final void closeServerSocketGraceful() {
if (bindState == BindState.BOUND_ON_START) {
+ // Stop accepting new connections
+ acceptor.stop(-1);
+ // Release locks that may be preventing the acceptor from stopping
+ releaseConnectionLatch();
+ unlockAccept();
+ // Signal to any multiplexed protocols (HTTP/2) that they may wish
+ // to stop accepting new streams
+ getHandler().pause();
+ // Update the bindState. This has the side-effect of disabling
+ // keep-alive for any in-progress connections
bindState = BindState.SOCKET_CLOSED_ON_STOP;
try {
doCloseServerSocket();
@@ -1313,6 +1341,30 @@
/**
+ * Wait for the client connections to the server to close gracefully. The
+ * method will return when all of the client connections have closed or the
+ * method has been waiting for {@code waitTimeMillis}.
+ *
+ * @param waitMillis The maximum time to wait in milliseconds for the
+ * client connections to close.
+ *
+ * @return The wait time, if any remaining when the method returned
+ */
+ public final long awaitConnectionsClose(long waitMillis) {
+ while (waitMillis > 0 && !connections.isEmpty()) {
+ try {
+ Thread.sleep(50);
+ waitMillis -= 50;
+ } catch (InterruptedException e) {
+ Thread.interrupted();
+ waitMillis = 0;
+ }
+ }
+ return waitMillis;
+ }
+
+
+ /**
* Actually close the server socket but don't perform any other clean-up.
*
* @throws IOException If an error occurs closing the socket
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 2251111..5603d10 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -106,6 +106,13 @@
<section name="Tomcat 10.0.0-M11 (markt)" rtext="in development">
<subsection name="Catalina">
<changelog>
+ <add>
+ <bug>64080</bug>: Enhance the graceful shutdown feature. Includes a new
+ option for <code>StandardService</code>,
+ <code>gracefulStopAwaitMillis</code>, that allows a time to be
+ specified to wait for client connections to complete and close before
+ the Container hierarchy is stopped. (markt)
+ </add>
<fix>
<bug>64921</bug>: Ensure that the <code>LoadBalancerDrainingValve</code>
uses the correct setting for the secure attribute for any session
diff --git a/webapps/docs/config/service.xml b/webapps/docs/config/service.xml
index bb8d94d..5fd0acd 100644
--- a/webapps/docs/config/service.xml
+++ b/webapps/docs/config/service.xml
@@ -80,6 +80,16 @@
common attributes listed above):</p>
<attributes>
+
+ <attribute name="gracefulStopAwaitMillis" required="false">
+ <p>The time to wait, in milliseconds, when stopping the Service for the
+ client connections to finish processing and close before the Service's
+ container hierarchy is stopped. The wait only applies to Connectors
+ configured with a <code>bindOnInit</code> value of <code>false</code>.
+ Any value of zero or less means there will be no wait. If not specified,
+ the default value of zero will be used.</p>
+ </attribute>
+
</attributes>
</subsection>