From 5c2ae8a73ff289a12cd825ecd3272b3e23156c5e Mon Sep 17 00:00:00 2001 From: Michael Lubavin Date: Fri, 26 Mar 2021 13:24:16 +0200 Subject: [PATCH] Tomcat keepalive customizations new properties: server.tomcat.keep-alive-timeout server.tomcat.max-keep-alive-requests --- .../autoconfigure/web/ServerProperties.java | 30 +++++++++++++++++++ .../TomcatWebServerFactoryCustomizer.java | 24 +++++++++++++++ .../web/ServerPropertiesTests.java | 17 +++++++++++ ...TomcatWebServerFactoryCustomizerTests.java | 23 ++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index eb6f29d82865..11191f2220f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -66,6 +66,7 @@ * @author HaiTao Zhang * @author Victor Mandujano * @author Chris Bono + * @author Michael Lubavin * @since 1.0.0 */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @@ -395,6 +396,19 @@ public static class Tomcat { */ private Duration connectionTimeout; + /** + * Amount of time a connection is allowed to remain idle until it is closed. Set + * to -1 to indicate idle connections should not be closed. + */ + private Duration keepAliveTimeout; + + /** + * The maximum number of requests which can be processed on one connection before + * it is closed by the server. Set to -1 for an unlimited number of requests per + * connection. The default is 100, same as Tomcat default. + */ + private int maxKeepAliveRequests = 100; + /** * Static resource configuration. */ @@ -530,6 +544,22 @@ public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; } + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + public int getMaxKeepAliveRequests() { + return this.maxKeepAliveRequests; + } + + public void setMaxKeepAliveRequests(int maxKeepAliveRequests) { + this.maxKeepAliveRequests = maxKeepAliveRequests; + } + public Resource getResource() { return this.resource; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 0aa4510f3bf8..184382b4c217 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -102,6 +102,10 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { propertyMapper.from(tomcatProperties::getUriEncoding).whenNonNull().to(factory::setUriEncoding); propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); + propertyMapper.from(tomcatProperties::getKeepAliveTimeout).whenNonNull() + .to((keepAliveTimeout) -> customizeKeepAliveTimeout(factory, keepAliveTimeout)); + propertyMapper.from(tomcatProperties::getMaxKeepAliveRequests) + .to((maxKeepAliveRequests) -> customizeMaxKeepAliveRequests(factory, maxKeepAliveRequests)); propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive) .to((maxConnections) -> customizeMaxConnections(factory, maxConnections)); propertyMapper.from(tomcatProperties::getAcceptCount).when(this::isPositive) @@ -159,6 +163,26 @@ private void customizeConnectionTimeout(ConfigurableTomcatWebServerFactory facto }); } + private void customizeKeepAliveTimeout(ConfigurableTomcatWebServerFactory factory, Duration keepAliveTimeout) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractProtocol) { + AbstractProtocol protocol = (AbstractProtocol) handler; + protocol.setKeepAliveTimeout((int) keepAliveTimeout.toMillis()); + } + }); + } + + private void customizeMaxKeepAliveRequests(ConfigurableTomcatWebServerFactory factory, int maxKeepAliveRequests) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractHttp11Protocol) { + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) handler; + protocol.setMaxKeepAliveRequests(maxKeepAliveRequests); + } + }); + } + private void customizeRelaxedPathChars(ConfigurableTomcatWebServerFactory factory, String relaxedChars) { factory.addConnectorCustomizers((connector) -> connector.setProperty("relaxedPathChars", relaxedChars)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 64477313bf75..a56ed6cb6adc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -38,6 +38,8 @@ import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.http11.Http11NioProtocol; +import org.apache.tomcat.util.net.NioEndpoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; @@ -82,6 +84,7 @@ * @author HaiTao Zhang * @author Rafiullah Hamedy * @author Chris Bono + * @author Michael Lubavin */ class ServerPropertiesTests { @@ -132,6 +135,8 @@ void testTomcatBinding() { map.put("server.tomcat.relaxed-path-chars", "|,<"); map.put("server.tomcat.relaxed-query-chars", "^ , | "); map.put("server.tomcat.use-relative-redirects", "true"); + map.put("server.tomcat.keep-alive-timeout", "100s"); + map.put("server.tomcat.max-keep-alive-requests", "-1"); bind(map); ServerProperties.Tomcat tomcat = this.properties.getTomcat(); Accesslog accesslog = tomcat.getAccesslog(); @@ -154,6 +159,8 @@ void testTomcatBinding() { assertThat(tomcat.getRelaxedPathChars()).containsExactly('|', '<'); assertThat(tomcat.getRelaxedQueryChars()).containsExactly('^', '|'); assertThat(tomcat.isUseRelativeRedirects()).isTrue(); + assertThat(tomcat.getKeepAliveTimeout()).hasSeconds(100); + assertThat(tomcat.getMaxKeepAliveRequests()).isEqualTo(-1); } @Test @@ -325,6 +332,16 @@ void tomcatMaxThreadsMatchesProtocolDefault() throws Exception { assertThat(this.properties.getTomcat().getThreads().getMax()).isEqualTo(getDefaultProtocol().getMaxThreads()); } + @Test + void tomcatMaxKeepAliveRequestsMatchesProtocolDefault() throws Exception { + Http11NioProtocol http11NioProtocol = (Http11NioProtocol) getDefaultProtocol(); + NioEndpoint endpoint = (NioEndpoint) ReflectionTestUtils.getField(http11NioProtocol, "endpoint"); + // cannot use getter since it returns 1 when endpoint is not bound, + // so using reflection to fetch the default maxKeepAliveRequests instead + int defaultMaxKeepAliveRequests = (int) ReflectionTestUtils.getField(endpoint, "maxKeepAliveRequests"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(defaultMaxKeepAliveRequests); + } + @Test void tomcatMinSpareThreadsMatchesProtocolDefault() throws Exception { assertThat(this.properties.getTomcat().getThreads().getMinSpare()) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index 4b6416b0a639..cc7a0d1abcbf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -324,6 +324,29 @@ void customConnectionTimeout() { .isEqualTo(30000)); } + @Test + void customKeepAliveTimeout() { + bind("server.tomcat.keep-alive-timeout=50s"); + customizeAndRunServer((server) -> assertThat( + ((AbstractProtocol) server.getTomcat().getConnector().getProtocolHandler()).getKeepAliveTimeout()) + .isEqualTo(50000)); + } + + @Test + void defaultMaxKeepAliveRequests() { + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(100)); + } + + @Test + void customMaxKeepAliveRequests() { + bind("server.tomcat.max-keep-alive-requests=-1"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(-1)); + } + @Test void accessLogBufferingCanBeDisabled() { bind("server.tomcat.accesslog.enabled=true", "server.tomcat.accesslog.buffered=false");