From f743dc82fc17a69fcd4bd247a2ea2cdb8069ea50 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 May 2024 20:31:40 -0700 Subject: [PATCH] Improve graceful shutdown documentation to remove ambiguity Closes gh-40108 --- .../docs/asciidoc/web/graceful-shutdown.adoc | 10 ++++-- .../web/embedded/jetty/JettyWebServer.java | 11 ++++++- .../web/embedded/netty/NettyWebServer.java | 8 +++++ .../web/embedded/tomcat/TomcatWebServer.java | 10 +++++- .../embedded/undertow/UndertowWebServer.java | 8 +++++ .../UndertowServletWebServerFactoryTests.java | 33 ++++++++++++++++++- 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/graceful-shutdown.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/graceful-shutdown.adoc index a7a544d5265..fce4f166aa4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/graceful-shutdown.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/graceful-shutdown.adoc @@ -3,9 +3,15 @@ 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. It occurs as part of closing the application context and is performed in the earliest phase of stopping `SmartLifecycle` beans. This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted. + The exact way in which new requests are not permitted varies depending on the web server that is being used. -Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer. -Undertow will accept requests but respond immediately with a service unavailable (503) response. +Implementations may stop accepting requests at the network layer, or they may return a response with a specific HTTP status code or HTTP header. +The use of persistent connections can also change the way that requests stop being accepted. + +TIP: To learn about more the specific method used with your web server, see the `shutDownGracefully` javadoc for {spring-boot-module-api}/web/embedded/tomcat/TomcatWebServer.html#shutDownGracefully(org.springframework.boot.web.server.GracefulShutdownCallback)[TomcatWebServer], {spring-boot-module-api}/web/embedded/netty/NettyWebServer.html#shutDownGracefully(org.springframework.boot.web.server.GracefulShutdownCallback)[NettyWebServer], {spring-boot-module-api}/web/embedded/jetty/JettyWebServer.html#shutDownGracefully(org.springframework.boot.web.server.GracefulShutdownCallback)[JettyWebServer] or {spring-boot-module-api}/web/embedded/undertow/UndertowWebServer.html#shutDownGracefully(org.springframework.boot.web.server.GracefulShutdownCallback)[UndertowWebServer]. + +Jetty, Reactor Netty, and Tomcat will stop accepting new requests at the network layer. +Undertow will accept new connections but respond immediately with a service unavailable (503) response. NOTE: Graceful shutdown with Tomcat requires Tomcat 9.0.33 or later. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index 95ae2e3c2b6..6472c882c53 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -268,6 +268,15 @@ public class JettyWebServer implements WebServer { return 0; } + /** + * Initiates a graceful shutdown of the Jetty web server. Handling of new requests is + * prevented and the given {@code callback} is invoked at the end of the attempt. The + * attempt can be explicitly ended by invoking {@link #stop}. + *

+ * Once shutdown has been initiated Jetty will reject any new connections. Requests on + * existing connections will be accepted, however, a {@code Connection: close} header + * will be returned in the response. + */ @Override public void shutDownGracefully(GracefulShutdownCallback callback) { if (this.gracefulShutdown == null) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index c441b8837d0..d9bd1245d1e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -157,6 +157,14 @@ public class NettyWebServer implements WebServer { return false; } + /** + * Initiates a graceful shutdown of the Netty web server. Handling of new requests is + * prevented and the given {@code callback} is invoked at the end of the attempt. The + * attempt can be explicitly ended by invoking {@link #stop}. + *

+ * Once shutdown has been initiated Netty will reject any new connections. Requests + + * on existing idle connections will also be rejected. + */ @Override public void shutDownGracefully(GracefulShutdownCallback callback) { if (this.gracefulShutdown == null) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 4c43a38d452..a8db1badb35 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -383,6 +383,14 @@ public class TomcatWebServer implements WebServer { return this.tomcat; } + /** + * Initiates a graceful shutdown of the Tomcat web server. Handling of new requests is + * prevented and the given {@code callback} is invoked at the end of the attempt. The + * attempt can be explicitly ended by invoking {@link #stop}. + *

+ * Once shutdown has been initiated Tomcat will reject any new connections. Requests + * on existing idle connections will also be rejected. + */ @Override public void shutDownGracefully(GracefulShutdownCallback callback) { if (this.gracefulShutdown == null) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index c4045af0cad..8bb2ad96253 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -289,6 +289,14 @@ public class UndertowWebServer implements WebServer { return ports.get(0).getNumber(); } + /** + * Initiates a graceful shutdown of the Undertow web server. Handling of new requests + * is prevented and the given {@code callback} is invoked at the end of the attempt. + * The attempt can be explicitly ended by invoking {@link #stop}. + *

+ * Once shutdown has been initiated Undertow will return an {@code HTTP 503} response + * for any new or existing connections. + */ @Override public void shutDownGracefully(GracefulShutdownCallback callback) { if (this.gracefulShutdown == null) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java index dd42e2bf996..4c26176d83e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -36,6 +36,8 @@ import io.undertow.Undertow.Builder; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.ServletContainer; import jakarta.servlet.ServletRegistration.Dynamic; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.HttpResponse; import org.apache.jasper.servlet.JspServlet; import org.awaitility.Awaitility; @@ -211,6 +213,35 @@ class UndertowServletWebServerFactoryTests extends AbstractServletWebServerFacto this.webServer.stop(); } + @Test + void whenServerIsShuttingDownARequestOnAnIdleConnectionAreRejectedWithServiceUnavailable() throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + factory.setShutdown(Shutdown.GRACEFUL); + BlockingServlet blockingServlet = new BlockingServlet(); + this.webServer = factory.getWebServer((context) -> { + Dynamic registration = context.addServlet("blockingServlet", blockingServlet); + registration.addMapping("/blocking"); + registration.setAsyncSupported(true); + }); + HttpClient httpClient = HttpClients.createMinimal(); + this.webServer.start(); + int port = this.webServer.getPort(); + Future keepAliveRequest = initiateGetRequest(httpClient, port, "/blocking"); + blockingServlet.awaitQueue(); + blockingServlet.admitOne(); + assertThat(keepAliveRequest.get()).isInstanceOf(HttpResponse.class); + Future request = initiateGetRequest(port, "/blocking"); + blockingServlet.awaitQueue(); + this.webServer.shutDownGracefully((result) -> { + }); + HttpResponse idleConnectionResponse = (HttpResponse) initiateGetRequest(httpClient, port, "/").get(); + assertThat(idleConnectionResponse.getCode()).isEqualTo(503); + blockingServlet.admitOne(); + Object response = request.get(); + assertThat(response).isInstanceOf(HttpResponse.class); + this.webServer.stop(); + } + private void testAccessLog(String prefix, String suffix, String expectedFile) throws IOException, URISyntaxException { UndertowServletWebServerFactory factory = getFactory();