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 e8742b414f..ca10a73353 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 @@ -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. @@ -905,6 +905,11 @@ public class ServerProperties { */ private int minSpare = 10; + /** + * Maximum capacity of the thread pool's backing queue. + */ + private int maxQueueCapacity = 2147483647; + public int getMax() { return this.max; } @@ -921,6 +926,14 @@ public class ServerProperties { this.minSpare = minSpare; } + public int getMaxQueueCapacity() { + return this.maxQueueCapacity; + } + + public void setMaxQueueCapacity(int maxQueueCapacity) { + this.maxQueueCapacity = maxQueueCapacity; + } + } /** 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 1c357d2d0a..4c5340ca51 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 @@ -21,7 +21,10 @@ import java.util.List; import java.util.function.ObjIntConsumer; import java.util.stream.Collectors; +import javax.management.ObjectName; + import org.apache.catalina.Lifecycle; +import org.apache.catalina.core.StandardThreadExecutor; import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.ErrorReportValve; import org.apache.catalina.valves.RemoteIpValve; @@ -36,6 +39,7 @@ import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeAttribu import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Remoteip; +import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Threads; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; @@ -94,13 +98,7 @@ public class TomcatWebServerFactoryCustomizer .as(Long::intValue) .to(factory::setBackgroundProcessorDelay); customizeRemoteIpValve(factory); - ServerProperties.Tomcat.Threads threadProperties = properties.getThreads(); - map.from(threadProperties::getMax) - .when(this::isPositive) - .to((maxThreads) -> customizeMaxThreads(factory, threadProperties.getMax())); - map.from(threadProperties::getMinSpare) - .when(this::isPositive) - .to((minSpareThreads) -> customizeMinThreads(factory, minSpareThreads)); + configureExecutor(factory, properties.getThreads()); map.from(this.serverProperties.getMaxHttpRequestHeaderSize()) .asInt(DataSize::toBytes) .when(this::isPositive) @@ -148,6 +146,19 @@ public class TomcatWebServerFactoryCustomizer customizeErrorReportValve(this.serverProperties.getError(), factory); } + private void configureExecutor(ConfigurableTomcatWebServerFactory factory, Threads threadProperties) { + factory.addProtocolHandlerCustomizers((handler) -> { + StandardThreadExecutor executor = new StandardThreadExecutor(); + executor.setMinSpareThreads(threadProperties.getMinSpare()); + executor.setMaxThreads(threadProperties.getMax()); + executor.setMaxQueueSize(threadProperties.getMaxQueueCapacity()); + if (handler instanceof AbstractProtocol protocol) { + executor.setNamePrefix(ObjectName.unquote(protocol.getName()) + "-exec-"); + } + handler.setExecutor(executor); + }); + } + private boolean isPositive(int value) { return value > 0; } @@ -252,16 +263,6 @@ public class TomcatWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy() == ServerProperties.ForwardHeadersStrategy.NATIVE; } - @SuppressWarnings("rawtypes") - private void customizeMaxThreads(ConfigurableTomcatWebServerFactory factory, int maxThreads) { - customizeHandler(factory, maxThreads, AbstractProtocol.class, AbstractProtocol::setMaxThreads); - } - - @SuppressWarnings("rawtypes") - private void customizeMinThreads(ConfigurableTomcatWebServerFactory factory, int minSpareThreads) { - customizeHandler(factory, minSpareThreads, AbstractProtocol.class, AbstractProtocol::setMinSpareThreads); - } - @SuppressWarnings("rawtypes") private void customizeMaxHttpRequestHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxHttpRequestHeaderSize) { 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 f885699393..205d98db53 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 @@ -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. @@ -17,10 +17,12 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.util.Locale; +import java.util.concurrent.Executor; import java.util.function.Consumer; import org.apache.catalina.Context; import org.apache.catalina.Valve; +import org.apache.catalina.core.StandardThreadExecutor; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.ErrorReportValve; @@ -58,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Rafiullah Hamedy * @author Victor Mandujano * @author Parviz Rozikov + * @author Moritz Halbritter */ class TomcatWebServerFactoryCustomizerTests { @@ -564,6 +567,20 @@ class TomcatWebServerFactoryCustomizerTests { server.stop(); } + @Test + void configureExecutor() { + bind("server.tomcat.threads.max=10", "server.tomcat.threads.min-spare=2", + "server.tomcat.threads.max-queue-capacity=20"); + customizeAndRunServer((server) -> { + Executor executor = server.getTomcat().getConnector().getProtocolHandler().getExecutor(); + assertThat(executor).isInstanceOf(StandardThreadExecutor.class); + StandardThreadExecutor standardThreadExecutor = (StandardThreadExecutor) executor; + assertThat(standardThreadExecutor.getMaxThreads()).isEqualTo(10); + assertThat(standardThreadExecutor.getMinSpareThreads()).isEqualTo(2); + assertThat(standardThreadExecutor.getMaxQueueSize()).isEqualTo(20); + }); + } + private void bind(String... inlinedProperties) { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties); new Binder(ConfigurationPropertySources.get(this.environment)).bind("server", diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 4981bb81e9..df94ecf9e4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -28,6 +28,7 @@ import java.util.Set; import org.apache.catalina.Context; import org.apache.catalina.Engine; +import org.apache.catalina.Executor; import org.apache.catalina.Host; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Valve; @@ -135,16 +136,24 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); + registerConnectorExecutor(tomcat, connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); + registerConnectorExecutor(tomcat, additionalConnector); } TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); prepareContext(tomcat.getHost(), servlet); return getTomcatWebServer(tomcat); } + private void registerConnectorExecutor(Tomcat tomcat, Connector connector) { + if (connector.getProtocolHandler().getExecutor() instanceof Executor executor) { + tomcat.getService().addExecutor(executor); + } + } + private void configureEngine(Engine engine) { engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); for (Valve valve : this.engineValves) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 5faed28060..fff2cd0100 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -39,6 +39,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import org.apache.catalina.Context; import org.apache.catalina.Engine; +import org.apache.catalina.Executor; import org.apache.catalina.Host; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; @@ -209,15 +210,23 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); + registerConnectorExecutor(tomcat, connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); + registerConnectorExecutor(tomcat, additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } + private void registerConnectorExecutor(Tomcat tomcat, Connector connector) { + if (connector.getProtocolHandler().getExecutor() instanceof Executor executor) { + tomcat.getService().addExecutor(executor); + } + } + private void configureEngine(Engine engine) { engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); for (Valve valve : this.engineValves) {