Use SmartLifecycle for graceful web server shutdown
Closes gh-21325
This commit is contained in:
parent
c42571ba40
commit
240898121f
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue