Add support for gracefully shutting down the web server

This commit adds support for gracefully shutting down the embedded
web server. When a grace period is configured
(server.shutdown.grace-period), upon shutdown, the web server will no
longer permit new requests and will wait for up to the grace period
for active requests to complete.

Closes gh-4657
pull/20434/head
Andy Wilkinson 5 years ago
parent 067accb3a8
commit 308e1d3675

@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit;
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.boot.web.servlet.server.Encoding;
import org.springframework.boot.web.servlet.server.Jsp;
@ -114,6 +115,9 @@ 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();
@ -199,6 +203,10 @@ public class ServerProperties {
return this.http2;
}
public Shutdown getShutdown() {
return this.shutdown;
}
public Servlet getServlet() {
return this.servlet;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -52,6 +52,7 @@ public class ReactiveWebServerFactoryCustomizer
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -60,6 +60,7 @@ public class ServletWebServerFactoryCustomizer
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -17,14 +17,18 @@
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;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ -71,4 +75,14 @@ class ReactiveWebServerFactoryCustomizerTests {
verify(factory).setSsl(ssl);
}
@Test
void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() {
this.properties.getShutdown().setGracePeriod(Duration.ofSeconds(30));
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));
}
}

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.web.servlet;
import java.io.File;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@ -29,6 +30,7 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.server.Jsp;
@ -154,6 +156,18 @@ class ServletWebServerFactoryCustomizerTests {
assertThat(sessionCaptor.getValue().getStoreDir()).isEqualTo(new File("myfolder"));
}
@Test
void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() {
Map<String, String> map = new HashMap<>();
map.put("server.shutdown.grace-period", "30s");
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));
}
private void bindProperties(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("server", Bindable.ofInstance(this.properties));

@ -2960,6 +2960,26 @@ 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.
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.
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:
[source,properties,indent=0,configprops]
----
server.shutdown.grace-period=30s
----
[[boot-features-rsocket]]
== RSocket
https://rsocket.io[RSocket] is a binary protocol for use on byte stream transports.

@ -0,0 +1,87 @@
/*
* 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.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.eclipse.jetty.server.ServerConnector;
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()) {
((ServerConnector) connector).setAccepting(false);
}
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 elaped with " + activeRequests + " request(s) still active");
}
return activeRequests == 0;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -103,7 +103,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);
return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod());
}
@Override

@ -398,7 +398,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
* @return a new {@link JettyWebServer} instance
*/
protected JettyWebServer getJettyWebServer(Server server) {
return new JettyWebServer(server, getPort() >= 0);
return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod());
}
@Override

@ -18,6 +18,7 @@ package org.springframework.boot.web.embedded.jetty;
import java.io.IOException;
import java.net.BindException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@ -32,8 +33,11 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
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.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
@ -63,6 +67,8 @@ public class JettyWebServer implements WebServer {
private final boolean autoStart;
private final GracefulShutdown gracefulShutdown;
private Connector[] connectors;
private volatile boolean started;
@ -81,9 +87,31 @@ 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;
GracefulShutdown gracefulShutdown = null;
if (shutdownGracePeriod != null) {
StatisticsHandler handler = new StatisticsHandler();
handler.setHandler(server.getHandler());
server.setHandler(handler);
gracefulShutdown = new JettyGracefulShutdown(server, handler::getRequestsActive, shutdownGracePeriod);
}
else {
gracefulShutdown = new ImmediateGracefulShutdown();
}
this.gracefulShutdown = gracefulShutdown;
initialize();
}
@ -261,6 +289,15 @@ public class JettyWebServer implements WebServer {
return 0;
}
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
/**
* Returns access to the underlying Jetty Server.
* @return the Jetty server

@ -0,0 +1,116 @@
/*
* 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.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
/**
* {@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 lifecycleTimeout;
private final Duration period;
private final AtomicLong activeRequests = new AtomicLong();
private volatile boolean shuttingDown;
NettyGracefulShutdown(Supplier<DisposableServer> disposableServer, Duration lifecycleTimeout, Duration period) {
this.disposableServer = disposableServer;
this.lifecycleTimeout = lifecycleTimeout;
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;
}
if (this.lifecycleTimeout != null) {
server.disposeNow(this.lifecycleTimeout);
}
else {
server.disposeNow();
}
this.shuttingDown = true;
long end = System.currentTimeMillis() + this.period.toMillis();
try {
while (this.activeRequests.get() > 0 && System.currentTimeMillis() < end) {
try {
Thread.sleep(50);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
long activeRequests = this.activeRequests.get();
if (activeRequests == 0) {
logger.info("Graceful shutdown complete");
return true;
}
if (logger.isInfoEnabled()) {
logger.info("Grace period elaped with " + activeRequests + " request(s) still active");
}
return false;
}
finally {
this.shuttingDown = false;
}
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
BiFunction<? super HttpServerRequest, ? super HttpServerResponse, ? extends Publisher<Void>> wrapHandler(
ReactorHttpHandlerAdapter handlerAdapter) {
if (this.period == null) {
return handlerAdapter;
}
return (request, response) -> {
this.activeRequests.incrementAndGet();
return handlerAdapter.apply(request, response).doOnTerminate(() -> this.activeRequests.decrementAndGet());
};
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -31,6 +31,7 @@ import reactor.netty.resources.LoopResources;
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.ReactorResourceFactory;
import org.springframework.http.server.reactive.HttpHandler;
@ -55,6 +56,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
private ReactorResourceFactory resourceFactory;
private Shutdown shutdown;
public NettyReactiveWebServerFactory() {
}
@ -66,7 +69,8 @@ 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);
NettyWebServer webServer = new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout,
(this.shutdown == null) ? null : this.shutdown.getGracePeriod());
webServer.setRouteProviders(this.routeProviders);
return webServer;
}
@ -136,6 +140,16 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
this.resourceFactory = resourceFactory;
}
@Override
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
@Override
public Shutdown getShutdown() {
return this.shutdown;
}
private HttpServer createHttpServer() {
HttpServer server = HttpServer.create();
if (this.resourceFactory != null) {

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -19,16 +19,21 @@ package org.springframework.boot.web.embedded.netty;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.netty.ChannelBindException;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
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.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
@ -53,20 +58,44 @@ public class NettyWebServer implements WebServer {
private final HttpServer httpServer;
private final ReactorHttpHandlerAdapter handlerAdapter;
private final BiFunction<? super HttpServerRequest, ? super HttpServerResponse, ? extends Publisher<Void>> handler;
private final Duration lifecycleTimeout;
private final GracefulShutdown shutdown;
private List<NettyRouteProvider> routeProviders = Collections.emptyList();
private DisposableServer disposableServer;
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) {
Assert.notNull(httpServer, "HttpServer must not be null");
Assert.notNull(handlerAdapter, "HandlerAdapter must not be null");
this.httpServer = httpServer;
this.handlerAdapter = handlerAdapter;
this.lifecycleTimeout = lifecycleTimeout;
if (shutdownGracePeriod != null) {
NettyGracefulShutdown gracefulShutdown = new NettyGracefulShutdown(() -> this.disposableServer,
lifecycleTimeout, shutdownGracePeriod);
this.handler = gracefulShutdown.wrapHandler(handlerAdapter);
this.shutdown = gracefulShutdown;
}
else {
this.handler = handlerAdapter;
this.shutdown = new ImmediateGracefulShutdown();
}
}
public void setRouteProviders(List<NettyRouteProvider> routeProviders) {
@ -91,10 +120,19 @@ public class NettyWebServer implements WebServer {
}
}
@Override
public boolean shutDownGracefully() {
return this.shutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.shutdown.isShuttingDown();
}
private DisposableServer startHttpServer() {
HttpServer server = this.httpServer;
if (this.routeProviders.isEmpty()) {
server = server.handle(this.handlerAdapter);
server = server.handle(this.handler);
}
else {
server = server.route(this::applyRouteProviders);
@ -109,7 +147,7 @@ public class NettyWebServer implements WebServer {
for (NettyRouteProvider provider : this.routeProviders) {
routes = provider.apply(routes);
}
routes.route(ALWAYS, this.handlerAdapter);
routes.route(ALWAYS, this.handler);
}
private ChannelBindException findBindException(Exception ex) {

@ -0,0 +1,126 @@
/*
* 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.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.catalina.Container;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
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;
import org.springframework.util.ReflectionUtils;
/**
* {@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 elaped 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 {
Field field = ReflectionUtils.findField(context.getClass(), "inProgressAsyncCount");
field.setAccessible(true);
AtomicLong inProgressAsyncCount = (AtomicLong) field.get(context);
if (inProgressAsyncCount.get() > 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()) {
for (Connector connector : service.findConnectors()) {
connectors.add(connector);
}
}
return connectors;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -131,7 +131,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
}
TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler);
prepareContext(tomcat.getHost(), servlet);
return new TomcatWebServer(tomcat, getPort() >= 0);
return getTomcatWebServer(tomcat);
}
private void configureEngine(Engine engine) {
@ -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);
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod());
}
/**

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -435,7 +435,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
* @return a new {@link TomcatWebServer} instance
*/
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0);
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod());
}
@Override

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -17,6 +17,7 @@
package org.springframework.boot.web.embedded.tomcat;
import java.net.BindException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@ -38,6 +39,8 @@ 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.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
@ -66,6 +69,8 @@ public class TomcatWebServer implements WebServer {
private final boolean autoStart;
private final GracefulShutdown gracefulShutdown;
private volatile boolean started;
/**
@ -82,9 +87,22 @@ public class TomcatWebServer implements WebServer {
* @param autoStart if the server should be started
*/
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
this(tomcat, autoStart, null);
}
/**
* 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
* @since 2.3.0
*/
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Duration shutdownGracePeriod) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdownGracePeriod != null) ? new TomcatGracefulShutdown(tomcat, shutdownGracePeriod)
: new ImmediateGracefulShutdown();
initialize();
}
@ -374,4 +392,13 @@ public class TomcatWebServer implements WebServer {
return this.tomcat;
}
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
}

@ -0,0 +1,76 @@
/*
* 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.handlers.GracefulShutdownHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.server.GracefulShutdown;
/**
* {@link GracefulShutdown} for Undertow.
*
* @author Andy Wilkinson
*/
class UndertowGracefulShutdown implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(UndertowGracefulShutdown.class);
private final GracefulShutdownHandler gracefulShutdownHandler;
private final Duration period;
private volatile boolean shuttingDown;
UndertowGracefulShutdown(GracefulShutdownHandler gracefulShutdownHandler, Duration period) {
this.gracefulShutdownHandler = gracefulShutdownHandler;
this.period = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
+ "s for active requests to complete");
this.gracefulShutdownHandler.shutdown();
this.shuttingDown = true;
boolean graceful = false;
try {
graceful = this.gracefulShutdownHandler.awaitShutdown(this.period.toMillis());
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
finally {
this.shuttingDown = false;
}
if (graceful) {
logger.info("Graceful shutdown complete");
return true;
}
logger.info("Grace period elaped with one or more requests still active");
return graceful;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -19,6 +19,7 @@ package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
@ -29,6 +30,7 @@ import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import org.xnio.OptionMap;
@ -38,6 +40,8 @@ import org.xnio.XnioWorker;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter;
import org.springframework.util.Assert;
@ -93,8 +97,29 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
@Override
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
Undertow.Builder builder = createBuilder(getPort());
Closeable closeable = configureHandler(builder, httpHandler);
return new UndertowWebServer(builder, getPort() >= 0, closeable);
HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
Closeable closeable = null;
GracefulShutdown gracefulShutdown = null;
if (isAccessLogEnabled()) {
AccessLogHandlerConfiguration accessLogHandlerConfiguration = configureAccessLogHandler(builder, handler);
closeable = accessLogHandlerConfiguration.closeable;
handler = accessLogHandlerConfiguration.accessLogHandler;
}
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(handler);
Duration gracePeriod = getShutdown().getGracePeriod();
if (gracePeriod != null) {
gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, gracePeriod);
handler = gracefulShutdownHandler;
}
else {
gracefulShutdown = new ImmediateGracefulShutdown();
}
builder.setHandler(handler);
return new UndertowWebServer(builder, getPort() >= 0, closeable, gracefulShutdown);
}
private Undertow.Builder createBuilder(int port) {
@ -123,24 +148,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
return builder;
}
private Closeable configureHandler(Undertow.Builder builder,
org.springframework.http.server.reactive.HttpHandler httpHandler) {
HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
Closeable closeable = null;
if (isAccessLogEnabled()) {
closeable = configureAccessLogHandler(builder, handler);
}
else {
builder.setHandler(handler);
}
return closeable;
}
private Closeable configureAccessLogHandler(Undertow.Builder builder, HttpHandler handler) {
private AccessLogHandlerConfiguration configureAccessLogHandler(Undertow.Builder builder, HttpHandler handler) {
try {
createAccessLogDirectoryIfNecessary();
XnioWorker worker = createWorker();
@ -148,9 +156,9 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(worker, this.accessLogDirectory,
prefix, this.accessLogSuffix, this.accessLogRotate);
String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern : "common");
builder.setHandler(
new AccessLogHandler(handler, accessLogReceiver, formatString, Undertow.class.getClassLoader()));
return () -> {
AccessLogHandler accessLogHandler = new AccessLogHandler(handler, accessLogReceiver, formatString,
Undertow.class.getClassLoader());
return new AccessLogHandlerConfiguration(accessLogHandler, () -> {
try {
accessLogReceiver.close();
worker.shutdown();
@ -162,7 +170,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
};
});
}
catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex);
@ -289,4 +297,17 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
this.builderCustomizers.addAll(Arrays.asList(customizers));
}
private static final class AccessLogHandlerConfiguration {
private final AccessLogHandler accessLogHandler;
private final Closeable closeable;
private AccessLogHandlerConfiguration(AccessLogHandler accessLogHandler, Closeable closeable) {
this.accessLogHandler = accessLogHandler;
this.closeable = closeable;
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -20,6 +20,7 @@ import java.lang.reflect.Field;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@ -29,12 +30,15 @@ import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.servlet.api.DeploymentManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
@ -74,10 +78,14 @@ public class UndertowServletWebServer implements WebServer {
private final String serverHeader;
private final Duration shutdownGracePeriod;
private Undertow undertow;
private volatile boolean started = false;
private volatile GracefulShutdown gracefulShutdown;
/**
* Create a new {@link UndertowServletWebServer} instance.
* @param builder the builder
@ -117,6 +125,25 @@ public class UndertowServletWebServer implements WebServer {
*/
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader) {
this(builder, manager, contextPath, useForwardHeaders, autoStart, compression, serverHeader, null);
}
/**
* Create a new {@link UndertowServletWebServer} instance.
* @param builder the builder
* @param manager the deployment manager
* @param contextPath the root context path
* @param useForwardHeaders if x-forward headers should be used
* @param autoStart if the server should be started
* @param compression compression configuration
* @param serverHeader string to be used in HTTP header
* @param shutdownGracePeriod the period to wait for activity to cease when shutting
* down the server gracefully
* @since 2.3.0
*/
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader,
Duration shutdownGracePeriod) {
this.builder = builder;
this.manager = manager;
this.contextPath = contextPath;
@ -124,6 +151,7 @@ public class UndertowServletWebServer implements WebServer {
this.autoStart = autoStart;
this.compression = compression;
this.serverHeader = serverHeader;
this.shutdownGracePeriod = shutdownGracePeriod;
}
@Override
@ -200,6 +228,14 @@ public class UndertowServletWebServer implements WebServer {
if (StringUtils.hasText(this.serverHeader)) {
httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader);
}
if (this.shutdownGracePeriod != null) {
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(httpHandler);
this.gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, this.shutdownGracePeriod);
httpHandler = gracefulShutdownHandler;
}
else {
this.gracefulShutdown = new ImmediateGracefulShutdown();
}
this.builder.setHandler(httpHandler);
return this.builder.build();
}
@ -314,6 +350,15 @@ public class UndertowServletWebServer implements WebServer {
return ports.get(0).getNumber();
}
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
/**
* An active Undertow port.
*/

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -449,7 +449,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
*/
protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) {
return new UndertowServletWebServer(builder, manager, getContextPath(), isUseForwardHeaders(), port >= 0,
getCompression(), getServerHeader());
getCompression(), getServerHeader(), getShutdown().getGracePeriod());
}
@Override

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -29,6 +29,8 @@ 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.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
@ -59,6 +61,8 @@ public class UndertowWebServer implements WebServer {
private final Closeable closeable;
private final GracefulShutdown gracefulShutdown;
private Undertow undertow;
private volatile boolean started = false;
@ -80,9 +84,23 @@ public class UndertowWebServer implements WebServer {
* @since 2.0.4
*/
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable) {
this(builder, autoStart, closeable, new ImmediateGracefulShutdown());
}
/**
* Create a new {@link UndertowWebServer} instance.
* @param builder the builder
* @param autoStart if the server should be started
* @param closeable called when the server is stopped
* @param gracefulShutdown handler for graceful shutdown
* @since 2.3.0
*/
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable,
GracefulShutdown gracefulShutdown) {
this.builder = builder;
this.autoStart = autoStart;
this.closeable = closeable;
this.gracefulShutdown = gracefulShutdown;
}
@Override
@ -245,6 +263,15 @@ public class UndertowWebServer implements WebServer {
return ports.get(0).getNumber();
}
@Override
public boolean shutDownGracefully() {
return (this.gracefulShutdown != null) ? this.gracefulShutdown.shutDownGracefully() : false;
}
boolean inGracefulShutdown() {
return (this.gracefulShutdown != null) ? this.gracefulShutdown.isShuttingDown() : false;
}
/**
* An active Undertow port.
*/

@ -146,6 +146,15 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli
return getBeanFactory().getBean(beanNames[0], HttpHandler.class);
}
@Override
protected void doClose() {
WebServer webServer = getWebServer();
if (webServer != null) {
webServer.shutDownGracefully();
}
super.doClose();
}
@Override
protected void onClose() {
super.onClose();

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -55,6 +55,8 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
private String serverHeader;
private Shutdown shutdown = new Shutdown();
/**
* Create a new {@link AbstractConfigurableWebServerFactory} instance.
*/
@ -162,6 +164,20 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
this.serverHeader = serverHeader;
}
@Override
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
/**
* Returns the shutdown configuration that will be applied to the server.
* @return the shutdown configuration
* @since 2.3.0
*/
public Shutdown getShutdown() {
return this.shutdown;
}
/**
* Return the absolute temp dir for given web server.
* @param prefix server name

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -80,4 +80,13 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag
*/
void setServerHeader(String serverHeader);
/**
* Sets the shutdown configuration that will be applied to the server.
* @param shutdown the shutdown configuration
* @since 2.3.0
*/
default void setShutdown(Shutdown shutdown) {
}
}

@ -0,0 +1,42 @@
/*
* 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.server;
/**
* Handles graceful shutdown of a {@link WebServer}.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public interface GracefulShutdown {
/**
* 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}
*/
boolean shutDownGracefully();
/**
* 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}.
*/
boolean isShuttingDown();
}

@ -0,0 +1,37 @@
/*
* 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.server;
/**
* A {@link GracefulShutdown} that returns immediately with no grace period.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public class ImmediateGracefulShutdown implements GracefulShutdown {
@Override
public boolean shutDownGracefully() {
return false;
}
@Override
public boolean isShuttingDown() {
return false;
}
}

@ -0,0 +1,43 @@
/*
* 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.server;
import java.time.Duration;
/**
* Configuration for shutting down a {@link WebServer}.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public class Shutdown {
/**
* Time to wait for web activity to cease before shutting down the application. By
* default, shutdown will proceed immediately.
*/
private Duration gracePeriod;
public Duration getGracePeriod() {
return this.gracePeriod;
}
public void setGracePeriod(Duration period) {
this.gracePeriod = period;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -47,4 +47,15 @@ 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}
* @since 2.3.0
*/
default boolean shutDownGracefully() {
return false;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -166,6 +166,15 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon
}
}
@Override
protected void doClose() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.shutDownGracefully();
}
super.doClose();
}
@Override
protected void onClose() {
super.onClose();

@ -91,6 +91,11 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet
this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
}
@Override
protected boolean inGracefulShutdown() {
return ((JettyWebServer) this.webServer).inGracefulShutdown();
}
@Test
void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) {
AbstractServletWebServerFactory factory = getFactory();

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -16,8 +16,14 @@
package org.springframework.boot.web.embedded.jetty;
import java.io.IOException;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
@ -26,11 +32,13 @@ import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.http.client.reactive.JettyResourceFactory;
import org.springframework.http.server.reactive.HttpHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@ -110,4 +118,38 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
assertThat(connector.getScheduler()).isEqualTo(resourceFactory.getScheduler());
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
JettyReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
getWebClient().build().get().retrieve().toBodilessEntity().subscribe();
blockingHandler.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
// We need to make two requests as Jetty accepts one additional request after a
// connector has been told to stop accepting requests
CountDownLatch responseLatch = new CountDownLatch(1);
AtomicReference<Throwable> errorReference = new AtomicReference<>();
getWebClient().build().get().retrieve().toBodilessEntity().doOnSuccess((response) -> responseLatch.countDown())
.doOnError(errorReference::set).subscribe();
getWebClient().build().get().retrieve().toBodilessEntity().doOnSuccess((response) -> responseLatch.countDown())
.doOnError(errorReference::set).subscribe();
assertThat(shutdownResult.get()).isEqualTo(false);
blockingHandler.completeOne();
blockingHandler.completeOne();
responseLatch.await(5, TimeUnit.SECONDS);
this.webServer.stop();
Throwable error = await().atMost(Duration.ofSeconds(5)).until(errorReference::get, (ex) -> ex != null);
assertThat(error).isInstanceOf(IOException.class);
}
@Override
protected boolean inGracefulShutdown() {
return ((JettyWebServer) this.webServer).inGracefulShutdown();
}
}

@ -21,12 +21,17 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration.Dynamic;
import org.apache.http.HttpResponse;
import org.apache.http.conn.HttpHostConnectException;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
@ -44,8 +49,10 @@ import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -169,6 +176,34 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()).containsExactly("TLSv1.1");
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() 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");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
// Jetty accepts one additional request after a connector has been told to stop
// accepting requests
Future<Object> unconnectableRequest1 = initiateGetRequest("/");
Future<Object> unconnectableRequest2 = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
blockingServlet.admitOne();
assertThat(request.get()).isInstanceOf(HttpResponse.class);
this.webServer.stop();
List<Object> results = Arrays.asList(unconnectableRequest1.get(), unconnectableRequest2.get());
assertThat(results).anySatisfy((result) -> assertThat(result).isInstanceOf(HttpHostConnectException.class));
}
private Ssl getSslSettings(String... enabledProtocols) {
Ssl ssl = new Ssl();
ssl.setKeyStore("src/test/resources/test.jks");

@ -16,8 +16,11 @@
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.junit.jupiter.api.Test;
import org.mockito.InOrder;
@ -28,6 +31,7 @@ import reactor.test.StepVerifier;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
@ -99,6 +103,27 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
StepVerifier.create(result).expectNext("Hello World").verifyComplete();
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
NettyReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().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.stop();
assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class);
}
protected Mono<String> testSslWithAlias(String alias) {
String keyStore = "classpath:test.jks";
String keyPassword = "password";
@ -117,4 +142,9 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
.exchange().flatMap((response) -> response.bodyToMono(String.class));
}
@Override
protected boolean inGracefulShutdown() {
return ((NettyWebServer) this.webServer).inGracefulShutdown();
}
}

@ -17,10 +17,14 @@
package org.springframework.boot.web.embedded.tomcat;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
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;
import org.apache.catalina.LifecycleEvent;
@ -41,10 +45,12 @@ import org.mockito.InOrder;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.SocketUtils;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -256,6 +262,27 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto
.isInstanceOf(WebServerException.class);
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
TomcatReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().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.stop();
assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class);
}
private void doWithBlockedPort(BlockedPortAction action) throws IOException {
int port = SocketUtils.findAvailableTcpPort(40000);
ServerSocket serverSocket = new ServerSocket();
@ -280,6 +307,11 @@ 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);

@ -26,6 +26,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.InitialContext;
@ -55,6 +56,8 @@ import org.apache.catalina.util.CharsetMapper;
import org.apache.catalina.valves.RemoteIpValve;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.http.HttpResponse;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.jasper.servlet.JspServlet;
import org.apache.tomcat.JarScanFilter;
import org.apache.tomcat.JarScanType;
@ -66,6 +69,7 @@ import org.mockito.InOrder;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.web.servlet.ExampleServlet;
import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
@ -557,6 +561,30 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
assertThatThrownBy(() -> factory.getWebServer(registration).start()).isInstanceOf(WebServerException.class);
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() 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");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> unconnectableRequest = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
blockingServlet.admitOne();
assertThat(request.get()).isInstanceOf(HttpResponse.class);
this.webServer.stop();
assertThat(unconnectableRequest.get()).isInstanceOf(HttpHostConnectException.class);
}
@Override
protected JspServlet getJspServlet() throws ServletException {
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
@ -610,4 +638,9 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
}
@Override
protected boolean inGracefulShutdown() {
return ((TomcatWebServer) this.webServer).inGracefulShutdown();
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -21,6 +21,8 @@ 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;
@ -30,10 +32,12 @@ 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.MediaType;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException.ServiceUnavailable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -105,6 +109,32 @@ class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFac
testAccessLog("my_access.", "logz", "my_access.logz");
}
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsAreRejectedWithServiceUnavailable() throws Exception {
UndertowReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().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.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)
throws IOException, URISyntaxException, InterruptedException {
UndertowReactiveWebServerFactory factory = getFactory();

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -25,15 +25,18 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.servlet.ServletRegistration.Dynamic;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.ServletContainer;
import org.apache.http.HttpResponse;
import org.apache.jasper.servlet.JspServlet;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
@ -42,6 +45,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.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests;
@ -173,6 +177,33 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto
testAccessLog("my_access.", "logz", "my_access.logz");
}
@Test
void whenServerIsShuttingDownGracefullyThenRequestsAreRejectedWithServiceUnavailable() 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");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> rejectedRequest = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
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())
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value());
}
private void testAccessLog(String prefix, String suffix, String expectedFile)
throws IOException, URISyntaxException, InterruptedException {
UndertowServletWebServerFactory factory = getFactory();
@ -278,4 +309,9 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto
this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
}
@Override
protected boolean inGracefulShutdown() {
return ((UndertowServletWebServer) this.webServer).inGracefulShutdown();
}
}

@ -23,7 +23,15 @@ import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
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;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
@ -38,11 +46,14 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
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.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer;
import org.springframework.core.io.buffer.DataBuffer;
@ -53,6 +64,7 @@ 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;
@ -333,6 +345,78 @@ public abstract class AbstractReactiveWebServerFactoryTests {
.hasMessageContaining("Could not load key store 'null'");
}
@Test
void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(30));
factory.setShutdown(shutdown);
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);
}
@Test
void whenARequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient().build().get().retrieve().toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
long start = System.currentTimeMillis();
assertThat(this.webServer.shutDownGracefully()).isFalse();
long end = System.currentTimeMillis();
assertThat(end - start).isGreaterThanOrEqualTo(5000);
assertThat(responseReference.get()).isNull();
blockingHandler.completeOne();
assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue();
}
@Test
void whenARequestCompletesDuringGracePeriodThenShutDownGracefullyReturnsTrueBeforePeriodElapses() 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);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient().build().get().retrieve().toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
assertThat(responseLatch.getCount()).isEqualTo(1);
blockingHandler.completeOne();
assertThat(shutdownResult.get()).isTrue();
long end = System.currentTimeMillis();
assertThat(end - start).isLessThanOrEqualTo(30000);
assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue();
}
protected WebClient prepareCompressionTest() {
Compression compression = new Compression();
compression.setEnabled(true);
@ -385,6 +469,26 @@ 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 (!this.inGracefulShutdown()) {
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
protected abstract boolean inGracefulShutdown();
protected static class EchoHandler implements HttpHandler {
public EchoHandler() {
@ -398,6 +502,40 @@ public abstract class AbstractReactiveWebServerFactoryTests {
}
protected static class BlockingHandler implements HttpHandler {
private final BlockingQueue<MonoProcessor<Void>> monoProcessors = new ArrayBlockingQueue<>(10);
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());
}
public void completeOne() {
try {
MonoProcessor<Void> processor = this.monoProcessors.take();
System.out.println("Completing " + processor);
processor.onComplete();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
public void awaitQueue() throws InterruptedException {
while (this.monoProcessors.isEmpty()) {
Thread.sleep(100);
}
}
}
static class CompressionDetectionHandler extends ChannelInboundHandlerAdapter {
@Override

@ -44,7 +44,15 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
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;
@ -52,6 +60,7 @@ import java.util.zip.GZIPInputStream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.servlet.AsyncContext;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@ -60,6 +69,7 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.SessionCookieConfig;
@ -69,8 +79,10 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.InputStreamFactory;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
@ -99,6 +111,7 @@ 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.MimeMappings;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ClientAuth;
import org.springframework.boot.web.server.SslStoreProvider;
@ -1002,6 +1015,155 @@ public abstract class AbstractServletWebServerFactoryTests {
.satisfies(this::wrapsFailingServletException);
}
@Test
void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(30));
factory.setShutdown(shutdown);
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);
}
@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();
Future<Object> request = initiateGetRequest("/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()
throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(30));
factory.setShutdown(shutdown);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
blockingServlet.admitOne();
assertThat(shutdownResult.get()).isTrue();
long end = System.currentTimeMillis();
assertThat(end - start).isLessThanOrEqualTo(30000);
assertThat(request.get()).isInstanceOf(HttpResponse.class);
}
@Test
void whenAnAsyncRequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
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();
Future<Object> request = initiateGetRequest("/blockingAsync");
blockingAsyncServlet.awaitQueue();
long start = System.currentTimeMillis();
assertThat(this.webServer.shutDownGracefully()).isFalse();
long end = System.currentTimeMillis();
assertThat(end - start).isGreaterThanOrEqualTo(5000);
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();
Future<Object> request = initiateGetRequest("/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);
}
protected Future<Boolean> initiateGracefulShutdown() {
RunnableFuture<Boolean> future = new FutureTask<Boolean>(() -> this.webServer.shutDownGracefully());
new Thread(future).start();
awaitInGracefulShutdown();
return future;
}
protected Future<Object> initiateGetRequest(String path) {
return initiateGetRequest(HttpClients.createDefault(), path);
}
protected Future<Object> initiateGetRequest(HttpClient httpClient, String path) {
RunnableFuture<Object> getRequest = new FutureTask<>(() -> {
try {
HttpResponse response = httpClient
.execute(new HttpGet("http://localhost:" + this.webServer.getPort() + path));
response.getEntity().getContent().close();
return response;
}
catch (Exception ex) {
return ex;
}
});
new Thread(getRequest, "GET " + path).start();
return getRequest;
}
protected void awaitInGracefulShutdown() {
while (!this.inGracefulShutdown()) {
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
private void wrapsFailingServletException(WebServerException ex) {
Throwable cause = ex.getCause();
while (cause != null) {
@ -1151,6 +1313,8 @@ 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");
}
@ -1315,4 +1479,96 @@ public abstract class AbstractServletWebServerFactoryTests {
}
protected static class BlockingServlet extends HttpServlet {
private final BlockingQueue<CyclicBarrier> barriers = new ArrayBlockingQueue<>(10);
public BlockingServlet() {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
CyclicBarrier barrier = new CyclicBarrier(2);
this.barriers.add(barrier);
try {
barrier.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
throw new ServletException(ex);
}
}
public void admitOne() {
try {
this.barriers.take().await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
throw new RuntimeException(ex);
}
}
public void awaitQueue() throws InterruptedException {
while (this.barriers.isEmpty()) {
Thread.sleep(100);
}
}
public void awaitQueue(int size) throws InterruptedException {
while (this.barriers.size() < size) {
Thread.sleep(100);
}
}
}
static class BlockingAsyncServlet extends HttpServlet {
private final BlockingQueue<CyclicBarrier> barriers = new ArrayBlockingQueue<>(10);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
CyclicBarrier barrier = new CyclicBarrier(2);
this.barriers.add(barrier);
AsyncContext async = req.startAsync();
new Thread(() -> {
try {
barrier.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
}
async.complete();
}).start();
}
private void admitOne() {
try {
this.barriers.take().await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (BrokenBarrierException ex) {
throw new RuntimeException(ex);
}
}
private void awaitQueue() throws InterruptedException {
while (this.barriers.isEmpty()) {
Thread.sleep(100);
}
}
}
}

Loading…
Cancel
Save