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 e7117df38a..5973cd8383 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 @@ -1031,15 +1031,21 @@ public class ServerProperties { */ private Integer selectors = -1; - /** - * Maximum number of threads. - */ - private Integer maxThreads = 200; - /** * Minimum number of threads. */ - private Integer minThreads = 8; + private int minThreads = 8; + + /** + * Maximum number of threads. + */ + private int maxThreads = 200; + + /** + * Maximum capacity of the thread pool's backing queue. A default is computed + * based on the threading configuration. + */ + private Integer maxQueueCapacity; /** * Maximum thread idle time. @@ -1090,22 +1096,30 @@ public class ServerProperties { this.selectors = selectors; } - public void setMinThreads(Integer minThreads) { + public void setMinThreads(int minThreads) { this.minThreads = minThreads; } - public Integer getMinThreads() { + public int getMinThreads() { return this.minThreads; } - public void setMaxThreads(Integer maxThreads) { + public void setMaxThreads(int maxThreads) { this.maxThreads = maxThreads; } - public Integer getMaxThreads() { + public int getMaxThreads() { return this.maxThreads; } + public void setMaxQueueCapacity(Integer maxQueueCapacity) { + this.maxQueueCapacity = maxQueueCapacity; + } + + public Integer getMaxQueueCapacity() { + return this.maxQueueCapacity; + } + public void setThreadIdleTimeout(Duration threadIdleTimeout) { this.threadIdleTimeout = threadIdleTimeout; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index c8d4ae81ec..54f3b42e08 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -18,7 +18,8 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.time.Duration; import java.util.Arrays; -import java.util.function.Consumer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -30,6 +31,7 @@ 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.util.BlockingArrayQueue; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; @@ -75,6 +77,7 @@ public class JettyWebServerFactoryCustomizer ServerProperties properties = this.serverProperties; ServerProperties.Jetty jettyProperties = properties.getJetty(); factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); + factory.setThreadPool(determineThreadPool(jettyProperties)); PropertyMapper propertyMapper = PropertyMapper.get(); propertyMapper.from(jettyProperties::getAcceptors).whenNonNull().to(factory::setAcceptors); propertyMapper.from(jettyProperties::getSelectors).whenNonNull().to(factory::setSelectors); @@ -83,12 +86,6 @@ public class JettyWebServerFactoryCustomizer .addServerCustomizers(new MaxHttpHeaderSizeCustomizer(maxHttpHeaderSize))); propertyMapper.from(jettyProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes).when(this::isPositive) .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); - propertyMapper.from(jettyProperties::getMaxThreads).when(this::isPositive) - .to((maxThreads) -> customizeThreadPool(factory, (threadPool) -> threadPool.setMaxThreads(maxThreads))); - propertyMapper.from(jettyProperties::getMinThreads).when(this::isPositive) - .to((minThreads) -> customizeThreadPool(factory, (threadPool) -> threadPool.setMinThreads(minThreads))); - propertyMapper.from(jettyProperties::getThreadIdleTimeout).whenNonNull().asInt(Duration::toMillis).to( - (idleTimeout) -> customizeThreadPool(factory, (threadPool) -> threadPool.setIdleTimeout(idleTimeout))); propertyMapper.from(properties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeIdleTimeout(factory, connectionTimeout)); propertyMapper.from(jettyProperties::getConnectionIdleTimeout).whenNonNull() @@ -144,13 +141,25 @@ public class JettyWebServerFactoryCustomizer }); } - private void customizeThreadPool(ConfigurableJettyWebServerFactory factory, Consumer customizer) { - factory.addServerCustomizers((connector) -> { - ThreadPool threadPool = connector.getThreadPool(); - if (threadPool instanceof QueuedThreadPool) { - customizer.accept((QueuedThreadPool) threadPool); - } - }); + private ThreadPool determineThreadPool(ServerProperties.Jetty properties) { + BlockingQueue queue = determineBlockingQueue(properties.getMaxQueueCapacity()); + int maxThreadCount = (properties.getMaxThreads() > 0) ? properties.getMaxThreads() : 200; + int minThreadCount = (properties.getMinThreads() > 0) ? properties.getMinThreads() : 8; + int threadIdleTimeout = (properties.getThreadIdleTimeout() != null) + ? (int) properties.getThreadIdleTimeout().toMillis() : 60000; + return new QueuedThreadPool(maxThreadCount, minThreadCount, threadIdleTimeout, queue); + } + + private BlockingQueue determineBlockingQueue(Integer maxQueueCapacity) { + if (maxQueueCapacity == null) { + return null; + } + if (maxQueueCapacity == 0) { + return new SynchronousQueue<>(); + } + else { + return new BlockingArrayQueue<>(maxQueueCapacity); + } } private void customizeAccessLog(ConfigurableJettyWebServerFactory factory, 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 abc75aa212..8caebfdbf2 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 @@ -237,6 +237,12 @@ class ServerPropertiesTests { assertThat(this.properties.getJetty().getThreadIdleTimeout()).hasSeconds(10); } + @Test + void testCustomizeJettyMaxQueueCapacity() { + bind("server.jetty.max-queue-capacity", "5150"); + assertThat(this.properties.getJetty().getMaxQueueCapacity()).isEqualTo(5150); + } + @Test void testCustomizeUndertowServerOption() { bind("server.undertow.options.server.ALWAYS_SET_KEEP_ALIVE", "true"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java index 976928d0b7..4a1c989d0e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java @@ -18,9 +18,12 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; import java.util.stream.Collectors; import org.eclipse.jetty.server.AbstractConnector; @@ -30,11 +33,15 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration.ConnectionFactory; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.RequestLogWriter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.Jetty; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; @@ -43,6 +50,7 @@ import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -135,7 +143,26 @@ class JettyWebServerFactoryCustomizerTests { } @Test - void maxThreadsCanBeCustomized() { + void threadPoolMatchesJettyDefaults() { + ThreadPool defaultThreadPool = new Server(0).getThreadPool(); + ThreadPool configuredThreadPool = customizeAndGetServer().getServer().getThreadPool(); + assertThat(defaultThreadPool).isInstanceOf(QueuedThreadPool.class); + assertThat(configuredThreadPool).isInstanceOf(QueuedThreadPool.class); + QueuedThreadPool defaultQueuedThreadPool = (QueuedThreadPool) defaultThreadPool; + QueuedThreadPool configuredQueuedThreadPool = (QueuedThreadPool) configuredThreadPool; + assertThat(configuredQueuedThreadPool.getMinThreads()).isEqualTo(defaultQueuedThreadPool.getMinThreads()); + assertThat(configuredQueuedThreadPool.getMaxThreads()).isEqualTo(defaultQueuedThreadPool.getMaxThreads()); + assertThat(configuredQueuedThreadPool.getIdleTimeout()).isEqualTo(defaultQueuedThreadPool.getIdleTimeout()); + BlockingQueue defaultQueue = getQueue(defaultThreadPool); + BlockingQueue configuredQueue = getQueue(configuredThreadPool); + assertThat(defaultQueue).isInstanceOf(BlockingArrayQueue.class); + assertThat(configuredQueue).isInstanceOf(BlockingArrayQueue.class); + assertThat(((BlockingArrayQueue) defaultQueue).getMaxCapacity()) + .isEqualTo(((BlockingArrayQueue) configuredQueue).getMaxCapacity()); + } + + @Test + void threadPoolMaxThreadsCanBeCustomized() { bind("server.jetty.max-threads=100"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); @@ -143,7 +170,7 @@ class JettyWebServerFactoryCustomizerTests { } @Test - void minThreadsCanBeCustomized() { + void threadPoolMinThreadsCanBeCustomized() { bind("server.jetty.min-threads=100"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); @@ -151,13 +178,65 @@ class JettyWebServerFactoryCustomizerTests { } @Test - void threadIdleTimeoutCanBeCustomized() { + void threadPoolIdleTimeoutCanBeCustomized() { bind("server.jetty.thread-idle-timeout=100s"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); assertThat(threadPool.getIdleTimeout()).isEqualTo(100000); } + @Test + void threadPoolWithMaxQueueCapacityEqualToZeroCreateSynchronousQueue() { + bind("server.jetty.max-queue-capacity=0"); + JettyWebServer server = customizeAndGetServer(); + ThreadPool threadPool = server.getServer().getThreadPool(); + BlockingQueue queue = getQueue(threadPool); + assertThat(queue).isInstanceOf(SynchronousQueue.class); + assertDefaultThreadPoolSettings(threadPool); + } + + @Test + void threadPoolWithMaxQueueCapacityEqualToZeroCustomizesThreadPool() { + bind("server.jetty.max-queue-capacity=0", "server.jetty.min-threads=100", "server.jetty.max-threads=100", + "server.jetty.thread-idle-timeout=6s"); + JettyWebServer server = customizeAndGetServer(); + QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); + assertThat(threadPool.getMinThreads()).isEqualTo(100); + assertThat(threadPool.getMaxThreads()).isEqualTo(100); + assertThat(threadPool.getIdleTimeout()).isEqualTo(Duration.ofSeconds(6).toMillis()); + } + + @Test + void threadPoolWithMaxQueueCapacityPositiveCreateBlockingArrayQueue() { + bind("server.jetty.max-queue-capacity=1234"); + JettyWebServer server = customizeAndGetServer(); + ThreadPool threadPool = server.getServer().getThreadPool(); + BlockingQueue queue = getQueue(threadPool); + assertThat(queue).isInstanceOf(BlockingArrayQueue.class); + assertThat(((BlockingArrayQueue) queue).getMaxCapacity()).isEqualTo(1234); + assertDefaultThreadPoolSettings(threadPool); + } + + @Test + void threadPoolWithMaxQueueCapacityPositiveCustomizesThreadPool() { + bind("server.jetty.max-queue-capacity=1234", "server.jetty.min-threads=10", "server.jetty.max-threads=150", + "server.jetty.thread-idle-timeout=3s"); + JettyWebServer server = customizeAndGetServer(); + QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); + assertThat(threadPool.getMinThreads()).isEqualTo(10); + assertThat(threadPool.getMaxThreads()).isEqualTo(150); + assertThat(threadPool.getIdleTimeout()).isEqualTo(Duration.ofSeconds(3).toMillis()); + } + + private void assertDefaultThreadPoolSettings(ThreadPool threadPool) { + assertThat(threadPool).isInstanceOf(QueuedThreadPool.class); + QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool; + Jetty defaultProperties = new Jetty(); + assertThat(queuedThreadPool.getMinThreads()).isEqualTo(defaultProperties.getMinThreads()); + assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(defaultProperties.getMaxThreads()); + assertThat(queuedThreadPool.getIdleTimeout()).isEqualTo(defaultProperties.getThreadIdleTimeout().toMillis()); + } + private CustomRequestLog getRequestLog(JettyWebServer server) { RequestLog requestLog = server.getServer().getRequestLog(); assertThat(requestLog).isInstanceOf(CustomRequestLog.class); @@ -236,6 +315,10 @@ class JettyWebServerFactoryCustomizerTests { return requestHeaderSizes; } + private BlockingQueue getQueue(ThreadPool threadPool) { + return ReflectionTestUtils.invokeMethod(threadPool, "getQueue"); + } + 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/jetty/ConfigurableJettyWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java index b80e4209bd..e6afc8319d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java @@ -17,6 +17,7 @@ package org.springframework.boot.web.embedded.jetty; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.ThreadPool; import org.springframework.boot.web.server.ConfigurableWebServerFactory; @@ -36,6 +37,13 @@ public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServer */ void setAcceptors(int acceptors); + /** + * Set the {@link ThreadPool} that should be used by the {@link Server}. If set to + * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. + * @param threadPool the ThreadPool to be used + */ + void setThreadPool(ThreadPool threadPool); + /** * Set the number of selector threads to use. * @param selectors the number of selector threads to use diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 950463a093..df883ab348 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -139,11 +139,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact return this.threadPool; } - /** - * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to - * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. - * @param threadPool a Jetty ThreadPool to be used - */ + @Override public void setThreadPool(ThreadPool threadPool) { this.threadPool = threadPool; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 85f57958a3..214370a673 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -484,11 +484,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor return this.threadPool; } - /** - * Set a Jetty {@link ThreadPool} that should be used by the {@link Server}. If set to - * {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly. - * @param threadPool a Jetty ThreadPool to be used - */ + @Override public void setThreadPool(ThreadPool threadPool) { this.threadPool = threadPool; }