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