Commit 308e1d36 authored by Andy Wilkinson's avatar Andy Wilkinson

Add support for gracefully shutting down the web server

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

Closes gh-4657
parent 067accb3
...@@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty; ...@@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.servlet.server.Encoding; import org.springframework.boot.web.servlet.server.Encoding;
import org.springframework.boot.web.servlet.server.Jsp; import org.springframework.boot.web.servlet.server.Jsp;
...@@ -114,6 +115,9 @@ public class ServerProperties { ...@@ -114,6 +115,9 @@ public class ServerProperties {
@NestedConfigurationProperty @NestedConfigurationProperty
private final Http2 http2 = new Http2(); private final Http2 http2 = new Http2();
@NestedConfigurationProperty
private final Shutdown shutdown = new Shutdown();
private final Servlet servlet = new Servlet(); private final Servlet servlet = new Servlet();
private final Tomcat tomcat = new Tomcat(); private final Tomcat tomcat = new Tomcat();
...@@ -199,6 +203,10 @@ public class ServerProperties { ...@@ -199,6 +203,10 @@ public class ServerProperties {
return this.http2; return this.http2;
} }
public Shutdown getShutdown() {
return this.shutdown;
}
public Servlet getServlet() { public Servlet getServlet() {
return this.servlet; return this.servlet;
} }
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -52,6 +52,7 @@ public class ReactiveWebServerFactoryCustomizer ...@@ -52,6 +52,7 @@ public class ReactiveWebServerFactoryCustomizer
map.from(this.serverProperties::getSsl).to(factory::setSsl); map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties::getCompression).to(factory::setCompression); map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2); map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
} }
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -60,6 +60,7 @@ public class ServletWebServerFactoryCustomizer ...@@ -60,6 +60,7 @@ public class ServletWebServerFactoryCustomizer
map.from(this.serverProperties::getHttp2).to(factory::setHttp2); map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader); map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters); map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
} }
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,14 +17,18 @@ ...@@ -17,14 +17,18 @@
package org.springframework.boot.autoconfigure.web.reactive; package org.springframework.boot.autoconfigure.web.reactive;
import java.net.InetAddress; import java.net.InetAddress;
import java.time.Duration;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
...@@ -71,4 +75,14 @@ class ReactiveWebServerFactoryCustomizerTests { ...@@ -71,4 +75,14 @@ class ReactiveWebServerFactoryCustomizerTests {
verify(factory).setSsl(ssl); verify(factory).setSsl(ssl);
} }
@Test
void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() {
this.properties.getShutdown().setGracePeriod(Duration.ofSeconds(30));
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
this.customizer.customize(factory);
ArgumentCaptor<Shutdown> shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class);
verify(factory).setShutdown(shutdownCaptor.capture());
assertThat(shutdownCaptor.getValue().getGracePeriod()).isEqualTo(Duration.ofSeconds(30));
}
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.web.servlet; package org.springframework.boot.autoconfigure.web.servlet;
import java.io.File; import java.io.File;
import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -29,6 +30,7 @@ import org.springframework.boot.context.properties.bind.Bindable; ...@@ -29,6 +30,7 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.server.Jsp; import org.springframework.boot.web.servlet.server.Jsp;
...@@ -154,6 +156,18 @@ class ServletWebServerFactoryCustomizerTests { ...@@ -154,6 +156,18 @@ class ServletWebServerFactoryCustomizerTests {
assertThat(sessionCaptor.getValue().getStoreDir()).isEqualTo(new File("myfolder")); assertThat(sessionCaptor.getValue().getStoreDir()).isEqualTo(new File("myfolder"));
} }
@Test
void whenGracePeriodPropertyIsSetThenGracePeriodIsCustomized() {
Map<String, String> map = new HashMap<>();
map.put("server.shutdown.grace-period", "30s");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
ArgumentCaptor<Shutdown> shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class);
verify(factory).setShutdown(shutdownCaptor.capture());
assertThat(shutdownCaptor.getValue().getGracePeriod()).isEqualTo(Duration.ofSeconds(30));
}
private void bindProperties(Map<String, String> map) { private void bindProperties(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map); ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("server", Bindable.ofInstance(this.properties)); new Binder(source).bind("server", Bindable.ofInstance(this.properties));
......
...@@ -2960,6 +2960,26 @@ You can learn more about the resource configuration on the client side in the << ...@@ -2960,6 +2960,26 @@ You can learn more about the resource configuration on the client side in the <<
[[boot-features-graceful-shutdown]]
== Graceful shutdown
Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications.
When enabled, shutdown of the application will include a grace period of configurable duration.
During this grace period, existing requests will be allowed to complete but no new requests will be permitted.
The exact way in which new requests are not permitted varies depending on the web server that is being used.
Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer.
Undertow will accept requests but respond immediately with a service unavailable (503) response.
Graceful shutdown occurs as one of the first steps during application close processing and before any beans have been destroyed.
This ensures that the beans are available for use by any processing that occurs while in-flight requests are being allowed to complete.
To enable graceful shutdown, configure the configprop:server.shutdown.grace-period[] property, as shown in the following example:
[source,properties,indent=0,configprops]
----
server.shutdown.grace-period=30s
----
[[boot-features-rsocket]] [[boot-features-rsocket]]
== RSocket == RSocket
https://rsocket.io[RSocket] is a binary protocol for use on byte stream transports. https://rsocket.io[RSocket] is a binary protocol for use on byte stream transports.
......
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.jetty;
import java.time.Duration;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.springframework.boot.web.server.GracefulShutdown;
/**
* {@link GracefulShutdown} for Jetty.
*
* @author Andy Wilkinson
*/
class JettyGracefulShutdown implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(JettyGracefulShutdown.class);
private final Server server;
private final Supplier<Integer> activeRequests;
private final Duration period;
private volatile boolean shuttingDown = false;
JettyGracefulShutdown(Server server, Supplier<Integer> activeRequests, Duration period) {
this.server = server;
this.activeRequests = activeRequests;
this.period = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
+ "s for active requests to complete");
for (Connector connector : this.server.getConnectors()) {
((ServerConnector) connector).setAccepting(false);
}
this.shuttingDown = true;
long end = System.currentTimeMillis() + this.period.toMillis();
while (System.currentTimeMillis() < end && (this.activeRequests.get() > 0)) {
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
this.shuttingDown = false;
long activeRequests = this.activeRequests.get();
if (activeRequests == 0) {
logger.info("Graceful shutdown complete");
return true;
}
if (logger.isInfoEnabled()) {
logger.info("Grace period elaped with " + activeRequests + " request(s) still active");
}
return activeRequests == 0;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -103,7 +103,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -103,7 +103,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
public WebServer getWebServer(HttpHandler httpHandler) { public WebServer getWebServer(HttpHandler httpHandler) {
JettyHttpHandlerAdapter servlet = new JettyHttpHandlerAdapter(httpHandler); JettyHttpHandlerAdapter servlet = new JettyHttpHandlerAdapter(httpHandler);
Server server = createJettyServer(servlet); Server server = createJettyServer(servlet);
return new JettyWebServer(server, getPort() >= 0); return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod());
} }
@Override @Override
......
...@@ -398,7 +398,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor ...@@ -398,7 +398,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
* @return a new {@link JettyWebServer} instance * @return a new {@link JettyWebServer} instance
*/ */
protected JettyWebServer getJettyWebServer(Server server) { protected JettyWebServer getJettyWebServer(Server server) {
return new JettyWebServer(server, getPort() >= 0); return new JettyWebServer(server, getPort() >= 0, getShutdown().getGracePeriod());
} }
@Override @Override
......
...@@ -18,6 +18,7 @@ package org.springframework.boot.web.embedded.jetty; ...@@ -18,6 +18,7 @@ package org.springframework.boot.web.embedded.jetty;
import java.io.IOException; import java.io.IOException;
import java.net.BindException; import java.net.BindException;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
...@@ -32,8 +33,11 @@ import org.eclipse.jetty.server.Server; ...@@ -32,8 +33,11 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
...@@ -63,6 +67,8 @@ public class JettyWebServer implements WebServer { ...@@ -63,6 +67,8 @@ public class JettyWebServer implements WebServer {
private final boolean autoStart; private final boolean autoStart;
private final GracefulShutdown gracefulShutdown;
private Connector[] connectors; private Connector[] connectors;
private volatile boolean started; private volatile boolean started;
...@@ -81,9 +87,31 @@ public class JettyWebServer implements WebServer { ...@@ -81,9 +87,31 @@ public class JettyWebServer implements WebServer {
* @param autoStart if auto-starting the server * @param autoStart if auto-starting the server
*/ */
public JettyWebServer(Server server, boolean autoStart) { 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; this.autoStart = autoStart;
Assert.notNull(server, "Jetty Server must not be null"); Assert.notNull(server, "Jetty Server must not be null");
this.server = server; this.server = server;
GracefulShutdown gracefulShutdown = null;
if (shutdownGracePeriod != null) {
StatisticsHandler handler = new StatisticsHandler();
handler.setHandler(server.getHandler());
server.setHandler(handler);
gracefulShutdown = new JettyGracefulShutdown(server, handler::getRequestsActive, shutdownGracePeriod);
}
else {
gracefulShutdown = new ImmediateGracefulShutdown();
}
this.gracefulShutdown = gracefulShutdown;
initialize(); initialize();
} }
...@@ -261,6 +289,15 @@ public class JettyWebServer implements WebServer { ...@@ -261,6 +289,15 @@ public class JettyWebServer implements WebServer {
return 0; return 0;
} }
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
/** /**
* Returns access to the underlying Jetty Server. * Returns access to the underlying Jetty Server.
* @return the Jetty server * @return the Jetty server
......
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.netty;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
/**
* {@link GracefulShutdown} for a Reactor Netty {@link DisposableServer}.
*
* @author Andy Wilkinson
*/
final class NettyGracefulShutdown implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(NettyGracefulShutdown.class);
private final Supplier<DisposableServer> disposableServer;
private final Duration lifecycleTimeout;
private final Duration period;
private final AtomicLong activeRequests = new AtomicLong();
private volatile boolean shuttingDown;
NettyGracefulShutdown(Supplier<DisposableServer> disposableServer, Duration lifecycleTimeout, Duration period) {
this.disposableServer = disposableServer;
this.lifecycleTimeout = lifecycleTimeout;
this.period = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
+ "s for active requests to complete");
DisposableServer server = this.disposableServer.get();
if (server == null) {
return false;
}
if (this.lifecycleTimeout != null) {
server.disposeNow(this.lifecycleTimeout);
}
else {
server.disposeNow();
}
this.shuttingDown = true;
long end = System.currentTimeMillis() + this.period.toMillis();
try {
while (this.activeRequests.get() > 0 && System.currentTimeMillis() < end) {
try {
Thread.sleep(50);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
long activeRequests = this.activeRequests.get();
if (activeRequests == 0) {
logger.info("Graceful shutdown complete");
return true;
}
if (logger.isInfoEnabled()) {
logger.info("Grace period elaped with " + activeRequests + " request(s) still active");
}
return false;
}
finally {
this.shuttingDown = false;
}
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
BiFunction<? super HttpServerRequest, ? super HttpServerResponse, ? extends Publisher<Void>> wrapHandler(
ReactorHttpHandlerAdapter handlerAdapter) {
if (this.period == null) {
return handlerAdapter;
}
return (request, response) -> {
this.activeRequests.incrementAndGet();
return handlerAdapter.apply(request, response).doOnTerminate(() -> this.activeRequests.decrementAndGet());
};
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -31,6 +31,7 @@ import reactor.netty.resources.LoopResources; ...@@ -31,6 +31,7 @@ import reactor.netty.resources.LoopResources;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; 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.boot.web.server.WebServer;
import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.client.reactive.ReactorResourceFactory;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
...@@ -55,6 +56,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -55,6 +56,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
private ReactorResourceFactory resourceFactory; private ReactorResourceFactory resourceFactory;
private Shutdown shutdown;
public NettyReactiveWebServerFactory() { public NettyReactiveWebServerFactory() {
} }
...@@ -66,7 +69,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -66,7 +69,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
public WebServer getWebServer(HttpHandler httpHandler) { public WebServer getWebServer(HttpHandler httpHandler) {
HttpServer httpServer = createHttpServer(); HttpServer httpServer = createHttpServer();
ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler); ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
NettyWebServer webServer = new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout); NettyWebServer webServer = new NettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout,
(this.shutdown == null) ? null : this.shutdown.getGracePeriod());
webServer.setRouteProviders(this.routeProviders); webServer.setRouteProviders(this.routeProviders);
return webServer; return webServer;
} }
...@@ -136,6 +140,16 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact ...@@ -136,6 +140,16 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
this.resourceFactory = resourceFactory; this.resourceFactory = resourceFactory;
} }
@Override
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
@Override
public Shutdown getShutdown() {
return this.shutdown;
}
private HttpServer createHttpServer() { private HttpServer createHttpServer() {
HttpServer server = HttpServer.create(); HttpServer server = HttpServer.create();
if (this.resourceFactory != null) { if (this.resourceFactory != null) {
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,16 +19,21 @@ package org.springframework.boot.web.embedded.netty; ...@@ -19,16 +19,21 @@ package org.springframework.boot.web.embedded.netty;
import java.time.Duration; import java.time.Duration;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.netty.ChannelBindException; import reactor.netty.ChannelBindException;
import reactor.netty.DisposableServer; import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.HttpServerRequest; import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.http.server.HttpServerResponse;
import reactor.netty.http.server.HttpServerRoutes; import reactor.netty.http.server.HttpServerRoutes;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
...@@ -53,20 +58,44 @@ public class NettyWebServer implements WebServer { ...@@ -53,20 +58,44 @@ public class NettyWebServer implements WebServer {
private final HttpServer httpServer; private final HttpServer httpServer;
private final ReactorHttpHandlerAdapter handlerAdapter; private final BiFunction<? super HttpServerRequest, ? super HttpServerResponse, ? extends Publisher<Void>> handler;
private final Duration lifecycleTimeout; private final Duration lifecycleTimeout;
private final GracefulShutdown shutdown;
private List<NettyRouteProvider> routeProviders = Collections.emptyList(); private List<NettyRouteProvider> routeProviders = Collections.emptyList();
private DisposableServer disposableServer; private volatile DisposableServer disposableServer;
public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout) { public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout) {
this(httpServer, handlerAdapter, lifecycleTimeout, null);
}
/**
* Creates a {@code NettyWebServer}.
* @param httpServer the Reactor Netty HTTP server
* @param handlerAdapter the Spring WebFlux handler adapter
* @param lifecycleTimeout lifecycle timeout
* @param shutdownGracePeriod grace period for handler for graceful shutdown
* @since 2.3.0
*/
public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout,
Duration shutdownGracePeriod) {
Assert.notNull(httpServer, "HttpServer must not be null"); Assert.notNull(httpServer, "HttpServer must not be null");
Assert.notNull(handlerAdapter, "HandlerAdapter must not be null"); Assert.notNull(handlerAdapter, "HandlerAdapter must not be null");
this.httpServer = httpServer; this.httpServer = httpServer;
this.handlerAdapter = handlerAdapter;
this.lifecycleTimeout = lifecycleTimeout; this.lifecycleTimeout = lifecycleTimeout;
if (shutdownGracePeriod != null) {
NettyGracefulShutdown gracefulShutdown = new NettyGracefulShutdown(() -> this.disposableServer,
lifecycleTimeout, shutdownGracePeriod);
this.handler = gracefulShutdown.wrapHandler(handlerAdapter);
this.shutdown = gracefulShutdown;
}
else {
this.handler = handlerAdapter;
this.shutdown = new ImmediateGracefulShutdown();
}
} }
public void setRouteProviders(List<NettyRouteProvider> routeProviders) { public void setRouteProviders(List<NettyRouteProvider> routeProviders) {
...@@ -91,10 +120,19 @@ public class NettyWebServer implements WebServer { ...@@ -91,10 +120,19 @@ public class NettyWebServer implements WebServer {
} }
} }
@Override
public boolean shutDownGracefully() {
return this.shutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.shutdown.isShuttingDown();
}
private DisposableServer startHttpServer() { private DisposableServer startHttpServer() {
HttpServer server = this.httpServer; HttpServer server = this.httpServer;
if (this.routeProviders.isEmpty()) { if (this.routeProviders.isEmpty()) {
server = server.handle(this.handlerAdapter); server = server.handle(this.handler);
} }
else { else {
server = server.route(this::applyRouteProviders); server = server.route(this::applyRouteProviders);
...@@ -109,7 +147,7 @@ public class NettyWebServer implements WebServer { ...@@ -109,7 +147,7 @@ public class NettyWebServer implements WebServer {
for (NettyRouteProvider provider : this.routeProviders) { for (NettyRouteProvider provider : this.routeProviders) {
routes = provider.apply(routes); routes = provider.apply(routes);
} }
routes.route(ALWAYS, this.handlerAdapter); routes.route(ALWAYS, this.handler);
} }
private ChannelBindException findBindException(Exception ex) { private ChannelBindException findBindException(Exception ex) {
......
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.tomcat;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.catalina.Container;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.util.ReflectionUtils;
/**
* {@link GracefulShutdown} for {@link Tomcat}.
*
* @author Andy Wilkinson
*/
class TomcatGracefulShutdown implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(TomcatGracefulShutdown.class);
private final Tomcat tomcat;
private final Duration period;
private volatile boolean shuttingDown = false;
TomcatGracefulShutdown(Tomcat tomcat, Duration period) {
this.tomcat = tomcat;
this.period = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
+ "s for active requests to complete");
List<Connector> connectors = getConnectors();
for (Connector connector : connectors) {
connector.pause();
connector.getProtocolHandler().closeServerSocketGraceful();
}
this.shuttingDown = true;
try {
long end = System.currentTimeMillis() + this.period.toMillis();
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
while (active(context)) {
if (System.currentTimeMillis() > end) {
logger.info("Grace period elaped with one or more requests still active");
return false;
}
Thread.sleep(50);
}
}
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
finally {
this.shuttingDown = false;
}
logger.info("Graceful shutdown complete");
return true;
}
private boolean active(Container context) {
try {
Field field = ReflectionUtils.findField(context.getClass(), "inProgressAsyncCount");
field.setAccessible(true);
AtomicLong inProgressAsyncCount = (AtomicLong) field.get(context);
if (inProgressAsyncCount.get() > 0) {
return true;
}
for (Container wrapper : context.findChildren()) {
if (((StandardWrapper) wrapper).getCountAllocated() > 0) {
return true;
}
}
return false;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private List<Connector> getConnectors() {
List<Connector> connectors = new ArrayList<>();
for (Service service : this.tomcat.getServer().findServices()) {
for (Connector connector : service.findConnectors()) {
connectors.add(connector);
}
}
return connectors;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -131,7 +131,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac ...@@ -131,7 +131,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
} }
TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler);
prepareContext(tomcat.getHost(), servlet); prepareContext(tomcat.getHost(), servlet);
return new TomcatWebServer(tomcat, getPort() >= 0); return getTomcatWebServer(tomcat);
} }
private void configureEngine(Engine engine) { private void configureEngine(Engine engine) {
...@@ -413,7 +413,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac ...@@ -413,7 +413,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
* @return a new {@link TomcatWebServer} instance * @return a new {@link TomcatWebServer} instance
*/ */
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0); return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod());
} }
/** /**
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -435,7 +435,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto ...@@ -435,7 +435,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
* @return a new {@link TomcatWebServer} instance * @return a new {@link TomcatWebServer} instance
*/ */
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0); return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown().getGracePeriod());
} }
@Override @Override
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.boot.web.embedded.tomcat; package org.springframework.boot.web.embedded.tomcat;
import java.net.BindException; import java.net.BindException;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -38,6 +39,8 @@ import org.apache.commons.logging.Log; ...@@ -38,6 +39,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.naming.ContextBindings; import org.apache.naming.ContextBindings;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
...@@ -66,6 +69,8 @@ public class TomcatWebServer implements WebServer { ...@@ -66,6 +69,8 @@ public class TomcatWebServer implements WebServer {
private final boolean autoStart; private final boolean autoStart;
private final GracefulShutdown gracefulShutdown;
private volatile boolean started; private volatile boolean started;
/** /**
...@@ -82,9 +87,22 @@ public class TomcatWebServer implements WebServer { ...@@ -82,9 +87,22 @@ public class TomcatWebServer implements WebServer {
* @param autoStart if the server should be started * @param autoStart if the server should be started
*/ */
public TomcatWebServer(Tomcat tomcat, boolean autoStart) { public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
this(tomcat, autoStart, null);
}
/**
* Create a new {@link TomcatWebServer} instance.
* @param tomcat the underlying Tomcat server
* @param autoStart if the server should be started
* @param shutdownGracePeriod grace period to use when shutting down
* @since 2.3.0
*/
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Duration shutdownGracePeriod) {
Assert.notNull(tomcat, "Tomcat Server must not be null"); Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat; this.tomcat = tomcat;
this.autoStart = autoStart; this.autoStart = autoStart;
this.gracefulShutdown = (shutdownGracePeriod != null) ? new TomcatGracefulShutdown(tomcat, shutdownGracePeriod)
: new ImmediateGracefulShutdown();
initialize(); initialize();
} }
...@@ -374,4 +392,13 @@ public class TomcatWebServer implements WebServer { ...@@ -374,4 +392,13 @@ public class TomcatWebServer implements WebServer {
return this.tomcat; return this.tomcat;
} }
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
} }
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.embedded.undertow;
import java.time.Duration;
import io.undertow.server.handlers.GracefulShutdownHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.server.GracefulShutdown;
/**
* {@link GracefulShutdown} for Undertow.
*
* @author Andy Wilkinson
*/
class UndertowGracefulShutdown implements GracefulShutdown {
private static final Log logger = LogFactory.getLog(UndertowGracefulShutdown.class);
private final GracefulShutdownHandler gracefulShutdownHandler;
private final Duration period;
private volatile boolean shuttingDown;
UndertowGracefulShutdown(GracefulShutdownHandler gracefulShutdownHandler, Duration period) {
this.gracefulShutdownHandler = gracefulShutdownHandler;
this.period = period;
}
@Override
public boolean shutDownGracefully() {
logger.info("Commencing graceful shutdown, allowing up to " + this.period.getSeconds()
+ "s for active requests to complete");
this.gracefulShutdownHandler.shutdown();
this.shuttingDown = true;
boolean graceful = false;
try {
graceful = this.gracefulShutdownHandler.awaitShutdown(this.period.toMillis());
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
finally {
this.shuttingDown = false;
}
if (graceful) {
logger.info("Graceful shutdown complete");
return true;
}
logger.info("Grace period elaped with one or more requests still active");
return graceful;
}
@Override
public boolean isShuttingDown() {
return this.shuttingDown;
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,6 +19,7 @@ package org.springframework.boot.web.embedded.undertow; ...@@ -19,6 +19,7 @@ package org.springframework.boot.web.embedded.undertow;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
...@@ -29,6 +30,7 @@ import io.undertow.Handlers; ...@@ -29,6 +30,7 @@ import io.undertow.Handlers;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.UndertowOptions; import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.accesslog.AccessLogHandler; import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import org.xnio.OptionMap; import org.xnio.OptionMap;
...@@ -38,6 +40,8 @@ import org.xnio.XnioWorker; ...@@ -38,6 +40,8 @@ import org.xnio.XnioWorker;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -93,8 +97,29 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF ...@@ -93,8 +97,29 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
@Override @Override
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) { public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
Undertow.Builder builder = createBuilder(getPort()); Undertow.Builder builder = createBuilder(getPort());
Closeable closeable = configureHandler(builder, httpHandler); HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
return new UndertowWebServer(builder, getPort() >= 0, closeable); if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
Closeable closeable = null;
GracefulShutdown gracefulShutdown = null;
if (isAccessLogEnabled()) {
AccessLogHandlerConfiguration accessLogHandlerConfiguration = configureAccessLogHandler(builder, handler);
closeable = accessLogHandlerConfiguration.closeable;
handler = accessLogHandlerConfiguration.accessLogHandler;
}
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(handler);
Duration gracePeriod = getShutdown().getGracePeriod();
if (gracePeriod != null) {
gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, gracePeriod);
handler = gracefulShutdownHandler;
}
else {
gracefulShutdown = new ImmediateGracefulShutdown();
}
builder.setHandler(handler);
return new UndertowWebServer(builder, getPort() >= 0, closeable, gracefulShutdown);
} }
private Undertow.Builder createBuilder(int port) { private Undertow.Builder createBuilder(int port) {
...@@ -123,24 +148,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF ...@@ -123,24 +148,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
return builder; return builder;
} }
private Closeable configureHandler(Undertow.Builder builder, private AccessLogHandlerConfiguration configureAccessLogHandler(Undertow.Builder builder, HttpHandler handler) {
org.springframework.http.server.reactive.HttpHandler httpHandler) {
HttpHandler handler = new UndertowHttpHandlerAdapter(httpHandler);
if (this.useForwardHeaders) {
handler = Handlers.proxyPeerAddress(handler);
}
handler = UndertowCompressionConfigurer.configureCompression(getCompression(), handler);
Closeable closeable = null;
if (isAccessLogEnabled()) {
closeable = configureAccessLogHandler(builder, handler);
}
else {
builder.setHandler(handler);
}
return closeable;
}
private Closeable configureAccessLogHandler(Undertow.Builder builder, HttpHandler handler) {
try { try {
createAccessLogDirectoryIfNecessary(); createAccessLogDirectoryIfNecessary();
XnioWorker worker = createWorker(); XnioWorker worker = createWorker();
...@@ -148,9 +156,9 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF ...@@ -148,9 +156,9 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(worker, this.accessLogDirectory, DefaultAccessLogReceiver accessLogReceiver = new DefaultAccessLogReceiver(worker, this.accessLogDirectory,
prefix, this.accessLogSuffix, this.accessLogRotate); prefix, this.accessLogSuffix, this.accessLogRotate);
String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern : "common"); String formatString = ((this.accessLogPattern != null) ? this.accessLogPattern : "common");
builder.setHandler( AccessLogHandler accessLogHandler = new AccessLogHandler(handler, accessLogReceiver, formatString,
new AccessLogHandler(handler, accessLogReceiver, formatString, Undertow.class.getClassLoader())); Undertow.class.getClassLoader());
return () -> { return new AccessLogHandlerConfiguration(accessLogHandler, () -> {
try { try {
accessLogReceiver.close(); accessLogReceiver.close();
worker.shutdown(); worker.shutdown();
...@@ -162,7 +170,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF ...@@ -162,7 +170,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
catch (InterruptedException ex) { catch (InterruptedException ex) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
}; });
} }
catch (IOException ex) { catch (IOException ex) {
throw new IllegalStateException("Failed to create AccessLogHandler", ex); throw new IllegalStateException("Failed to create AccessLogHandler", ex);
...@@ -289,4 +297,17 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF ...@@ -289,4 +297,17 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
this.builderCustomizers.addAll(Arrays.asList(customizers)); this.builderCustomizers.addAll(Arrays.asList(customizers));
} }
private static final class AccessLogHandlerConfiguration {
private final AccessLogHandler accessLogHandler;
private final Closeable closeable;
private AccessLogHandlerConfiguration(AccessLogHandler accessLogHandler, Closeable closeable) {
this.accessLogHandler = accessLogHandler;
this.closeable = closeable;
}
}
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -20,6 +20,7 @@ import java.lang.reflect.Field; ...@@ -20,6 +20,7 @@ import java.lang.reflect.Field;
import java.net.BindException; import java.net.BindException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -29,12 +30,15 @@ import io.undertow.Handlers; ...@@ -29,12 +30,15 @@ import io.undertow.Handlers;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.Undertow.Builder; import io.undertow.Undertow.Builder;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.DeploymentManager;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel; import org.xnio.channels.BoundChannel;
import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
...@@ -74,10 +78,14 @@ public class UndertowServletWebServer implements WebServer { ...@@ -74,10 +78,14 @@ public class UndertowServletWebServer implements WebServer {
private final String serverHeader; private final String serverHeader;
private final Duration shutdownGracePeriod;
private Undertow undertow; private Undertow undertow;
private volatile boolean started = false; private volatile boolean started = false;
private volatile GracefulShutdown gracefulShutdown;
/** /**
* Create a new {@link UndertowServletWebServer} instance. * Create a new {@link UndertowServletWebServer} instance.
* @param builder the builder * @param builder the builder
...@@ -117,6 +125,25 @@ public class UndertowServletWebServer implements WebServer { ...@@ -117,6 +125,25 @@ public class UndertowServletWebServer implements WebServer {
*/ */
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath, public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader) { boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader) {
this(builder, manager, contextPath, useForwardHeaders, autoStart, compression, serverHeader, null);
}
/**
* Create a new {@link UndertowServletWebServer} instance.
* @param builder the builder
* @param manager the deployment manager
* @param contextPath the root context path
* @param useForwardHeaders if x-forward headers should be used
* @param autoStart if the server should be started
* @param compression compression configuration
* @param serverHeader string to be used in HTTP header
* @param shutdownGracePeriod the period to wait for activity to cease when shutting
* down the server gracefully
* @since 2.3.0
*/
public UndertowServletWebServer(Builder builder, DeploymentManager manager, String contextPath,
boolean useForwardHeaders, boolean autoStart, Compression compression, String serverHeader,
Duration shutdownGracePeriod) {
this.builder = builder; this.builder = builder;
this.manager = manager; this.manager = manager;
this.contextPath = contextPath; this.contextPath = contextPath;
...@@ -124,6 +151,7 @@ public class UndertowServletWebServer implements WebServer { ...@@ -124,6 +151,7 @@ public class UndertowServletWebServer implements WebServer {
this.autoStart = autoStart; this.autoStart = autoStart;
this.compression = compression; this.compression = compression;
this.serverHeader = serverHeader; this.serverHeader = serverHeader;
this.shutdownGracePeriod = shutdownGracePeriod;
} }
@Override @Override
...@@ -200,6 +228,14 @@ public class UndertowServletWebServer implements WebServer { ...@@ -200,6 +228,14 @@ public class UndertowServletWebServer implements WebServer {
if (StringUtils.hasText(this.serverHeader)) { if (StringUtils.hasText(this.serverHeader)) {
httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader); httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader);
} }
if (this.shutdownGracePeriod != null) {
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(httpHandler);
this.gracefulShutdown = new UndertowGracefulShutdown(gracefulShutdownHandler, this.shutdownGracePeriod);
httpHandler = gracefulShutdownHandler;
}
else {
this.gracefulShutdown = new ImmediateGracefulShutdown();
}
this.builder.setHandler(httpHandler); this.builder.setHandler(httpHandler);
return this.builder.build(); return this.builder.build();
} }
...@@ -314,6 +350,15 @@ public class UndertowServletWebServer implements WebServer { ...@@ -314,6 +350,15 @@ public class UndertowServletWebServer implements WebServer {
return ports.get(0).getNumber(); return ports.get(0).getNumber();
} }
@Override
public boolean shutDownGracefully() {
return this.gracefulShutdown.shutDownGracefully();
}
boolean inGracefulShutdown() {
return this.gracefulShutdown.isShuttingDown();
}
/** /**
* An active Undertow port. * An active Undertow port.
*/ */
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -449,7 +449,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac ...@@ -449,7 +449,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
*/ */
protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) { protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) {
return new UndertowServletWebServer(builder, manager, getContextPath(), isUseForwardHeaders(), port >= 0, return new UndertowServletWebServer(builder, manager, getContextPath(), isUseForwardHeaders(), port >= 0,
getCompression(), getServerHeader()); getCompression(), getServerHeader(), getShutdown().getGracePeriod());
} }
@Override @Override
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -29,6 +29,8 @@ import org.apache.commons.logging.Log; ...@@ -29,6 +29,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.xnio.channels.BoundChannel; import org.xnio.channels.BoundChannel;
import org.springframework.boot.web.server.GracefulShutdown;
import org.springframework.boot.web.server.ImmediateGracefulShutdown;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
...@@ -59,6 +61,8 @@ public class UndertowWebServer implements WebServer { ...@@ -59,6 +61,8 @@ public class UndertowWebServer implements WebServer {
private final Closeable closeable; private final Closeable closeable;
private final GracefulShutdown gracefulShutdown;
private Undertow undertow; private Undertow undertow;
private volatile boolean started = false; private volatile boolean started = false;
...@@ -80,9 +84,23 @@ public class UndertowWebServer implements WebServer { ...@@ -80,9 +84,23 @@ public class UndertowWebServer implements WebServer {
* @since 2.0.4 * @since 2.0.4
*/ */
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable) { public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable) {
this(builder, autoStart, closeable, new ImmediateGracefulShutdown());
}
/**
* Create a new {@link UndertowWebServer} instance.
* @param builder the builder
* @param autoStart if the server should be started
* @param closeable called when the server is stopped
* @param gracefulShutdown handler for graceful shutdown
* @since 2.3.0
*/
public UndertowWebServer(Undertow.Builder builder, boolean autoStart, Closeable closeable,
GracefulShutdown gracefulShutdown) {
this.builder = builder; this.builder = builder;
this.autoStart = autoStart; this.autoStart = autoStart;
this.closeable = closeable; this.closeable = closeable;
this.gracefulShutdown = gracefulShutdown;
} }
@Override @Override
...@@ -245,6 +263,15 @@ public class UndertowWebServer implements WebServer { ...@@ -245,6 +263,15 @@ public class UndertowWebServer implements WebServer {
return ports.get(0).getNumber(); return ports.get(0).getNumber();
} }
@Override
public boolean shutDownGracefully() {
return (this.gracefulShutdown != null) ? this.gracefulShutdown.shutDownGracefully() : false;
}
boolean inGracefulShutdown() {
return (this.gracefulShutdown != null) ? this.gracefulShutdown.isShuttingDown() : false;
}
/** /**
* An active Undertow port. * An active Undertow port.
*/ */
......
...@@ -146,6 +146,15 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli ...@@ -146,6 +146,15 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli
return getBeanFactory().getBean(beanNames[0], HttpHandler.class); return getBeanFactory().getBean(beanNames[0], HttpHandler.class);
} }
@Override
protected void doClose() {
WebServer webServer = getWebServer();
if (webServer != null) {
webServer.shutDownGracefully();
}
super.doClose();
}
@Override @Override
protected void onClose() { protected void onClose() {
super.onClose(); super.onClose();
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -55,6 +55,8 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab ...@@ -55,6 +55,8 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
private String serverHeader; private String serverHeader;
private Shutdown shutdown = new Shutdown();
/** /**
* Create a new {@link AbstractConfigurableWebServerFactory} instance. * Create a new {@link AbstractConfigurableWebServerFactory} instance.
*/ */
...@@ -162,6 +164,20 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab ...@@ -162,6 +164,20 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
this.serverHeader = serverHeader; this.serverHeader = serverHeader;
} }
@Override
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
/**
* Returns the shutdown configuration that will be applied to the server.
* @return the shutdown configuration
* @since 2.3.0
*/
public Shutdown getShutdown() {
return this.shutdown;
}
/** /**
* Return the absolute temp dir for given web server. * Return the absolute temp dir for given web server.
* @param prefix server name * @param prefix server name
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -80,4 +80,13 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag ...@@ -80,4 +80,13 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag
*/ */
void setServerHeader(String serverHeader); void setServerHeader(String serverHeader);
/**
* Sets the shutdown configuration that will be applied to the server.
* @param shutdown the shutdown configuration
* @since 2.3.0
*/
default void setShutdown(Shutdown shutdown) {
}
} }
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server;
/**
* Handles graceful shutdown of a {@link WebServer}.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public interface GracefulShutdown {
/**
* Shuts down the {@link WebServer}, returning {@code true} if activity ceased during
* the grace period, otherwise {@code false}.
* @return {@code true} if activity ceased during the grace period, otherwise
* {@code false}
*/
boolean shutDownGracefully();
/**
* Returns whether the handler is in the process of gracefully shutting down the web
* server.
* @return {@code true} is graceful shutdown is in progress, otherwise {@code false}.
*/
boolean isShuttingDown();
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server;
/**
* A {@link GracefulShutdown} that returns immediately with no grace period.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public class ImmediateGracefulShutdown implements GracefulShutdown {
@Override
public boolean shutDownGracefully() {
return false;
}
@Override
public boolean isShuttingDown() {
return false;
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server;
import java.time.Duration;
/**
* Configuration for shutting down a {@link WebServer}.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public class Shutdown {
/**
* Time to wait for web activity to cease before shutting down the application. By
* default, shutdown will proceed immediately.
*/
private Duration gracePeriod;
public Duration getGracePeriod() {
return this.gracePeriod;
}
public void setGracePeriod(Duration period) {
this.gracePeriod = period;
}
}
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -47,4 +47,15 @@ public interface WebServer { ...@@ -47,4 +47,15 @@ public interface WebServer {
*/ */
int getPort(); int getPort();
/**
* Gracefully shuts down the web server by preventing the handling of new requests and
* waiting for a configurable period for there to be no active requests.
* @return {@code true} if graceful shutdown completed within the period, otherwise
* {@code false}
* @since 2.3.0
*/
default boolean shutDownGracefully() {
return false;
}
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -166,6 +166,15 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon ...@@ -166,6 +166,15 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon
} }
} }
@Override
protected void doClose() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.shutDownGracefully();
}
super.doClose();
}
@Override @Override
protected void onClose() { protected void onClose() {
super.onClose(); super.onClose();
......
...@@ -91,6 +91,11 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet ...@@ -91,6 +91,11 @@ abstract class AbstractJettyServletWebServerFactoryTests extends AbstractServlet
this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort); this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
} }
@Override
protected boolean inGracefulShutdown() {
return ((JettyWebServer) this.webServer).inGracefulShutdown();
}
@Test @Test
void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) { void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) {
AbstractServletWebServerFactory factory = getFactory(); AbstractServletWebServerFactory factory = getFactory();
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,8 +16,14 @@ ...@@ -16,8 +16,14 @@
package org.springframework.boot.web.embedded.jetty; package org.springframework.boot.web.embedded.jetty;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
...@@ -26,11 +32,13 @@ import org.junit.jupiter.api.Test; ...@@ -26,11 +32,13 @@ import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.client.reactive.JettyResourceFactory;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
...@@ -110,4 +118,38 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor ...@@ -110,4 +118,38 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
assertThat(connector.getScheduler()).isEqualTo(resourceFactory.getScheduler()); assertThat(connector.getScheduler()).isEqualTo(resourceFactory.getScheduler());
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
JettyReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
getWebClient().build().get().retrieve().toBodilessEntity().subscribe();
blockingHandler.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
// We need to make two requests as Jetty accepts one additional request after a
// connector has been told to stop accepting requests
CountDownLatch responseLatch = new CountDownLatch(1);
AtomicReference<Throwable> errorReference = new AtomicReference<>();
getWebClient().build().get().retrieve().toBodilessEntity().doOnSuccess((response) -> responseLatch.countDown())
.doOnError(errorReference::set).subscribe();
getWebClient().build().get().retrieve().toBodilessEntity().doOnSuccess((response) -> responseLatch.countDown())
.doOnError(errorReference::set).subscribe();
assertThat(shutdownResult.get()).isEqualTo(false);
blockingHandler.completeOne();
blockingHandler.completeOne();
responseLatch.await(5, TimeUnit.SECONDS);
this.webServer.stop();
Throwable error = await().atMost(Duration.ofSeconds(5)).until(errorReference::get, (ex) -> ex != null);
assertThat(error).isInstanceOf(IOException.class);
}
@Override
protected boolean inGracefulShutdown() {
return ((JettyWebServer) this.webServer).inGracefulShutdown();
}
} }
...@@ -21,12 +21,17 @@ import java.time.Duration; ...@@ -21,12 +21,17 @@ import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener; import javax.servlet.ServletContextListener;
import javax.servlet.ServletRegistration.Dynamic;
import org.apache.http.HttpResponse;
import org.apache.http.conn.HttpHostConnectException;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
...@@ -44,8 +49,10 @@ import org.eclipse.jetty.webapp.WebAppContext; ...@@ -44,8 +49,10 @@ import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
...@@ -169,6 +176,34 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac ...@@ -169,6 +176,34 @@ class JettyServletWebServerFactoryTests extends AbstractJettyServletWebServerFac
assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()).containsExactly("TLSv1.1"); assertThat(connectionFactory.getSslContextFactory().getIncludeProtocols()).containsExactly("TLSv1.1");
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
// Jetty accepts one additional request after a connector has been told to stop
// accepting requests
Future<Object> unconnectableRequest1 = initiateGetRequest("/");
Future<Object> unconnectableRequest2 = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
blockingServlet.admitOne();
assertThat(request.get()).isInstanceOf(HttpResponse.class);
this.webServer.stop();
List<Object> results = Arrays.asList(unconnectableRequest1.get(), unconnectableRequest2.get());
assertThat(results).anySatisfy((result) -> assertThat(result).isInstanceOf(HttpHostConnectException.class));
}
private Ssl getSslSettings(String... enabledProtocols) { private Ssl getSslSettings(String... enabledProtocols) {
Ssl ssl = new Ssl(); Ssl ssl = new Ssl();
ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyStore("src/test/resources/test.jks");
......
...@@ -16,8 +16,11 @@ ...@@ -16,8 +16,11 @@
package org.springframework.boot.web.embedded.netty; package org.springframework.boot.web.embedded.netty;
import java.net.ConnectException;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
...@@ -28,6 +31,7 @@ import reactor.test.StepVerifier; ...@@ -28,6 +31,7 @@ import reactor.test.StepVerifier;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
...@@ -99,6 +103,27 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor ...@@ -99,6 +103,27 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
StepVerifier.create(result).expectNext("Hello World").verifyComplete(); StepVerifier.create(result).expectNext("Hello World").verifyComplete();
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
NettyReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().build();
webClient.get().retrieve().toBodilessEntity().subscribe();
blockingHandler.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> errorReference = new AtomicReference<>();
webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe();
assertThat(shutdownResult.get()).isEqualTo(false);
blockingHandler.completeOne();
this.webServer.stop();
assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class);
}
protected Mono<String> testSslWithAlias(String alias) { protected Mono<String> testSslWithAlias(String alias) {
String keyStore = "classpath:test.jks"; String keyStore = "classpath:test.jks";
String keyPassword = "password"; String keyPassword = "password";
...@@ -117,4 +142,9 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor ...@@ -117,4 +142,9 @@ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
.exchange().flatMap((response) -> response.bodyToMono(String.class)); .exchange().flatMap((response) -> response.bodyToMono(String.class));
} }
@Override
protected boolean inGracefulShutdown() {
return ((NettyWebServer) this.webServer).inGracefulShutdown();
}
} }
...@@ -17,10 +17,14 @@ ...@@ -17,10 +17,14 @@
package org.springframework.boot.web.embedded.tomcat; package org.springframework.boot.web.embedded.tomcat;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.catalina.Context; import org.apache.catalina.Context;
import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleEvent;
...@@ -41,10 +45,12 @@ import org.mockito.InOrder; ...@@ -41,10 +45,12 @@ import org.mockito.InOrder;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.SocketUtils; import org.springframework.util.SocketUtils;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
...@@ -256,6 +262,27 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto ...@@ -256,6 +262,27 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto
.isInstanceOf(WebServerException.class); .isInstanceOf(WebServerException.class);
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
TomcatReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().build();
webClient.get().retrieve().toBodilessEntity().subscribe();
blockingHandler.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> errorReference = new AtomicReference<>();
webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe();
assertThat(shutdownResult.get()).isEqualTo(false);
blockingHandler.completeOne();
this.webServer.stop();
assertThat(errorReference.get()).hasCauseInstanceOf(ConnectException.class);
}
private void doWithBlockedPort(BlockedPortAction action) throws IOException { private void doWithBlockedPort(BlockedPortAction action) throws IOException {
int port = SocketUtils.findAvailableTcpPort(40000); int port = SocketUtils.findAvailableTcpPort(40000);
ServerSocket serverSocket = new ServerSocket(); ServerSocket serverSocket = new ServerSocket();
...@@ -280,6 +307,11 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto ...@@ -280,6 +307,11 @@ class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFacto
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort); assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
} }
@Override
protected boolean inGracefulShutdown() {
return ((TomcatWebServer) this.webServer).inGracefulShutdown();
}
interface BlockedPortAction { interface BlockedPortAction {
void run(int port); void run(int port);
......
...@@ -26,6 +26,7 @@ import java.util.Arrays; ...@@ -26,6 +26,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.naming.InitialContext; import javax.naming.InitialContext;
...@@ -55,6 +56,8 @@ import org.apache.catalina.util.CharsetMapper; ...@@ -55,6 +56,8 @@ import org.apache.catalina.util.CharsetMapper;
import org.apache.catalina.valves.RemoteIpValve; import org.apache.catalina.valves.RemoteIpValve;
import org.apache.coyote.ProtocolHandler; import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.http.HttpResponse;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.jasper.servlet.JspServlet; import org.apache.jasper.servlet.JspServlet;
import org.apache.tomcat.JarScanFilter; import org.apache.tomcat.JarScanFilter;
import org.apache.tomcat.JarScanType; import org.apache.tomcat.JarScanType;
...@@ -66,6 +69,7 @@ import org.mockito.InOrder; ...@@ -66,6 +69,7 @@ import org.mockito.InOrder;
import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.web.servlet.ExampleServlet; import org.springframework.boot.testsupport.web.servlet.ExampleServlet;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
...@@ -557,6 +561,30 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory ...@@ -557,6 +561,30 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
assertThatThrownBy(() -> factory.getWebServer(registration).start()).isInstanceOf(WebServerException.class); assertThatThrownBy(() -> factory.getWebServer(registration).start()).isInstanceOf(WebServerException.class);
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> unconnectableRequest = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
blockingServlet.admitOne();
assertThat(request.get()).isInstanceOf(HttpResponse.class);
this.webServer.stop();
assertThat(unconnectableRequest.get()).isInstanceOf(HttpHostConnectException.class);
}
@Override @Override
protected JspServlet getJspServlet() throws ServletException { protected JspServlet getJspServlet() throws ServletException {
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
...@@ -610,4 +638,9 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory ...@@ -610,4 +638,9 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort); assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
} }
@Override
protected boolean inGracefulShutdown() {
return ((TomcatWebServer) this.webServer).inGracefulShutdown();
}
} }
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -21,6 +21,8 @@ import java.io.IOException; ...@@ -21,6 +21,8 @@ import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import io.undertow.Undertow; import io.undertow.Undertow;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
...@@ -30,10 +32,12 @@ import org.mockito.InOrder; ...@@ -30,10 +32,12 @@ import org.mockito.InOrder;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException.ServiceUnavailable;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
...@@ -105,6 +109,32 @@ class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFac ...@@ -105,6 +109,32 @@ class UndertowReactiveWebServerFactoryTests extends AbstractReactiveWebServerFac
testAccessLog("my_access.", "logz", "my_access.logz"); testAccessLog("my_access.", "logz", "my_access.logz");
} }
@Test
void whenServerIsShuttingDownGracefullyThenNewConnectionsAreRejectedWithServiceUnavailable() throws Exception {
UndertowReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
WebClient webClient = getWebClient().build();
webClient.get().retrieve().toBodilessEntity().subscribe();
blockingHandler.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
AtomicReference<Throwable> errorReference = new AtomicReference<>();
webClient.get().retrieve().toBodilessEntity().doOnError(errorReference::set).subscribe();
assertThat(shutdownResult.get()).isEqualTo(false);
blockingHandler.completeOne();
this.webServer.stop();
assertThat(errorReference.get()).isInstanceOf(ServiceUnavailable.class);
}
@Override
protected boolean inGracefulShutdown() {
return ((UndertowWebServer) this.webServer).inGracefulShutdown();
}
private void testAccessLog(String prefix, String suffix, String expectedFile) private void testAccessLog(String prefix, String suffix, String expectedFile)
throws IOException, URISyntaxException, InterruptedException { throws IOException, URISyntaxException, InterruptedException {
UndertowReactiveWebServerFactory factory = getFactory(); UndertowReactiveWebServerFactory factory = getFactory();
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -25,15 +25,18 @@ import java.time.Duration; ...@@ -25,15 +25,18 @@ import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import javax.servlet.ServletRegistration.Dynamic;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.Undertow.Builder; import io.undertow.Undertow.Builder;
import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.ServletContainer; import io.undertow.servlet.api.ServletContainer;
import org.apache.http.HttpResponse;
import org.apache.jasper.servlet.JspServlet; import org.apache.jasper.servlet.JspServlet;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -42,6 +45,7 @@ import org.mockito.InOrder; ...@@ -42,6 +45,7 @@ import org.mockito.InOrder;
import org.springframework.boot.testsupport.web.servlet.ExampleServlet; import org.springframework.boot.testsupport.web.servlet.ExampleServlet;
import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.PortInUseException;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests;
...@@ -173,6 +177,33 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto ...@@ -173,6 +177,33 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto
testAccessLog("my_access.", "logz", "my_access.logz"); testAccessLog("my_access.", "logz", "my_access.logz");
} }
@Test
void whenServerIsShuttingDownGracefullyThenRequestsAreRejectedWithServiceUnavailable() throws Exception {
AbstractServletWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingServlet blockingServlet = new BlockingServlet();
this.webServer = factory.getWebServer((context) -> {
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
registration.addMapping("/blocking");
registration.setAsyncSupported(true);
});
this.webServer.start();
Future<Object> request = initiateGetRequest("/blocking");
blockingServlet.awaitQueue();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
Future<Object> rejectedRequest = initiateGetRequest("/");
assertThat(shutdownResult.get()).isEqualTo(false);
blockingServlet.admitOne();
assertThat(request.get()).isInstanceOf(HttpResponse.class);
this.webServer.stop();
Object requestResult = rejectedRequest.get();
assertThat(requestResult).isInstanceOf(HttpResponse.class);
assertThat(((HttpResponse) requestResult).getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value());
}
private void testAccessLog(String prefix, String suffix, String expectedFile) private void testAccessLog(String prefix, String suffix, String expectedFile)
throws IOException, URISyntaxException, InterruptedException { throws IOException, URISyntaxException, InterruptedException {
UndertowServletWebServerFactory factory = getFactory(); UndertowServletWebServerFactory factory = getFactory();
...@@ -278,4 +309,9 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto ...@@ -278,4 +309,9 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto
this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort); this.handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
} }
@Override
protected boolean inGracefulShutdown() {
return ((UndertowServletWebServer) this.webServer).inGracefulShutdown();
}
} }
...@@ -23,7 +23,15 @@ import java.nio.charset.StandardCharsets; ...@@ -23,7 +23,15 @@ import java.nio.charset.StandardCharsets;
import java.security.KeyStore; import java.security.KeyStore;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
...@@ -38,11 +46,14 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; ...@@ -38,11 +46,14 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.netty.NettyPipeline; import reactor.netty.NettyPipeline;
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import reactor.test.StepVerifier; 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.Compression;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServer;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
...@@ -53,6 +64,7 @@ import org.springframework.http.HttpStatus; ...@@ -53,6 +64,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; 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.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
...@@ -333,6 +345,78 @@ public abstract class AbstractReactiveWebServerFactoryTests { ...@@ -333,6 +345,78 @@ public abstract class AbstractReactiveWebServerFactoryTests {
.hasMessageContaining("Could not load key store 'null'"); .hasMessageContaining("Could not load key store 'null'");
} }
@Test
void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(30));
factory.setShutdown(shutdown);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
long start = System.currentTimeMillis();
assertThat(this.webServer.shutDownGracefully()).isTrue();
long end = System.currentTimeMillis();
assertThat(end - start).isLessThanOrEqualTo(30000);
}
@Test
void whenARequestRemainsInFlightThenShutDownGracefullyReturnsFalseAfterPeriodElapses() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(5));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient().build().get().retrieve().toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
long start = System.currentTimeMillis();
assertThat(this.webServer.shutDownGracefully()).isFalse();
long end = System.currentTimeMillis();
assertThat(end - start).isGreaterThanOrEqualTo(5000);
assertThat(responseReference.get()).isNull();
blockingHandler.completeOne();
assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue();
}
@Test
void whenARequestCompletesDuringGracePeriodThenShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception {
AbstractReactiveWebServerFactory factory = getFactory();
if (factory instanceof NettyReactiveWebServerFactory) {
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
resourceFactory.afterPropertiesSet();
((NettyReactiveWebServerFactory) factory).setResourceFactory(resourceFactory);
}
Shutdown shutdown = new Shutdown();
shutdown.setGracePeriod(Duration.ofSeconds(30));
factory.setShutdown(shutdown);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient().build().get().retrieve().toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
long start = System.currentTimeMillis();
Future<Boolean> shutdownResult = initiateGracefulShutdown();
assertThat(responseLatch.getCount()).isEqualTo(1);
blockingHandler.completeOne();
assertThat(shutdownResult.get()).isTrue();
long end = System.currentTimeMillis();
assertThat(end - start).isLessThanOrEqualTo(30000);
assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue();
}
protected WebClient prepareCompressionTest() { protected WebClient prepareCompressionTest() {
Compression compression = new Compression(); Compression compression = new Compression();
compression.setEnabled(true); compression.setEnabled(true);
...@@ -385,6 +469,26 @@ public abstract class AbstractReactiveWebServerFactoryTests { ...@@ -385,6 +469,26 @@ public abstract class AbstractReactiveWebServerFactoryTests {
throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure); throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure);
} }
protected Future<Boolean> initiateGracefulShutdown() {
RunnableFuture<Boolean> future = new FutureTask<Boolean>(() -> this.webServer.shutDownGracefully());
new Thread(future).start();
awaitInGracefulShutdown();
return future;
}
protected void awaitInGracefulShutdown() {
while (!this.inGracefulShutdown()) {
try {
Thread.sleep(100);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
protected abstract boolean inGracefulShutdown();
protected static class EchoHandler implements HttpHandler { protected static class EchoHandler implements HttpHandler {
public EchoHandler() { public EchoHandler() {
...@@ -398,6 +502,40 @@ public abstract class AbstractReactiveWebServerFactoryTests { ...@@ -398,6 +502,40 @@ public abstract class AbstractReactiveWebServerFactoryTests {
} }
protected static class BlockingHandler implements HttpHandler {
private final BlockingQueue<MonoProcessor<Void>> monoProcessors = new ArrayBlockingQueue<>(10);
public BlockingHandler() {
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
MonoProcessor<Void> completion = MonoProcessor.create();
this.monoProcessors.add(completion);
return completion.then(Mono.empty());
}
public void completeOne() {
try {
MonoProcessor<Void> processor = this.monoProcessors.take();
System.out.println("Completing " + processor);
processor.onComplete();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
public void awaitQueue() throws InterruptedException {
while (this.monoProcessors.isEmpty()) {
Thread.sleep(100);
}
}
}
static class CompressionDetectionHandler extends ChannelInboundHandlerAdapter { static class CompressionDetectionHandler extends ChannelInboundHandlerAdapter {
@Override @Override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment