Use SmartLifecycle for graceful web server shutdown

Closes gh-21325
This commit is contained in:
Andy Wilkinson 2020-05-07 18:02:28 +01:00
parent c42571ba40
commit 240898121f
40 changed files with 795 additions and 953 deletions

View File

@ -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;
}

View File

@ -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",

View File

@ -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<Shutdown> shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class);
verify(factory).setShutdown(shutdownCaptor.capture());
assertThat(shutdownCaptor.getValue().getGracePeriod()).isEqualTo(Duration.ofSeconds(30));
assertThat(shutdownCaptor.getValue()).isEqualTo(Shutdown.GRACEFUL);
}
}

View File

@ -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<String, String> 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<Shutdown> 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<String, String> map) {

View File

@ -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
----

View File

@ -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<Integer> activeRequests;
private final Duration period;
private volatile boolean shuttingDown = false;
JettyGracefulShutdown(Server server, Supplier<Integer> 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;
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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<Integer> activeRequests;
private volatile boolean shuttingDown = false;
private GracefulShutdown(Server server, Supplier<Integer> 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;
}
}
}

View File

@ -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> disposableServer;
private final Duration period;
private volatile boolean shuttingDown;
NettyGracefulShutdown(Supplier<DisposableServer> 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;
}
}

View File

@ -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;
}

View File

@ -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<NettyRouteProvider> 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<NettyRouteProvider> 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> disposableServer;
private volatile Thread shutdownThread;
private volatile boolean shuttingDown;
private GracefulShutdown(Supplier<DisposableServer> 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();
}
}
}
}

View File

@ -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<Connector> 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<Connector> getConnectors() {
List<Connector> connectors = new ArrayList<>();
for (Service service : this.tomcat.getServer().findServices()) {
Collections.addAll(connectors, service.findConnectors());
}
return connectors;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}

View File

@ -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());
}
/**

View File

@ -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

View File

@ -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<Connector> 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<Connector> getConnectors() {
List<Connector> connectors = new ArrayList<>();
for (Service service : this.tomcat.getServer().findServices()) {
Collections.addAll(connectors, service.findConnectors());
}
return connectors;
}
}
}

View File

@ -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;
}
}

View File

@ -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:
* <ul>
* <li>{@link Closeable} - if they wish to be closed just before server stops.</li>
* <li>{@link GracefulShutdown} - if they wish to manage graceful shutdown.</li>
* <li>{@link GracefulShutdownHandler} - if they wish to manage graceful shutdown.</li>
* </ul>
*
* @author Phillip Webb

View File

@ -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> 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<Closeable> 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() {

View File

@ -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<HttpHandlerFactory> createHttpHandlerFactories(AbstractConfigurableWebServerFactory webServerFactory,
HttpHandlerFactory... initialHttpHandlerFactories) {
List<HttpHandlerFactory> 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<HttpHandlerFactory> createHttpHandlerFactories(Compression compression, boolean useForwardHeaders,
String serverHeader, Duration shutdownGracePeriod, HttpHandlerFactory... initialHttpHandlerFactories) {
String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) {
List<HttpHandlerFactory> factories = new ArrayList<HttpHandlerFactory>();
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;
}

View File

@ -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<HttpHandler> 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<HttpHandler> handlerSupplier;
private final boolean lazyInit;
private volatile HttpHandler delegate = this::handleUninitialized;
private DelayedInitializationHttpHandler(Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
this.handlerSupplier = handlerSupplier;
this.lazyInit = lazyInit;
}
private Mono<Void> handleUninitialized(ServerHttpRequest request, ServerHttpResponse response) {
throw new IllegalStateException("The HttpHandler has not yet been initialized");
}
@Override
public Mono<Void> 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<Void> 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<Void> 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<HttpHandler> 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;
}
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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<String, String> 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");
}
}

View File

@ -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<Boolean> shutdownResult = initiateGracefulShutdown();
Mono<ResponseEntity<Void>> 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();
}
}

View File

@ -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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
this.webServer.shutDownGracefully((result) -> {
});
Future<Object> 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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<GracefulShutdownResult> 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);
}

View File

@ -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<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> 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<String> 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();
}
}

View File

@ -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<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> 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);

View File

@ -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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> unconnectableRequest = initiateGetRequest(port, "/");
assertThat(shutdownResult.get()).isEqualTo(false);
this.webServer.shutDownGracefully((result) -> {
});
Future<Object> 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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
initiateGracefulShutdown();
this.webServer.shutDownGracefully((result) -> {
});
Future<Object> 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();
}
}

View File

@ -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<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> 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)

View File

@ -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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> rejectedRequest = initiateGetRequest(port, "/");
assertThat(shutdownResult.get()).isEqualTo(false);
AtomicReference<GracefulShutdownResult> 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();
}
}

View File

@ -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);
}

View File

@ -97,9 +97,9 @@ class ReactiveWebServerApplicationContextTests {
this.context.addApplicationListener(listener);
this.context.refresh();
List<ApplicationEvent> 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);
}

View File

@ -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<GracefulShutdownResult> 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<GracefulShutdownResult> 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<Boolean> shutdownResult = initiateGracefulShutdown();
assertThat(responseLatch.getCount()).isEqualTo(1);
AtomicReference<GracefulShutdownResult> 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<Boolean> initiateGracefulShutdown() {
RunnableFuture<Boolean> future = new FutureTask<Boolean>(() -> 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<MonoProcessor<Void>> monoProcessors = new ArrayBlockingQueue<>(10);
private volatile boolean blocking = true;
public BlockingHandler() {
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
MonoProcessor<Void> completion = MonoProcessor.create();
this.monoProcessors.add(completion);
return completion.then(Mono.empty());
if (this.blocking) {
MonoProcessor<Void> 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 {

View File

@ -145,9 +145,9 @@ class ServletWebServerApplicationContextTests {
this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class));
this.context.refresh();
List<ApplicationEvent> 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);
}

View File

@ -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<GracefulShutdownResult> 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<Object> 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<Object> request = initiateGetRequest(port, "/blocking");
blockingServlet.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<GracefulShutdownResult> 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<Object> 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<GracefulShutdownResult> 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<Object> request = initiateGetRequest(port, "/blockingAsync");
blockingAsyncServlet.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> 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<Boolean> initiateGracefulShutdown() {
RunnableFuture<Boolean> future = new FutureTask<Boolean>(() -> this.webServer.shutDownGracefully());
new Thread(future).start();
awaitInGracefulShutdown();
return future;
}
protected Future<Object> 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");
}