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