diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 12773a79ea..5ad0b071be 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -100,6 +100,11 @@ public class ServerProperties { */ private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8); + /** + * Type of shutdown that the server will support. + */ + private Shutdown shutdown = Shutdown.IMMEDIATE; + @NestedConfigurationProperty private Ssl ssl; @@ -109,9 +114,6 @@ public class ServerProperties { @NestedConfigurationProperty private final Http2 http2 = new Http2(); - @NestedConfigurationProperty - private final Shutdown shutdown = new Shutdown(); - private final Servlet servlet = new Servlet(); private final Tomcat tomcat = new Tomcat(); @@ -154,6 +156,14 @@ public class ServerProperties { this.maxHttpHeaderSize = maxHttpHeaderSize; } + public Shutdown getShutdown() { + return this.shutdown; + } + + public void setShutdown(Shutdown shutdown) { + this.shutdown = shutdown; + } + public ErrorProperties getError() { return this.error; } @@ -174,10 +184,6 @@ public class ServerProperties { return this.http2; } - public Shutdown getShutdown() { - return this.shutdown; - } - public Servlet getServlet() { return this.servlet; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 7dcdd36fd4..dca86a542b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -266,6 +266,9 @@ "name": "server.ssl.trust-store-type", "description": "Type of the trust store." }, + { "name": "server.shutdown", + "defaultValue:": "immediate" + }, { "name": "server.tomcat.max-http-post-size", "type": "org.springframework.util.unit.DataSize", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java index c806b58ca9..dba8b407f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.web.reactive; import java.net.InetAddress; -import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -76,13 +75,13 @@ class ReactiveWebServerFactoryCustomizerTests { } @Test - void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() { - this.properties.getShutdown().setGracePeriod(Duration.ofSeconds(30)); + void whenShutdownPropertyIsSetThenShutdownIsCustomized() { + this.properties.setShutdown(Shutdown.GRACEFUL); ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class); this.customizer.customize(factory); ArgumentCaptor shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class); verify(factory).setShutdown(shutdownCaptor.capture()); - assertThat(shutdownCaptor.getValue().getGracePeriod()).isEqualTo(Duration.ofSeconds(30)); + assertThat(shutdownCaptor.getValue()).isEqualTo(Shutdown.GRACEFUL); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java index 92360864c3..fe871eabc9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.io.File; -import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -165,15 +164,15 @@ class ServletWebServerFactoryCustomizerTests { } @Test - void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() { + void whenShutdownPropertyIsSetThenShutdownIsCustomized() { Map map = new HashMap<>(); - map.put("server.shutdown.grace-period", "30s"); + map.put("server.shutdown", "graceful"); bindProperties(map); ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class); this.customizer.customize(factory); ArgumentCaptor shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class); verify(factory).setShutdown(shutdownCaptor.capture()); - assertThat(shutdownCaptor.getValue().getGracePeriod()).isEqualTo(Duration.ofSeconds(30)); + assertThat(shutdownCaptor.getValue()).isEqualTo(Shutdown.GRACEFUL); } private void bindProperties(Map map) { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index c45ab8f1a6..efd1a39fce 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -3145,21 +3145,19 @@ You can learn more about the resource configuration on the client side in the << [[boot-features-graceful-shutdown]] == Graceful shutdown Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. -When enabled, shutdown of the application will include a grace period of configurable duration. -During this grace period, existing requests will be allowed to complete but no new requests will be permitted. +It occurs as part of closing the application context and is performed in the earliest phase of stopping `SmartLifecycle` beans. +This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted. The exact way in which new requests are not permitted varies depending on the web server that is being used. Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer. Undertow will accept requests but respond immediately with a service unavailable (503) response. NOTE: Graceful shutdown with Tomcat requires Tomcat 9.0.33 or later. -Graceful shutdown occurs as one of the first steps during application close processing and before any beans have been destroyed. -This ensures that the beans are available for use by any processing that occurs while in-flight requests are being allowed to complete. -To enable graceful shutdown, configure the configprop:server.shutdown.grace-period[] property, as shown in the following example: +To enable graceful shutdown, configure the configprop:server.shutdown[] property, as shown in the following example: [source,properties,indent=0,configprops] ---- -server.shutdown.grace-period=30s +server.shutdown=graceful ---- diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyGracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyGracefulShutdown.java deleted file mode 100644 index f5eb4cafbb..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyGracefulShutdown.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed 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 - * - * https://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.springframework.boot.web.embedded.jetty; - -import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.function.Supplier; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; - -import org.springframework.boot.web.server.GracefulShutdown; - -/** - * {@link GracefulShutdown} for Jetty. - * - * @author Andy Wilkinson - */ -class JettyGracefulShutdown implements GracefulShutdown { - - private static final Log logger = LogFactory.getLog(JettyGracefulShutdown.class); - - private final Server server; - - private final Supplier activeRequests; - - private final Duration period; - - private volatile boolean shuttingDown = false; - - JettyGracefulShutdown(Server server, Supplier activeRequests, Duration period) { - this.server = server; - this.activeRequests = activeRequests; - this.period = period; - } - - @Override - public boolean shutDownGracefully() { - logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds() - + "s for active requests to complete"); - for (Connector connector : this.server.getConnectors()) { - shutdown(connector); - } - this.shuttingDown = true; - long end = System.currentTimeMillis() + this.period.toMillis(); - while (System.currentTimeMillis() < end && (this.activeRequests.get() > 0)) { - try { - Thread.sleep(100); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - this.shuttingDown = false; - long activeRequests = this.activeRequests.get(); - if (activeRequests == 0) { - logger.info("Graceful shutdown complete"); - return true; - } - if (logger.isInfoEnabled()) { - logger.info("Grace period elapsed with " + activeRequests + " request(s) still active"); - } - return activeRequests == 0; - } - - private void shutdown(Connector connector) { - try { - connector.shutdown().get(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - catch (ExecutionException ex) { - // Continue - } - } - - @Override - public boolean isShuttingDown() { - return this.shuttingDown; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 58485d543e..fb195d6c55 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -32,12 +32,14 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.thread.ThreadPool; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; @@ -103,7 +105,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact public WebServer getWebServer(HttpHandler httpHandler) { JettyHttpHandlerAdapter servlet = new JettyHttpHandlerAdapter(httpHandler); Server server = createJettyServer(servlet); - return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod()); + return new JettyWebServer(server, getPort() >= 0); } @Override @@ -182,6 +184,11 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact if (this.useForwardHeaders) { new ForwardHeadersCustomizer().customize(server); } + if (getShutdown() == Shutdown.GRACEFUL) { + StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.setHandler(server.getHandler()); + server.setHandler(statisticsHandler); + } return server; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 2f457f1b4e..fde5218367 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -41,6 +41,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.session.DefaultSessionCache; import org.eclipse.jetty.server.session.FileSessionDataStore; import org.eclipse.jetty.server.session.SessionHandler; @@ -57,6 +58,7 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; @@ -152,6 +154,11 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor if (this.useForwardHeaders) { new ForwardHeadersCustomizer().customize(server); } + if (getShutdown() == Shutdown.GRACEFUL) { + StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.setHandler(server.getHandler()); + server.setHandler(statisticsHandler); + } return getJettyWebServer(server); } @@ -398,7 +405,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor * @return a new {@link JettyWebServer} instance */ protected JettyWebServer getJettyWebServer(Server server) { - return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod()); + return new JettyWebServer(server, getPort() >= 0); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index f972d63b4b..2baa2671db 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -17,10 +17,11 @@ package org.springframework.boot.web.embedded.jetty; import java.io.IOException; -import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -35,7 +36,8 @@ import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.util.component.AbstractLifeCycle; -import org.springframework.boot.web.server.GracefulShutdown; +import org.springframework.boot.web.server.GracefulShutdownCallback; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; @@ -85,32 +87,33 @@ public class JettyWebServer implements WebServer { * @param autoStart if auto-starting the server */ public JettyWebServer(Server server, boolean autoStart) { - this(server, autoStart, null); - } - - /** - * Create a new {@link JettyWebServer} instance. - * @param server the underlying Jetty server - * @param autoStart if auto-starting the server - * @param shutdownGracePeriod grace period to use when shutting down - * @since 2.3.0 - */ - public JettyWebServer(Server server, boolean autoStart, Duration shutdownGracePeriod) { this.autoStart = autoStart; Assert.notNull(server, "Jetty Server must not be null"); this.server = server; - this.gracefulShutdown = createGracefulShutdown(server, shutdownGracePeriod); + this.gracefulShutdown = createGracefulShutdown(server); initialize(); } - private GracefulShutdown createGracefulShutdown(Server server, Duration shutdownGracePeriod) { - if (shutdownGracePeriod == null) { - return GracefulShutdown.IMMEDIATE; + private GracefulShutdown createGracefulShutdown(Server server) { + StatisticsHandler statisticsHandler = findStatisticsHandler(server); + if (statisticsHandler == null) { + return null; } - StatisticsHandler handler = new StatisticsHandler(); - handler.setHandler(server.getHandler()); - server.setHandler(handler); - return new JettyGracefulShutdown(server, handler::getRequestsActive, shutdownGracePeriod); + return new GracefulShutdown(server, statisticsHandler::getRequestsActive); + } + + private StatisticsHandler findStatisticsHandler(Server server) { + return findStatisticsHandler(server.getHandler()); + } + + private StatisticsHandler findStatisticsHandler(Handler handler) { + if (handler instanceof StatisticsHandler) { + return (StatisticsHandler) handler; + } + if (handler instanceof HandlerWrapper) { + return findStatisticsHandler(((HandlerWrapper) handler).getHandler()); + } + return null; } private void initialize() { @@ -256,6 +259,9 @@ public class JettyWebServer implements WebServer { public void stop() { synchronized (this.monitor) { this.started = false; + if (this.gracefulShutdown != null) { + this.gracefulShutdown.abort(); + } try { this.server.stop(); } @@ -279,12 +285,13 @@ public class JettyWebServer implements WebServer { } @Override - public boolean shutDownGracefully() { - return this.gracefulShutdown.shutDownGracefully(); - } - - boolean inGracefulShutdown() { - return this.gracefulShutdown.isShuttingDown(); + public void shutDownGracefully(GracefulShutdownCallback callback) { + if (this.gracefulShutdown != null) { + this.gracefulShutdown.shutDownGracefully(callback); + } + else { + callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); + } } /** @@ -295,4 +302,68 @@ public class JettyWebServer implements WebServer { return this.server; } + static final class GracefulShutdown { + + private static final Log logger = LogFactory.getLog(GracefulShutdown.class); + + private final Server server; + + private final Supplier activeRequests; + + private volatile boolean shuttingDown = false; + + private GracefulShutdown(Server server, Supplier activeRequests) { + this.server = server; + this.activeRequests = activeRequests; + } + + private void shutDownGracefully(GracefulShutdownCallback callback) { + logger.info("Commencing graceful shutdown. Waiting for active requests to complete"); + for (Connector connector : this.server.getConnectors()) { + shutdown(connector); + } + this.shuttingDown = true; + new Thread(() -> { + while (this.shuttingDown && this.activeRequests.get() > 0) { + try { + Thread.sleep(100); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + this.shuttingDown = false; + long activeRequests = this.activeRequests.get(); + if (activeRequests == 0) { + logger.info("Graceful shutdown complete"); + callback.shutdownComplete(GracefulShutdownResult.IDLE); + } + else { + if (logger.isInfoEnabled()) { + logger.info("Graceful shutdown aborted with " + activeRequests + " request(s) still active"); + } + callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); + } + }, "jetty-shutdown").start(); + + } + + private void shutdown(Connector connector) { + try { + connector.shutdown().get(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (ExecutionException ex) { + // Continue + } + } + + private void abort() { + this.shuttingDown = false; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyGracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyGracefulShutdown.java deleted file mode 100644 index 60c250e811..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyGracefulShutdown.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed 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 - * - * https://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.springframework.boot.web.embedded.netty; - -import java.time.Duration; -import java.util.function.Supplier; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.netty.DisposableServer; - -import org.springframework.boot.web.server.GracefulShutdown; - -/** - * {@link GracefulShutdown} for a Reactor Netty {@link DisposableServer}. - * - * @author Andy Wilkinson - */ -final class NettyGracefulShutdown implements GracefulShutdown { - - private static final Log logger = LogFactory.getLog(NettyGracefulShutdown.class); - - private final Supplier disposableServer; - - private final Duration period; - - private volatile boolean shuttingDown; - - NettyGracefulShutdown(Supplier disposableServer, Duration period) { - this.disposableServer = disposableServer; - this.period = period; - } - - @Override - public boolean shutDownGracefully() { - logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds() - + "s for active requests to complete"); - DisposableServer server = this.disposableServer.get(); - if (server == null) { - return false; - } - this.shuttingDown = true; - try { - disposeNow(server); - logger.info("Graceful shutdown complete"); - return true; - } - catch (IllegalStateException ex) { - logger.info("Grace period elapsed with one ore more requests still active"); - return false; - } - finally { - this.shuttingDown = false; - } - } - - private void disposeNow(DisposableServer server) { - if (this.period != null) { - server.disposeNow(this.period); - } - else { - server.disposeNow(); - } - } - - @Override - public boolean isShuttingDown() { - return this.shuttingDown; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 9b0b759be3..0bd1dfea7e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -69,8 +69,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact public WebServer getWebServer(HttpHandler httpHandler) { HttpServer httpServer = createHttpServer(); ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler); - NettyWebServer webServer = new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout, - (this.shutdown == null) ? null : this.shutdown.getGracePeriod()); + NettyWebServer webServer = new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout, getShutdown()); webServer.setRouteProviders(this.routeProviders); return webServer; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index dfd140fbf9..24aeb48e44 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.function.Supplier; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.unix.Errors.NativeIoException; @@ -35,8 +36,10 @@ import reactor.netty.http.server.HttpServerRequest; import reactor.netty.http.server.HttpServerResponse; import reactor.netty.http.server.HttpServerRoutes; -import org.springframework.boot.web.server.GracefulShutdown; +import org.springframework.boot.web.server.GracefulShutdownCallback; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; @@ -69,40 +72,21 @@ public class NettyWebServer implements WebServer { private final Duration lifecycleTimeout; - private final GracefulShutdown shutdown; + private final GracefulShutdown gracefulShutdown; private List routeProviders = Collections.emptyList(); private volatile DisposableServer disposableServer; - public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout) { - this(httpServer, handlerAdapter, lifecycleTimeout, null); - } - - /** - * Creates a {@code NettyWebServer}. - * @param httpServer the Reactor Netty HTTP server - * @param handlerAdapter the Spring WebFlux handler adapter - * @param lifecycleTimeout lifecycle timeout - * @param shutdownGracePeriod grace period for handler for graceful shutdown - * @since 2.3.0 - */ public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, - Duration shutdownGracePeriod) { + Shutdown shutdown) { Assert.notNull(httpServer, "HttpServer must not be null"); Assert.notNull(handlerAdapter, "HandlerAdapter must not be null"); this.lifecycleTimeout = lifecycleTimeout; this.handler = handlerAdapter; - if (shutdownGracePeriod != null) { - this.httpServer = httpServer.channelGroup(new DefaultChannelGroup(new DefaultEventExecutor())); - NettyGracefulShutdown gracefulShutdown = new NettyGracefulShutdown(() -> this.disposableServer, - shutdownGracePeriod); - this.shutdown = gracefulShutdown; - } - else { - this.httpServer = httpServer; - this.shutdown = GracefulShutdown.IMMEDIATE; - } + this.httpServer = httpServer.channelGroup(new DefaultChannelGroup(new DefaultEventExecutor())); + this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(() -> this.disposableServer) + : null; } public void setRouteProviders(List routeProviders) { @@ -140,12 +124,13 @@ public class NettyWebServer implements WebServer { } @Override - public boolean shutDownGracefully() { - return this.shutdown.shutDownGracefully(); - } - - boolean inGracefulShutdown() { - return this.shutdown.isShuttingDown(); + public void shutDownGracefully(GracefulShutdownCallback callback) { + if (this.gracefulShutdown != null) { + this.gracefulShutdown.shutDownGracefully(callback); + } + else { + callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); + } } private DisposableServer startHttpServer() { @@ -186,11 +171,19 @@ public class NettyWebServer implements WebServer { @Override public void stop() throws WebServerException { if (this.disposableServer != null) { - if (this.lifecycleTimeout != null) { - this.disposableServer.disposeNow(this.lifecycleTimeout); + if (this.gracefulShutdown != null) { + this.gracefulShutdown.abort(); } - else { - this.disposableServer.disposeNow(); + try { + if (this.lifecycleTimeout != null) { + this.disposableServer.disposeNow(this.lifecycleTimeout); + } + else { + this.disposableServer.disposeNow(); + } + } + catch (IllegalStateException ex) { + // Continue } this.disposableServer = null; } @@ -204,4 +197,60 @@ public class NettyWebServer implements WebServer { return 0; } + private static final class GracefulShutdown { + + private static final Log logger = LogFactory.getLog(GracefulShutdown.class); + + private final Supplier disposableServer; + + private volatile Thread shutdownThread; + + private volatile boolean shuttingDown; + + private GracefulShutdown(Supplier disposableServer) { + this.disposableServer = disposableServer; + } + + private void shutDownGracefully(GracefulShutdownCallback callback) { + DisposableServer server = this.disposableServer.get(); + if (server == null) { + return; + } + logger.info("Commencing graceful shutdown. Waiting for active requests to complete"); + this.shutdownThread = new Thread(() -> { + this.shuttingDown = true; + try { + server.disposeNow(Duration.ofMillis(Long.MAX_VALUE)); + logger.info("Graceful shutdown complete"); + callback.shutdownComplete(GracefulShutdownResult.IDLE); + } + catch (Exception ex) { + logger.info("Graceful shutdown aborted with one or more active requests"); + callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); + } + finally { + this.shutdownThread = null; + this.shuttingDown = false; + } + }, "netty-shutdown"); + this.shutdownThread.start(); + } + + private void abort() { + Thread shutdownThread = this.shutdownThread; + if (shutdownThread != null) { + while (!this.shuttingDown) { + try { + Thread.sleep(50); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + this.shutdownThread.interrupt(); + } + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatGracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatGracefulShutdown.java deleted file mode 100644 index 8878dc178f..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatGracefulShutdown.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed 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 - * - * https://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.springframework.boot.web.embedded.tomcat; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.catalina.Container; -import org.apache.catalina.Service; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.core.StandardContext; -import org.apache.catalina.core.StandardWrapper; -import org.apache.catalina.startup.Tomcat; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.web.server.GracefulShutdown; - -/** - * {@link GracefulShutdown} for {@link Tomcat}. - * - * @author Andy Wilkinson - */ -class TomcatGracefulShutdown implements GracefulShutdown { - - private static final Log logger = LogFactory.getLog(TomcatGracefulShutdown.class); - - private final Tomcat tomcat; - - private final Duration period; - - private volatile boolean shuttingDown = false; - - TomcatGracefulShutdown(Tomcat tomcat, Duration period) { - this.tomcat = tomcat; - this.period = period; - } - - @Override - public boolean shutDownGracefully() { - logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds() - + "s for active requests to complete"); - List connectors = getConnectors(); - for (Connector connector : connectors) { - connector.pause(); - connector.getProtocolHandler().closeServerSocketGraceful(); - } - this.shuttingDown = true; - try { - long end = System.currentTimeMillis() + this.period.toMillis(); - for (Container host : this.tomcat.getEngine().findChildren()) { - for (Container context : host.findChildren()) { - while (active(context)) { - if (System.currentTimeMillis() > end) { - logger.info("Grace period elapsed with one or more requests still active"); - return false; - } - Thread.sleep(50); - } - } - } - - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - finally { - this.shuttingDown = false; - } - logger.info("Graceful shutdown complete"); - return true; - } - - private boolean active(Container context) { - try { - if (((StandardContext) context).getInProgressAsyncCount() > 0) { - return true; - } - for (Container wrapper : context.findChildren()) { - if (((StandardWrapper) wrapper).getCountAllocated() > 0) { - return true; - } - } - return false; - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private List getConnectors() { - List connectors = new ArrayList<>(); - for (Service service : this.tomcat.getServer().findServices()) { - Collections.addAll(connectors, service.findConnectors()); - } - return connectors; - } - - @Override - public boolean isShuttingDown() { - return this.shuttingDown; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 6fa8583313..e5eba0a5b7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -413,7 +413,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac * @return a new {@link TomcatWebServer} instance */ protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { - return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod()); + return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown()); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 7cc4097635..d9b9bc637a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -434,7 +434,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto * @return a new {@link TomcatWebServer} instance */ protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { - return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod()); + return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown()); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 314fb7302e..8e94364c36 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -16,9 +16,11 @@ package org.springframework.boot.web.embedded.tomcat; -import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -33,13 +35,17 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.apache.catalina.Service; import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardWrapper; import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.naming.ContextBindings; -import org.springframework.boot.web.server.GracefulShutdown; +import org.springframework.boot.web.server.GracefulShutdownCallback; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; import org.springframework.util.Assert; @@ -85,22 +91,21 @@ public class TomcatWebServer implements WebServer { * @param autoStart if the server should be started */ public TomcatWebServer(Tomcat tomcat, boolean autoStart) { - this(tomcat, autoStart, null); + this(tomcat, autoStart, Shutdown.IMMEDIATE); } /** * Create a new {@link TomcatWebServer} instance. * @param tomcat the underlying Tomcat server * @param autoStart if the server should be started - * @param shutdownGracePeriod grace period to use when shutting down + * @param shutdown type of shutdown supported by the server * @since 2.3.0 */ - public TomcatWebServer(Tomcat tomcat, boolean autoStart, Duration shutdownGracePeriod) { + public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; - this.gracefulShutdown = (shutdownGracePeriod != null) ? new TomcatGracefulShutdown(tomcat, shutdownGracePeriod) - : GracefulShutdown.IMMEDIATE; + this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null; initialize(); } @@ -325,6 +330,9 @@ public class TomcatWebServer implements WebServer { try { this.started = false; try { + if (this.gracefulShutdown != null) { + this.gracefulShutdown.abort(); + } stopTomcat(); this.tomcat.destroy(); } @@ -379,12 +387,87 @@ public class TomcatWebServer implements WebServer { } @Override - public boolean shutDownGracefully() { - return this.gracefulShutdown.shutDownGracefully(); + public void shutDownGracefully(GracefulShutdownCallback callback) { + if (this.gracefulShutdown != null) { + this.gracefulShutdown.shutDownGracefully(callback); + } + else { + callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); + } } - boolean inGracefulShutdown() { - return this.gracefulShutdown.isShuttingDown(); + private static final class GracefulShutdown { + + private static final Log logger = LogFactory.getLog(GracefulShutdown.class); + + private final Tomcat tomcat; + + private volatile boolean aborted = false; + + private GracefulShutdown(Tomcat tomcat) { + this.tomcat = tomcat; + } + + private void shutDownGracefully(GracefulShutdownCallback callback) { + logger.info("Commencing graceful shutdown. Waiting for active requests to complete"); + new Thread(() -> { + List connectors = getConnectors(); + for (Connector connector : connectors) { + connector.pause(); + connector.getProtocolHandler().closeServerSocketGraceful(); + } + try { + for (Container host : this.tomcat.getEngine().findChildren()) { + for (Container context : host.findChildren()) { + while (active(context)) { + if (this.aborted) { + logger.info("Graceful shutdown aborted with one or more requests still active"); + callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); + return; + } + Thread.sleep(50); + } + } + } + + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + logger.info("Graceful shutdown complete"); + callback.shutdownComplete(GracefulShutdownResult.IDLE); + }, "tomcat-shutdown").start(); + } + + private void abort() { + this.aborted = true; + } + + private boolean active(Container context) { + try { + if (((StandardContext) context).getInProgressAsyncCount() > 0) { + return true; + } + for (Container wrapper : context.findChildren()) { + if (((StandardWrapper) wrapper).getCountAllocated() > 0) { + return true; + } + } + return false; + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private List getConnectors() { + List connectors = new ArrayList<>(); + for (Service service : this.tomcat.getServer().findServices()) { + Collections.addAll(connectors, service.findConnectors()); + } + return connectors; + } + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java deleted file mode 100644 index 4774b15898..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/GracefulShutdownHttpHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed 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 - * - * https://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.springframework.boot.web.embedded.undertow; - -import java.time.Duration; - -import io.undertow.server.HttpHandler; -import io.undertow.server.handlers.GracefulShutdownHandler; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.web.server.GracefulShutdown; - -/** - * A {@link GracefulShutdownHandler} with support for our own {@link GracefulShutdown} - * interface. - * - * @author Andy Wilkinson - */ -class GracefulShutdownHttpHandler extends GracefulShutdownHandler implements GracefulShutdown { - - private static final Log logger = LogFactory.getLog(GracefulShutdownHttpHandler.class); - - private final Duration gracePeriod; - - private volatile boolean shuttingDown; - - GracefulShutdownHttpHandler(HttpHandler next, Duration period) { - super(next); - this.gracePeriod = period; - } - - @Override - public boolean shutDownGracefully() { - logger.info("Commencing graceful shutdown, allowing up to " + this.gracePeriod.getSeconds() - + "s for active requests to complete"); - shutdown(); - this.shuttingDown = true; - boolean graceful = false; - try { - graceful = awaitShutdown(this.gracePeriod.toMillis()); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - finally { - this.shuttingDown = false; - } - if (graceful) { - logger.info("Graceful shutdown complete"); - return true; - } - logger.info("Grace period elapsed with one or more requests still active"); - return graceful; - } - - @Override - public boolean isShuttingDown() { - return this.shuttingDown; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java index f05a1ec930..581a673485 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/HttpHandlerFactory.java @@ -19,8 +19,7 @@ package org.springframework.boot.web.embedded.undertow; import java.io.Closeable; import io.undertow.server.HttpHandler; - -import org.springframework.boot.web.server.GracefulShutdown; +import io.undertow.server.handlers.GracefulShutdownHandler; /** * Factory used by {@link UndertowServletWebServer} to add {@link HttpHandler @@ -28,7 +27,7 @@ import org.springframework.boot.web.server.GracefulShutdown; * following interfaces: *
    *
  • {@link Closeable} - if they wish to be closed just before server stops.
  • - *
  • {@link GracefulShutdown} - if they wish to manage graceful shutdown.
  • + *
  • {@link GracefulShutdownHandler} - if they wish to manage graceful shutdown.
  • *
* * @author Phillip Webb diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index 41aa84098f..c2a8f94c2a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -24,15 +24,18 @@ import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import io.undertow.Undertow; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.GracefulShutdownHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.xnio.channels.BoundChannel; -import org.springframework.boot.web.server.GracefulShutdown; +import org.springframework.boot.web.server.GracefulShutdownCallback; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; @@ -56,6 +59,8 @@ public class UndertowWebServer implements WebServer { private static final Log logger = LogFactory.getLog(UndertowWebServer.class); + private final AtomicReference gracefulShutdownCallback = new AtomicReference<>(); + private final Object monitor = new Object(); private final Undertow.Builder builder; @@ -68,7 +73,7 @@ public class UndertowWebServer implements WebServer { private volatile boolean started = false; - private volatile GracefulShutdown gracefulShutdown; + private volatile GracefulShutdownHandler gracefulShutdown; private volatile List closeables; @@ -180,9 +185,9 @@ public class UndertowWebServer implements WebServer { if (handler instanceof Closeable) { this.closeables.add((Closeable) handler); } - if (handler instanceof GracefulShutdown) { - Assert.isNull(this.gracefulShutdown, "Only a single GracefulShutdown handler can be defined"); - this.gracefulShutdown = (GracefulShutdown) handler; + if (handler instanceof GracefulShutdownHandler) { + Assert.isNull(this.gracefulShutdown, "Only a single GracefulShutdownHandler can be defined"); + this.gracefulShutdown = (GracefulShutdownHandler) handler; } } return handler; @@ -271,6 +276,9 @@ public class UndertowWebServer implements WebServer { return; } this.started = false; + if (this.gracefulShutdown != null) { + notifyGracefulCallback(false); + } try { this.undertow.stop(); for (Closeable closeable : this.closeables) { @@ -293,12 +301,30 @@ public class UndertowWebServer implements WebServer { } @Override - public boolean shutDownGracefully() { - return (this.gracefulShutdown != null) ? this.gracefulShutdown.shutDownGracefully() : false; + public void shutDownGracefully(GracefulShutdownCallback callback) { + if (this.gracefulShutdown != null) { + logger.info("Commencing graceful shutdown. Wait for active requests to complete"); + this.gracefulShutdownCallback.set(callback); + this.gracefulShutdown.shutdown(); + this.gracefulShutdown.addShutdownListener((success) -> notifyGracefulCallback(success)); + } + else { + callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); + } } - boolean inGracefulShutdown() { - return (this.gracefulShutdown != null) ? this.gracefulShutdown.isShuttingDown() : false; + private void notifyGracefulCallback(boolean success) { + GracefulShutdownCallback callback = this.gracefulShutdownCallback.getAndSet(null); + if (callback != null) { + if (success) { + logger.info("Graceful shutdown complete"); + callback.shutdownComplete(GracefulShutdownResult.IDLE); + } + else { + logger.info("Graceful shutdown aborted with one or more requests still active"); + callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); + } + } } protected String getStartLogMessage() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java index d25aec95be..31f1e59b6c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java @@ -18,7 +18,6 @@ package org.springframework.boot.web.embedded.undertow; import java.io.File; import java.net.InetAddress; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -34,6 +33,7 @@ import io.undertow.UndertowOptions; import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Http2; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -178,8 +178,8 @@ class UndertowWebServerFactoryDelegate { List createHttpHandlerFactories(AbstractConfigurableWebServerFactory webServerFactory, HttpHandlerFactory... initialHttpHandlerFactories) { List factories = createHttpHandlerFactories(webServerFactory.getCompression(), - this.useForwardHeaders, webServerFactory.getServerHeader(), - webServerFactory.getShutdown().getGracePeriod(), initialHttpHandlerFactories); + this.useForwardHeaders, webServerFactory.getServerHeader(), webServerFactory.getShutdown(), + initialHttpHandlerFactories); if (isAccessLogEnabled()) { factories.add(new AccessLogHttpHandlerFactory(this.accessLogDirectory, this.accessLogPattern, this.accessLogPrefix, this.accessLogSuffix, this.accessLogRotate)); @@ -188,7 +188,7 @@ class UndertowWebServerFactoryDelegate { } static List createHttpHandlerFactories(Compression compression, boolean useForwardHeaders, - String serverHeader, Duration shutdownGracePeriod, HttpHandlerFactory... initialHttpHandlerFactories) { + String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) { List factories = new ArrayList(); factories.addAll(Arrays.asList(initialHttpHandlerFactories)); if (compression != null && compression.getEnabled()) { @@ -200,8 +200,8 @@ class UndertowWebServerFactoryDelegate { if (StringUtils.hasText(serverHeader)) { factories.add((next) -> Handlers.header(next, "Server", serverHeader)); } - if (shutdownGracePeriod != null) { - factories.add((next) -> new GracefulShutdownHttpHandler(next, shutdownGracePeriod)); + if (shutdown == Shutdown.GRACEFUL) { + factories.add(Handlers::gracefulShutdown); } return factories; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java index 01bb1da678..693dbbadf4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java @@ -28,6 +28,7 @@ import org.springframework.boot.web.context.ConfigurableWebServerApplicationCont import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; import org.springframework.context.ApplicationContextException; +import org.springframework.context.SmartLifecycle; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -69,7 +70,10 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli super.refresh(); } catch (RuntimeException ex) { - stopAndReleaseReactiveWebServer(); + ServerManager serverManager = this.serverManager; + if (serverManager != null) { + serverManager.server.stop(); + } throw ex; } } @@ -91,7 +95,11 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli String webServerFactoryBeanName = getWebServerFactoryBeanName(); ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName); boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit(); - this.serverManager = new ServerManager(webServerFactory, lazyInit); + this.serverManager = new ServerManager(webServerFactory, this::getHttpHandler, lazyInit); + getBeanFactory().registerSingleton("webServerGracefulShutdown", + new WebServerGracefulShutdownLifecycle(this.serverManager)); + getBeanFactory().registerSingleton("webServerStartStop", + new WebServerStartStopLifecycle(this.serverManager)); } initPropertySources(); } @@ -114,24 +122,6 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli return getBeanFactory().getBean(factoryBeanName, ReactiveWebServerFactory.class); } - @Override - protected void finishRefresh() { - super.finishRefresh(); - WebServer webServer = startReactiveWebServer(); - if (webServer != null) { - publishEvent(new ReactiveWebServerInitializedEvent(webServer, this)); - } - } - - private WebServer startReactiveWebServer() { - ServerManager serverManager = this.serverManager; - if (serverManager != null) { - serverManager.start(this::getHttpHandler); - return serverManager.server; - } - return null; - } - /** * Return the {@link HttpHandler} that should be used to process the reactive web * server. By default this method searches for a suitable bean in the context itself. @@ -155,31 +145,9 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli @Override protected void doClose() { AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); - WebServer webServer = getWebServer(); - if (webServer != null) { - webServer.shutDownGracefully(); - } super.doClose(); } - @Override - protected void onClose() { - super.onClose(); - stopAndReleaseReactiveWebServer(); - } - - private void stopAndReleaseReactiveWebServer() { - ServerManager serverManager = this.serverManager; - if (serverManager != null) { - try { - serverManager.server.stop(); - } - finally { - this.serverManager = null; - } - } - } - /** * Returns the {@link WebServer} that was created by the context or {@code null} if * the server has not yet been created. @@ -201,6 +169,77 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli this.serverNamespace = serverNamespace; } + /** + * Internal class used to manage the server and the {@link HttpHandler}, taking care + * not to initialize the handler too early. + */ + final class ServerManager { + + private final WebServer server; + + private DelayedInitializationHttpHandler handler; + + private ServerManager(ReactiveWebServerFactory factory, Supplier handlerSupplier, + boolean lazyInit) { + Assert.notNull(factory, "ReactiveWebServerFactory must not be null"); + this.handler = new DelayedInitializationHttpHandler(handlerSupplier, lazyInit); + this.server = factory.getWebServer(this.handler); + } + + private void start() { + this.handler.initializeHandler(); + this.server.start(); + ReactiveWebServerApplicationContext.this.publishEvent( + new ReactiveWebServerInitializedEvent(this.server, ReactiveWebServerApplicationContext.this)); + } + + void shutDownGracefully(Runnable callback) { + this.server.shutDownGracefully((result) -> callback.run()); + } + + void stop() { + this.server.stop(); + } + + HttpHandler getHandler() { + return this.handler; + } + + } + + static final class DelayedInitializationHttpHandler implements HttpHandler { + + private final Supplier handlerSupplier; + + private final boolean lazyInit; + + private volatile HttpHandler delegate = this::handleUninitialized; + + private DelayedInitializationHttpHandler(Supplier handlerSupplier, boolean lazyInit) { + this.handlerSupplier = handlerSupplier; + this.lazyInit = lazyInit; + } + + private Mono handleUninitialized(ServerHttpRequest request, ServerHttpResponse response) { + throw new IllegalStateException("The HttpHandler has not yet been initialized"); + } + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + return this.delegate.handle(request, response); + } + + private void initializeHandler() { + this.delegate = this.lazyInit ? new LazyHttpHandler(Mono.fromSupplier(this.handlerSupplier)) + : this.handlerSupplier.get(); + } + + HttpHandler getHandler() { + return this.delegate; + } + + } + /** * {@link HttpHandler} that initializes its delegate on first request. */ @@ -219,42 +258,69 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli } - /** - * Internal class used to manage the server and the {@link HttpHandler}, taking care - * not to initialize the handler too early. - */ - static final class ServerManager implements HttpHandler { + private static final class WebServerStartStopLifecycle implements SmartLifecycle { - private final WebServer server; + private final ServerManager serverManager; - private final boolean lazyInit; + private volatile boolean running; - private volatile HttpHandler handler; - - private ServerManager(ReactiveWebServerFactory factory, boolean lazyInit) { - Assert.notNull(factory, "ReactiveWebServerFactory must not be null"); - this.handler = this::handleUninitialized; - this.server = factory.getWebServer(this); - this.lazyInit = lazyInit; - } - - private Mono handleUninitialized(ServerHttpRequest request, ServerHttpResponse response) { - throw new IllegalStateException("The HttpHandler has not yet been initialized"); + private WebServerStartStopLifecycle(ServerManager serverManager) { + this.serverManager = serverManager; } @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return this.handler.handle(request, response); + public void start() { + this.serverManager.start(); + this.running = true; } - HttpHandler getHandler() { - return this.handler; + @Override + public void stop() { + this.running = false; + this.serverManager.stop(); } - private void start(Supplier handlerSupplier) { - this.handler = this.lazyInit ? new LazyHttpHandler(Mono.fromSupplier(handlerSupplier)) - : handlerSupplier.get(); - this.server.start(); + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE - 1; + } + + } + + private static final class WebServerGracefulShutdownLifecycle implements SmartLifecycle { + + private final ServerManager serverManager; + + private volatile boolean running; + + private WebServerGracefulShutdownLifecycle(ServerManager serverManager) { + this.serverManager = serverManager; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + throw new UnsupportedOperationException("Stop must not be invoked directly"); + } + + @Override + public void stop(Runnable callback) { + this.running = false; + this.serverManager.shutDownGracefully(callback); + } + + @Override + public boolean isRunning() { + return this.running; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index d9fa662e1c..b58a351c74 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -55,7 +55,7 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab private String serverHeader; - private Shutdown shutdown = new Shutdown(); + private Shutdown shutdown = Shutdown.IMMEDIATE; /** * Create a new {@link AbstractConfigurableWebServerFactory} instance. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ImmediateGracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownCallback.java similarity index 65% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ImmediateGracefulShutdown.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownCallback.java index d599a2790e..c43efdf1c2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ImmediateGracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownCallback.java @@ -17,20 +17,19 @@ package org.springframework.boot.web.server; /** - * A {@link GracefulShutdown} that returns immediately with no grace period. + * A callback for the result of a graceful shutdown request. * * @author Andy Wilkinson + * @since 2.3.0 + * @see WebServer#shutDownGracefully(GracefulShutdownCallback) */ -class ImmediateGracefulShutdown implements GracefulShutdown { +@FunctionalInterface +public interface GracefulShutdownCallback { - @Override - public boolean shutDownGracefully() { - return false; - } - - @Override - public boolean isShuttingDown() { - return false; - } + /** + * Graceful shutdown has completed with the given {@code result}. + * @param result the result of the shutdown + */ + void shutdownComplete(GracefulShutdownResult result); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java similarity index 51% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdown.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java index 8809212d7e..c1a94d106e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java @@ -17,31 +17,28 @@ package org.springframework.boot.web.server; /** - * Handles graceful shutdown of a {@link WebServer}. + * The result of a graceful shutdown request. * * @author Andy Wilkinson * @since 2.3.0 + * @see GracefulShutdownCallback + * @see WebServer#shutDownGracefully(GracefulShutdownCallback) */ -public interface GracefulShutdown { +public enum GracefulShutdownResult { /** - * A {@link GracefulShutdown} that returns immediately with no grace period. + * Requests remained active at the end of the grace period. */ - GracefulShutdown IMMEDIATE = new ImmediateGracefulShutdown(); + REQUESTS_ACTIVE, /** - * Shuts down the {@link WebServer}, returning {@code true} if activity ceased during - * the grace period, otherwise {@code false}. - * @return {@code true} if activity ceased during the grace period, otherwise - * {@code false} + * The server was idle with no active requests at the end of the grace period. */ - boolean shutDownGracefully(); + IDLE, /** - * Returns whether the handler is in the process of gracefully shutting down the web - * server. - * @return {@code true} is graceful shutdown is in progress, otherwise {@code false}. + * The server was shutdown immediately, ignoring any active requests. */ - boolean isShuttingDown(); + IMMEDIATE; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java index 6451708fda..c09fab5750 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java @@ -16,28 +16,23 @@ package org.springframework.boot.web.server; -import java.time.Duration; - /** * Configuration for shutting down a {@link WebServer}. * * @author Andy Wilkinson * @since 2.3.0 */ -public class Shutdown { +public enum Shutdown { /** - * Time to wait for web activity to cease before shutting down the application. By - * default, shutdown will proceed immediately. + * The {@link WebServer} should support graceful shutdown, allowing active requests + * time to complete. */ - private Duration gracePeriod; + GRACEFUL, - public Duration getGracePeriod() { - return this.gracePeriod; - } - - public void setGracePeriod(Duration period) { - this.gracePeriod = period; - } + /** + * The {@link WebServer} should shut down immediately. + */ + IMMEDIATE; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java index eab3311072..6b70c02ca7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java @@ -48,14 +48,17 @@ public interface WebServer { int getPort(); /** - * Gracefully shuts down the web server by preventing the handling of new requests and - * waiting for a configurable period for there to be no active requests. - * @return {@code true} if graceful shutdown completed within the period, otherwise - * {@code false} + * Initiates a graceful shutdown of the web server. Handling of new requests is + * prevented and the given {@code callback} is invoked at the end of the attempt. The + * attempt can be explicitly ended by invoking {@link #stop}. The default + * implementation invokes the callback immediately with + * {@link GracefulShutdownResult#IMMEDIATE}, i.e. no attempt is made at a graceful + * shutdown. + * @param callback the callback to invoke when the graceful shutdown completes * @since 2.3.0 */ - default boolean shutDownGracefully() { - return false; + default void shutDownGracefully(GracefulShutdownCallback callback) { + callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java index f2f6844776..27a68ff7e3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java @@ -48,6 +48,7 @@ import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; +import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; import org.springframework.util.StringUtils; import org.springframework.web.context.ContextLoader; @@ -143,7 +144,10 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon super.refresh(); } catch (RuntimeException ex) { - stopAndReleaseWebServer(); + WebServer webServer = this.webServer; + if (webServer != null) { + webServer.stop(); + } throw ex; } } @@ -159,37 +163,21 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon } } - @Override - protected void finishRefresh() { - super.finishRefresh(); - WebServer webServer = startWebServer(); - if (webServer != null) { - publishEvent(new ServletWebServerInitializedEvent(webServer, this)); - } - } - @Override protected void doClose() { AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); - WebServer webServer = this.webServer; - if (webServer != null) { - webServer.shutDownGracefully(); - } super.doClose(); } - @Override - protected void onClose() { - super.onClose(); - stopAndReleaseWebServer(); - } - private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); + getBeanFactory().registerSingleton("webServerGracefulShutdown", + new WebServerGracefulShutdownLifecycle(this.webServer)); + getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.webServer)); } else if (servletContext != null) { try { @@ -303,27 +291,6 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon } } - private WebServer startWebServer() { - WebServer webServer = this.webServer; - if (webServer != null) { - webServer.start(); - } - return webServer; - } - - private void stopAndReleaseWebServer() { - WebServer webServer = this.webServer; - if (webServer != null) { - try { - webServer.stop(); - this.webServer = null; - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - } - @Override protected Resource getResourceByPath(String path) { if (getServletContext() == null) { @@ -403,4 +370,72 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon } + private final class WebServerStartStopLifecycle implements SmartLifecycle { + + private final WebServer webServer; + + private volatile boolean running; + + private WebServerStartStopLifecycle(WebServer webServer) { + this.webServer = webServer; + } + + @Override + public void start() { + this.webServer.start(); + this.running = true; + ServletWebServerApplicationContext.this.publishEvent( + new ServletWebServerInitializedEvent(this.webServer, ServletWebServerApplicationContext.this)); + } + + @Override + public void stop() { + this.webServer.stop(); + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE - 1; + } + + } + + private final class WebServerGracefulShutdownLifecycle implements SmartLifecycle { + + private final WebServer webServer; + + private volatile boolean running; + + WebServerGracefulShutdownLifecycle(WebServer webServer) { + this.webServer = webServer; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + throw new UnsupportedOperationException("Stop must not be invoked directly"); + } + + @Override + public void stop(Runnable callback) { + this.running = false; + this.webServer.shutDownGracefully((result) -> callback.run()); + } + + @Override + public boolean isRunning() { + return this.running; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/AbstractJettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/AbstractJettyServletWebServerFactoryTests.java index a8923e0de7..3c38319904 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/AbstractJettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/AbstractJettyServletWebServerFactoryTests.java @@ -21,7 +21,9 @@ import java.util.Locale; import java.util.Map; import org.apache.jasper.servlet.JspServlet; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.Test; @@ -57,7 +59,7 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet @Override protected JspServlet getJspServlet() throws Exception { - WebAppContext context = (WebAppContext) ((JettyWebServer) this.webServer).getServer().getHandler(); + WebAppContext context = findWebAppContext((JettyWebServer) this.webServer); ServletHolder holder = context.getServletHandler().getServlet("jsp"); if (holder == null) { return null; @@ -69,13 +71,13 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet @Override protected Map getActualMimeMappings() { - WebAppContext context = (WebAppContext) ((JettyWebServer) this.webServer).getServer().getHandler(); + WebAppContext context = findWebAppContext((JettyWebServer) this.webServer); return context.getMimeTypes().getMimeMap(); } @Override protected Charset getCharset(Locale locale) { - WebAppContext context = (WebAppContext) ((JettyWebServer) this.webServer).getServer().getHandler(); + WebAppContext context = findWebAppContext((JettyWebServer) this.webServer); String charsetName = context.getLocaleEncoding(locale); return (charsetName != null) ? Charset.forName(charsetName) : null; } @@ -91,11 +93,6 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort); } - @Override - protected boolean inGracefulShutdown() { - return ((JettyWebServer) this.webServer).inGracefulShutdown(); - } - @Test void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) { AbstractServletWebServerFactory factory = getFactory(); @@ -108,4 +105,18 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet assertThat(output).containsOnlyOnce("with context path '/custom'"); } + protected WebAppContext findWebAppContext(JettyWebServer webServer) { + return findWebAppContext(webServer.getServer().getHandler()); + } + + private WebAppContext findWebAppContext(Handler handler) { + if (handler instanceof WebAppContext) { + return (WebAppContext) handler; + } + if (handler instanceof HandlerWrapper) { + return findWebAppContext(((HandlerWrapper) handler).getHandler()); + } + throw new IllegalStateException("No WebAppCotext found"); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index 7bf3c53059..621a44c466 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -20,24 +20,21 @@ import java.net.ConnectException; import java.net.InetAddress; import java.time.Duration; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; +import org.awaitility.Awaitility; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.junit.jupiter.api.Test; import org.mockito.InOrder; -import reactor.core.publisher.Mono; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.server.Shutdown; -import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; @@ -121,29 +118,24 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception { JettyReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); - int port = this.webServer.getPort(); - CountDownLatch responseLatch = new CountDownLatch(1); - getWebClient(port).build().get().retrieve().toBodilessEntity() - .subscribe((response) -> responseLatch.countDown()); - blockingHandler.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - Mono> unconnectableRequest = getWebClient(port).build().get().retrieve() - .toBodilessEntity(); - assertThat(shutdownResult.get()).isEqualTo(false); - blockingHandler.completeOne(); - assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> unconnectableRequest.block()) - .withCauseInstanceOf(ConnectException.class); - } - - @Override - protected boolean inGracefulShutdown() { - return ((JettyWebServer) this.webServer).inGracefulShutdown(); + WebClient webClient = getWebClient(this.webServer.getPort()).build(); + this.webServer.shutDownGracefully((result) -> { + }); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> { + blockingHandler.stopBlocking(); + try { + webClient.get().retrieve().toBodilessEntity().block(); + return false; + } + catch (RuntimeException ex) { + return ex.getCause() instanceof ConnectException; + } + }); + this.webServer.stop(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 9ade73e2e5..44a02c996a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,6 +36,7 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.conn.HttpHostConnectException; import org.apache.http.impl.client.HttpClients; +import org.awaitility.Awaitility; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -52,6 +54,7 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.Test; import org.mockito.InOrder; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; @@ -182,9 +185,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -195,9 +196,9 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); + this.webServer.shutDownGracefully((result) -> { + }); Future unconnectableRequest = initiateGetRequest(port, "/"); - assertThat(shutdownResult.get()).isEqualTo(false); blockingServlet.admitOne(); Object response = request.get(); assertThat(response).isInstanceOf(HttpResponse.class); @@ -209,9 +210,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac void whenServerIsShuttingDownGracefullyThenResponseToRequestOnIdleConnectionWillHaveAConnectionCloseHeader() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -228,7 +227,8 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac assertThat(response).isInstanceOf(HttpResponse.class); assertThat(((HttpResponse) response).getStatusLine().getStatusCode()).isEqualTo(200); assertThat(((HttpResponse) response).getFirstHeader("Connection")).isNull(); - this.webServer.shutDownGracefully(); + this.webServer.shutDownGracefully((result) -> { + }); request = initiateGetRequest(client, port, "/blocking"); blockingServlet.awaitQueue(); blockingServlet.admitOne(); @@ -244,9 +244,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac void whenARequestCompletesAfterGracefulShutdownHasBegunThenItHasAConnectionCloseHeader() throws InterruptedException, ExecutionException { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -257,12 +255,10 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - long start = System.currentTimeMillis(); - Future shutdownResult = initiateGracefulShutdown(); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); blockingServlet.admitOne(); - assertThat(shutdownResult.get()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> GracefulShutdownResult.IDLE == result.get()); Object requestResult = request.get(); assertThat(requestResult).isInstanceOf(HttpResponse.class); assertThat(((HttpResponse) requestResult).getFirstHeader("Connection").getValue()).isEqualTo("close"); @@ -281,8 +277,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac private void assertTimeout(JettyServletWebServerFactory factory, int expected) { this.webServer = factory.getWebServer(); JettyWebServer jettyWebServer = (JettyWebServer) this.webServer; - Handler[] handlers = jettyWebServer.getServer().getChildHandlersByClass(WebAppContext.class); - WebAppContext webAppContext = (WebAppContext) handlers[0]; + WebAppContext webAppContext = findWebAppContext(jettyWebServer); int actual = webAppContext.getSessionHandler().getMaxInactiveInterval(); assertThat(actual).isEqualTo(expected); } @@ -414,7 +409,7 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac }); JettyWebServer jettyWebServer = (JettyWebServer) factory.getWebServer(); - WebAppContext context = (WebAppContext) jettyWebServer.getServer().getHandler(); + WebAppContext context = findWebAppContext(jettyWebServer); assertThat(context.getErrorHandler()).isInstanceOf(CustomErrorHandler.class); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index 3ac1943d8a..d2890c7076 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -19,9 +19,8 @@ package org.springframework.boot.web.embedded.netty; import java.net.ConnectException; import java.time.Duration; import java.util.Arrays; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import reactor.core.publisher.Mono; @@ -106,22 +105,24 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception { NettyReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); WebClient webClient = getWebClient(this.webServer.getPort()).build(); - webClient.get().retrieve().toBodilessEntity().subscribe(); - blockingHandler.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - AtomicReference errorReference = new AtomicReference<>(); - webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe(); - assertThat(shutdownResult.get()).isEqualTo(false); - blockingHandler.completeOne(); + this.webServer.shutDownGracefully((result) -> { + }); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> { + blockingHandler.stopBlocking(); + try { + webClient.get().retrieve().toBodilessEntity().block(); + return false; + } + catch (RuntimeException ex) { + return ex.getCause() instanceof ConnectException; + } + }); this.webServer.stop(); - assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class); } protected Mono testSslWithAlias(String alias) { @@ -142,9 +143,4 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor .exchange().flatMap((response) -> response.bodyToMono(String.class)); } - @Override - protected boolean inGracefulShutdown() { - return ((NettyWebServer) this.webServer).inGracefulShutdown(); - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java index cfd1d7a1e6..93cc4a46c9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java @@ -23,7 +23,6 @@ import java.net.ServerSocket; import java.time.Duration; import java.util.Arrays; import java.util.Map; -import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; import org.apache.catalina.Context; @@ -38,6 +37,7 @@ import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -257,22 +257,24 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception { TomcatReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); WebClient webClient = getWebClient(this.webServer.getPort()).build(); - webClient.get().retrieve().toBodilessEntity().subscribe(); - blockingHandler.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - AtomicReference errorReference = new AtomicReference<>(); - webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe(); - assertThat(shutdownResult.get()).isEqualTo(false); - blockingHandler.completeOne(); + this.webServer.shutDownGracefully((result) -> { + }); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> { + blockingHandler.stopBlocking(); + try { + webClient.get().retrieve().toBodilessEntity().block(); + return false; + } + catch (RuntimeException ex) { + return ex.getCause() instanceof ConnectException; + } + }); this.webServer.stop(); - assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class); } @Test @@ -314,11 +316,6 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort); } - @Override - protected boolean inGracefulShutdown() { - return ((TomcatWebServer) this.webServer).inGracefulShutdown(); - } - interface BlockedPortAction { void run(int port); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index ea25ed9d54..bdb520520f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -560,9 +560,7 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -573,9 +571,9 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - Future unconnectableRequest = initiateGetRequest(port, "/"); - assertThat(shutdownResult.get()).isEqualTo(false); + this.webServer.shutDownGracefully((result) -> { + }); + Future unconnectableRequest = initiateGetRequest(HttpClients.createDefault(), port, "/"); blockingServlet.admitOne(); assertThat(request.get()).isInstanceOf(HttpResponse.class); assertThat(unconnectableRequest.get()).isInstanceOf(HttpHostConnectException.class); @@ -585,9 +583,7 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory @Test void whenServerIsShuttingDownARequestOnAnIdleConnectionResultsInConnectionReset() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -603,7 +599,8 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory assertThat(keepAliveRequest.get()).isInstanceOf(HttpResponse.class); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - initiateGracefulShutdown(); + this.webServer.shutDownGracefully((result) -> { + }); Future idleConnectionRequest = initiateGetRequest(httpClient, port, "/blocking"); blockingServlet.admitOne(); Object response = request.get(); @@ -669,9 +666,4 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort); } - @Override - protected boolean inGracefulShutdown() { - return ((TomcatWebServer) this.webServer).inGracefulShutdown(); - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java index 86ad899b16..9de21c3af3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.net.URISyntaxException; import java.time.Duration; import java.util.Arrays; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; import io.undertow.Undertow; import org.awaitility.Awaitility; @@ -112,27 +110,24 @@ class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFac @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsAreRejectedWithServiceUnavailable() throws Exception { UndertowReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); + this.webServer.shutDownGracefully((result) -> { + }); WebClient webClient = getWebClient(this.webServer.getPort()).build(); - webClient.get().retrieve().toBodilessEntity().subscribe(); - blockingHandler.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - AtomicReference errorReference = new AtomicReference<>(); - webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe(); - assertThat(shutdownResult.get()).isEqualTo(false); - blockingHandler.completeOne(); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> { + blockingHandler.stopBlocking(); + try { + webClient.get().retrieve().toBodilessEntity().block(); + return false; + } + catch (RuntimeException ex) { + return ex instanceof ServiceUnavailable; + } + }); this.webServer.stop(); - assertThat(errorReference.get()).isInstanceOf(ServiceUnavailable.class); - } - - @Override - protected boolean inGracefulShutdown() { - return ((UndertowWebServer) this.webServer).inGracefulShutdown(); } private void testAccessLog(String prefix, String suffix, String expectedFile) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java index 3bc296a87e..69ff461711 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java @@ -44,6 +44,7 @@ import org.mockito.InOrder; import org.springframework.boot.testsupport.web.servlet.ExampleServlet; import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.servlet.ServletRegistrationBean; @@ -180,9 +181,7 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto @Test void whenServerIsShuttingDownGracefullyThenRequestsAreRejectedWithServiceUnavailable() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); @@ -193,16 +192,16 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - Future shutdownResult = initiateGracefulShutdown(); - Future rejectedRequest = initiateGetRequest(port, "/"); - assertThat(shutdownResult.get()).isEqualTo(false); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); + assertThat(result.get()).isNull(); blockingServlet.admitOne(); assertThat(request.get()).isInstanceOf(HttpResponse.class); - this.webServer.stop(); - Object requestResult = rejectedRequest.get(); - assertThat(requestResult).isInstanceOf(HttpResponse.class); - assertThat(((HttpResponse) requestResult).getStatusLine().getStatusCode()) + Object rejectedResult = initiateGetRequest(port, "/").get(); + assertThat(rejectedResult).isInstanceOf(HttpResponse.class); + assertThat(((HttpResponse) rejectedResult).getStatusLine().getStatusCode()) .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value()); + this.webServer.stop(); } private void testAccessLog(String prefix, String suffix, String expectedFile) @@ -310,9 +309,4 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort); } - @Override - protected boolean inGracefulShutdown() { - return ((UndertowServletWebServer) this.webServer).inGracefulShutdown(); - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/AnnotationConfigReactiveWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/AnnotationConfigReactiveWebServerApplicationContextTests.java index 632ed66a9b..e8c9671fb2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/AnnotationConfigReactiveWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/AnnotationConfigReactiveWebServerApplicationContextTests.java @@ -18,7 +18,7 @@ package org.springframework.boot.web.reactive.context; import org.junit.jupiter.api.Test; -import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.ServerManager; +import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.DelayedInitializationHttpHandler; import org.springframework.boot.web.reactive.context.config.ExampleReactiveWebServerApplicationConfiguration; import org.springframework.boot.web.reactive.server.MockReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; @@ -93,8 +93,8 @@ class AnnotationConfigReactiveWebServerApplicationContextTests { MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class); HttpHandler expectedHandler = this.context.getBean(HttpHandler.class); HttpHandler actualHandler = factory.getWebServer().getHttpHandler(); - if (actualHandler instanceof ServerManager) { - actualHandler = ((ServerManager) actualHandler).getHandler(); + if (actualHandler instanceof DelayedInitializationHttpHandler) { + actualHandler = ((DelayedInitializationHttpHandler) actualHandler).getHandler(); } assertThat(actualHandler).isEqualTo(expectedHandler); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java index 87c5728a7b..a217123c4f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java @@ -97,9 +97,9 @@ class ReactiveWebServerApplicationContextTests { this.context.addApplicationListener(listener); this.context.refresh(); List events = listener.receivedEvents(); - assertThat(events).hasSize(2).extracting("class").containsExactly(ContextRefreshedEvent.class, - ReactiveWebServerInitializedEvent.class); - ReactiveWebServerInitializedEvent initializedEvent = (ReactiveWebServerInitializedEvent) events.get(1); + assertThat(events).hasSize(2).extracting("class").containsExactly(ReactiveWebServerInitializedEvent.class, + ContextRefreshedEvent.class); + ReactiveWebServerInitializedEvent initializedEvent = (ReactiveWebServerInitializedEvent) events.get(0); assertThat(initializedEvent.getSource().getPort()).isGreaterThanOrEqualTo(0); assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 6f73447206..337d422a0d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -28,9 +28,6 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.RunnableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -45,6 +42,7 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -53,8 +51,8 @@ import reactor.netty.NettyPipeline; import reactor.netty.http.client.HttpClient; import reactor.test.StepVerifier; -import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.server.Compression; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; @@ -66,7 +64,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -353,23 +350,19 @@ public abstract class AbstractReactiveWebServerFactoryTests { @Test void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception { AbstractReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); this.webServer = factory.getWebServer(new EchoHandler()); this.webServer.start(); - long start = System.currentTimeMillis(); - assertThat(this.webServer.shutDownGracefully()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> GracefulShutdownResult.IDLE == result.get()); } @Test - void whenARequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception { + void whenARequestRemainsInFlightThenShutDownGracefullyDoesNotInvokeCallbackUntilTheRequestCompletes() + throws Exception { AbstractReactiveWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); @@ -382,26 +375,19 @@ public abstract class AbstractReactiveWebServerFactoryTests { responseLatch.countDown(); }); blockingHandler.awaitQueue(); - long start = System.currentTimeMillis(); - assertThat(this.webServer.shutDownGracefully()).isFalse(); - long end = System.currentTimeMillis(); - assertThat(end - start).isGreaterThanOrEqualTo(5000); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); assertThat(responseReference.get()).isNull(); blockingHandler.completeOne(); assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> GracefulShutdownResult.IDLE == result.get()); } @Test - void whenARequestCompletesDuringGracePeriodThenShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception { + void givenAnInflightRequestWhenTheServerIsStoppedThenGracefulShutdownCallbackIsCalledWithRequestsActive() + throws Exception { AbstractReactiveWebServerFactory factory = getFactory(); - if (factory instanceof NettyReactiveWebServerFactory) { - ReactorResourceFactory resourceFactory = new ReactorResourceFactory(); - resourceFactory.afterPropertiesSet(); - ((NettyReactiveWebServerFactory) factory).setResourceFactory(resourceFactory); - } - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingHandler blockingHandler = new BlockingHandler(); this.webServer = factory.getWebServer(blockingHandler); this.webServer.start(); @@ -414,14 +400,19 @@ public abstract class AbstractReactiveWebServerFactoryTests { responseLatch.countDown(); }); blockingHandler.awaitQueue(); - long start = System.currentTimeMillis(); - Future shutdownResult = initiateGracefulShutdown(); - assertThat(responseLatch.getCount()).isEqualTo(1); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); + assertThat(responseReference.get()).isNull(); + try { + this.webServer.stop(); + } + catch (Exception ex) { + // Continue + } + System.out.println("Stopped"); + Awaitility.await().atMost(Duration.ofSeconds(5)) + .until(() -> GracefulShutdownResult.REQUESTS_ACTIVE == result.get()); blockingHandler.completeOne(); - assertThat(shutdownResult.get()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); - assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue(); } @Test @@ -439,7 +430,12 @@ public abstract class AbstractReactiveWebServerFactoryTests { responseLatch.countDown(); }); blockingHandler.awaitQueue(); - this.webServer.stop(); + try { + this.webServer.stop(); + } + catch (Exception ex) { + // Continue + } blockingHandler.completeOne(); } @@ -497,26 +493,6 @@ public abstract class AbstractReactiveWebServerFactoryTests { throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure); } - protected Future initiateGracefulShutdown() { - RunnableFuture future = new FutureTask(() -> this.webServer.shutDownGracefully()); - new Thread(future).start(); - awaitInGracefulShutdown(); - return future; - } - - protected void awaitInGracefulShutdown() { - while (!inGracefulShutdown()) { - try { - Thread.sleep(100); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } - - protected abstract boolean inGracefulShutdown(); - protected static class EchoHandler implements HttpHandler { public EchoHandler() { @@ -534,15 +510,20 @@ public abstract class AbstractReactiveWebServerFactoryTests { private final BlockingQueue> monoProcessors = new ArrayBlockingQueue<>(10); + private volatile boolean blocking = true; + public BlockingHandler() { } @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - MonoProcessor completion = MonoProcessor.create(); - this.monoProcessors.add(completion); - return completion.then(Mono.empty()); + if (this.blocking) { + MonoProcessor completion = MonoProcessor.create(); + this.monoProcessors.add(completion); + return completion.then(Mono.empty()); + } + return Mono.empty(); } public void completeOne() { @@ -561,6 +542,11 @@ public abstract class AbstractReactiveWebServerFactoryTests { } } + public void stopBlocking() { + this.blocking = false; + this.monoProcessors.forEach(MonoProcessor::onComplete); + } + } static class CompressionDetectionHandler extends ChannelInboundHandlerAdapter { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java index 423702a205..26f30ad358 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java @@ -145,9 +145,9 @@ class ServletWebServerApplicationContextTests { this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class)); this.context.refresh(); List events = this.context.getBean(TestApplicationListener.class).receivedEvents(); - assertThat(events).hasSize(2).extracting("class").contains(ContextRefreshedEvent.class, - ServletWebServerInitializedEvent.class); - ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(1); + assertThat(events).hasSize(2).extracting("class").containsExactly(ServletWebServerInitializedEvent.class, + ContextRefreshedEvent.class); + ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(0); assertThat(initializedEvent.getSource().getPort() >= 0).isTrue(); assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index b7e0042516..c51f1f0f19 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -53,7 +53,6 @@ import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -97,6 +96,7 @@ import org.apache.http.ssl.TrustStrategy; import org.apache.jasper.EmbeddedServletOptions; import org.apache.jasper.servlet.JspServlet; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; @@ -112,6 +112,7 @@ import org.springframework.boot.testsupport.web.servlet.ExampleFilter; import org.springframework.boot.testsupport.web.servlet.ExampleServlet; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.Shutdown; @@ -1033,75 +1034,42 @@ public abstract class AbstractServletWebServerFactoryTests { } @Test - void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception { + void whenThereAreNoInFlightRequestsShutDownGracefullyInvokesCallbackWithIdle() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); this.webServer = factory.getWebServer(); this.webServer.start(); - long start = System.currentTimeMillis(); - assertThat(this.webServer.shutDownGracefully()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> GracefulShutdownResult.IDLE == result.get()); } @Test - void whenARequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception { - AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); - BlockingServlet blockingServlet = new BlockingServlet(); - this.webServer = factory.getWebServer((context) -> { - Dynamic registration = context.addServlet("blockingServlet", blockingServlet); - registration.addMapping("/blocking"); - }); - this.webServer.start(); - int port = this.webServer.getPort(); - Future request = initiateGetRequest(port, "/blocking"); - blockingServlet.awaitQueue(); - long start = System.currentTimeMillis(); - assertThat(this.webServer.shutDownGracefully()).isFalse(); - long end = System.currentTimeMillis(); - assertThat(end - start).isGreaterThanOrEqualTo(5000); - assertThat(request.isDone()).isFalse(); - blockingServlet.admitOne(); - assertThat(request.get()).isInstanceOf(HttpResponse.class); - } - - @Test - void whenARequestCompletesAndTheConnectionIsClosedDuringGracePeriodThenShutDownGracefullyReturnsTrueBeforePeriodElapses() + void whenARequestRemainsInFlightThenShutDownGracefullyDoesNotInvokeCallbackUntilTheRequestCompletes() throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingServlet blockingServlet = new BlockingServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingServlet); registration.addMapping("/blocking"); - registration.setAsyncSupported(true); }); this.webServer.start(); int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blocking"); blockingServlet.awaitQueue(); - long start = System.currentTimeMillis(); - Future shutdownResult = initiateGracefulShutdown(); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); blockingServlet.admitOne(); - assertThat(shutdownResult.get()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); assertThat(request.get()).isInstanceOf(HttpResponse.class); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> GracefulShutdownResult.IDLE == result.get()); } @Test - void whenAnAsyncRequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception { + void whenAnAsyncRequestRemainsInFlightThenShutDownGracefullyDoesNotInvokeCallbackUntilRequestCompletes() + throws Exception { AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(5)); - factory.setShutdown(shutdown); + factory.setShutdown(Shutdown.GRACEFUL); BlockingAsyncServlet blockingAsyncServlet = new BlockingAsyncServlet(); this.webServer = factory.getWebServer((context) -> { Dynamic registration = context.addServlet("blockingServlet", blockingAsyncServlet); @@ -1112,39 +1080,14 @@ public abstract class AbstractServletWebServerFactoryTests { int port = this.webServer.getPort(); Future request = initiateGetRequest(port, "/blockingAsync"); blockingAsyncServlet.awaitQueue(); - long start = System.currentTimeMillis(); - assertThat(this.webServer.shutDownGracefully()).isFalse(); - long end = System.currentTimeMillis(); - assertThat(end - start).isGreaterThanOrEqualTo(5000); + AtomicReference result = new AtomicReference<>(); + this.webServer.shutDownGracefully(result::set); + Thread.sleep(5000); + assertThat(result.get()).isNull(); assertThat(request.isDone()).isFalse(); blockingAsyncServlet.admitOne(); assertThat(request.get()).isInstanceOf(HttpResponse.class); - } - - @Test - void whenAnAsyncRequestCompletesAndTheConnectionIsClosedDuringGracePeriodThenShutDownGracefullyReturnsTrueBeforePeriodElapses() - throws Exception { - AbstractServletWebServerFactory factory = getFactory(); - Shutdown shutdown = new Shutdown(); - shutdown.setGracePeriod(Duration.ofSeconds(30)); - factory.setShutdown(shutdown); - BlockingAsyncServlet blockingAsyncServlet = new BlockingAsyncServlet(); - this.webServer = factory.getWebServer((context) -> { - Dynamic registration = context.addServlet("blockingServlet", blockingAsyncServlet); - registration.addMapping("/blockingAsync"); - registration.setAsyncSupported(true); - }); - this.webServer.start(); - int port = this.webServer.getPort(); - Future request = initiateGetRequest(port, "/blockingAsync"); - blockingAsyncServlet.awaitQueue(); - long start = System.currentTimeMillis(); - Future shutdownResult = initiateGracefulShutdown(); - blockingAsyncServlet.admitOne(); - assertThat(shutdownResult.get()).isTrue(); - long end = System.currentTimeMillis(); - assertThat(end - start).isLessThanOrEqualTo(30000); - assertThat(request.get(30, TimeUnit.SECONDS)).isInstanceOf(HttpResponse.class); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> GracefulShutdownResult.IDLE == result.get()); } @Test @@ -1166,13 +1109,6 @@ public abstract class AbstractServletWebServerFactoryTests { } } - protected Future initiateGracefulShutdown() { - RunnableFuture future = new FutureTask(() -> this.webServer.shutDownGracefully()); - new Thread(future).start(); - awaitInGracefulShutdown(); - return future; - } - protected Future initiateGetRequest(int port, String path) { return initiateGetRequest(HttpClients.createMinimal(), port, path); } @@ -1192,17 +1128,6 @@ public abstract class AbstractServletWebServerFactoryTests { return getRequest; } - protected void awaitInGracefulShutdown() { - while (!inGracefulShutdown()) { - try { - Thread.sleep(100); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } - private void wrapsFailingServletException(WebServerException ex) { Throwable cause = ex.getCause(); while (cause != null) { @@ -1352,8 +1277,6 @@ public abstract class AbstractServletWebServerFactoryTests { protected abstract org.apache.jasper.servlet.JspServlet getJspServlet() throws Exception; - protected abstract boolean inGracefulShutdown(); - protected ServletContextInitializer exampleServletRegistration() { return new ServletRegistrationBean<>(new ExampleServlet(), "/hello"); }