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..ff00c56518 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 @@ -1041,6 +1041,11 @@ public class ServerProperties { */ private Integer minThreads = 8; + /** + * Maximum capacity of the thread pools backing queue. + */ + private Integer maxQueueCapacity; + /** * Maximum thread idle time. */ @@ -1106,6 +1111,14 @@ public class ServerProperties { 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/JettyConstrainedQueuedThreadPoolFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyConstrainedQueuedThreadPoolFactory.java new file mode 100644 index 0000000000..4ad0d10da6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyConstrainedQueuedThreadPoolFactory.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2019 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.autoconfigure.web.embedded; + +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.Jetty; +import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory; + +/** + * A {@link JettyThreadPoolFactory} that creates a thread pool that uses a backing queue + * with a max capacity if the {@link Jetty#maxQueueCapacity} is specified. If the max + * capacity is not specified then the factory will return null thus allowing the standard + * Jetty server thread pool to be used. + * + * @author Chris Bono + * @since 2.3.0 + */ +public class JettyConstrainedQueuedThreadPoolFactory implements JettyThreadPoolFactory { + + private ServerProperties serverProperties; + + public JettyConstrainedQueuedThreadPoolFactory(ServerProperties serverProperties) { + this.serverProperties = serverProperties; + } + + /** + *

+ * Creates a {@link QueuedThreadPool} with the following settings (if + * {@link Jetty#maxQueueCapacity} is specified): + *

+ * @return thread pool as described above or {@code null} if + * {@link Jetty#maxQueueCapacity} is not specified. + */ + @Override + public QueuedThreadPool create() { + + Integer maxQueueCapacity = this.serverProperties.getJetty().getMaxQueueCapacity(); + + // Max depth is not specified - let Jetty server use its defaults in this case + if (maxQueueCapacity == null) { + return null; + } + + BlockingQueue queue; + if (maxQueueCapacity == 0) { + /** + * This queue will cause jetty to reject requests whenever there is no idle + * thread available to handle them. If this queue is used, it is strongly + * recommended to set _minThreads equal to _maxThreads. Jetty's + * QueuedThreadPool class may not behave like a regular java thread pool and + * may not add threads properly when a SynchronousQueue is used. + */ + queue = new SynchronousQueue<>(); + } + else { + /** + * Create a queue of fixed size. This queue will not grow. If a request + * arrives and the queue is empty, the client will see an immediate + * "connection reset" error. + */ + queue = new BlockingArrayQueue<>(maxQueueCapacity); + } + + Integer maxThreadCount = this.serverProperties.getJetty().getMaxThreads(); + Integer minThreadCount = this.serverProperties.getJetty().getMinThreads(); + Duration threadIdleTimeout = this.serverProperties.getJetty().getThreadIdleTimeout(); + + return new QueuedThreadPool((maxThreadCount != null) ? maxThreadCount : 200, + (minThreadCount != null) ? minThreadCount : 8, + (threadIdleTimeout != null) ? (int) threadIdleTimeout.toMillis() : 60000, queue); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java index e03963cd3d..0e2371212c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java @@ -24,8 +24,12 @@ import reactor.netty.http.server.HttpServer; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; +import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.netty.NettyRouteProvider; import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; @@ -101,6 +105,7 @@ abstract class ReactiveWebServerFactoryConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveWebServerFactory.class) @ConditionalOnClass({ org.eclipse.jetty.server.Server.class }) + @EnableConfigurationProperties(ServerProperties.class) static class EmbeddedJetty { @Bean @@ -109,10 +114,17 @@ abstract class ReactiveWebServerFactoryConfiguration { return new JettyResourceFactory(); } + @Bean + @ConditionalOnMissingBean + JettyThreadPoolFactory jettyThreadPoolFactory(ServerProperties serverProperties) { + return new JettyConstrainedQueuedThreadPoolFactory(serverProperties); + } + @Bean JettyReactiveWebServerFactory jettyReactiveWebServerFactory(JettyResourceFactory resourceFactory, - ObjectProvider serverCustomizers) { + JettyThreadPoolFactory threadPoolFactory, ObjectProvider serverCustomizers) { JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory(); + serverFactory.setThreadPool(threadPoolFactory.create()); serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList())); serverFactory.setResourceFactory(resourceFactory); return serverFactory; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java index 03dbe4e92b..fcf5fe44a4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java @@ -32,8 +32,12 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; @@ -90,12 +94,20 @@ class ServletWebServerFactoryConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) + @EnableConfigurationProperties(ServerProperties.class) static class EmbeddedJetty { @Bean - JettyServletWebServerFactory JettyServletWebServerFactory( + @ConditionalOnMissingBean + JettyThreadPoolFactory jettyThreadPoolFactory(ServerProperties serverProperties) { + return new JettyConstrainedQueuedThreadPoolFactory(serverProperties); + } + + @Bean + JettyServletWebServerFactory JettyServletWebServerFactory(JettyThreadPoolFactory threadPoolFactory, ObjectProvider serverCustomizers) { JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); + factory.setThreadPool(threadPoolFactory.create()); factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList())); return 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..cf3560b066 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 @@ -75,6 +75,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Andrew McGhie * @author HaiTao Zhang * @author Rafiullah Hamedy + * @author Chris Bono */ class ServerPropertiesTests { @@ -237,6 +238,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/JettyConstrainedQueuedThreadPoolFactoryTest.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyConstrainedQueuedThreadPoolFactoryTest.java new file mode 100644 index 0000000000..81b45c0147 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyConstrainedQueuedThreadPoolFactoryTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2019 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.autoconfigure.web.embedded; + +import java.lang.reflect.Method; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JettyConstrainedQueuedThreadPoolFactory}. + * + * @author Chris Bono + */ +class JettyConstrainedQueuedThreadPoolFactoryTest { + + private MockEnvironment environment; + + private ServerProperties serverProperties; + + private JettyConstrainedQueuedThreadPoolFactory factory; + + @BeforeEach + void setup() { + this.environment = new MockEnvironment(); + this.serverProperties = new ServerProperties(); + ConfigurationPropertySources.attach(this.environment); + this.factory = new JettyConstrainedQueuedThreadPoolFactory(this.serverProperties); + } + + @Test + void factoryReturnsNullWhenMaxCapacityNotSpecified() { + bind("server.jetty.max-queue-capacity="); + assertThat(this.factory.create()).isNull(); + } + + @Test + void factoryReturnsSynchronousQueueWhenMaxCapacityIsZero() { + bind("server.jetty.max-queue-capacity=0"); + QueuedThreadPool queuedThreadPool = this.factory.create(); + assertThat(getQueue(queuedThreadPool, SynchronousQueue.class)).isNotNull(); + } + + @Test + void factoryReturnsBlockingArrayQueueWithDefaultsWhenOnlyMaxCapacityIsSet() { + bind("server.jetty.max-queue-capacity=5150"); + QueuedThreadPool queuedThreadPool = this.factory.create(); + assertThat(queuedThreadPool.getMinThreads()).isEqualTo(8); + assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(200); + assertThat(queuedThreadPool.getIdleTimeout()).isEqualTo(60000); + assertThat(getQueue(queuedThreadPool, BlockingArrayQueue.class).getMaxCapacity()).isEqualTo(5150); + } + + @Test + void factoryReturnsBlockingArrayQueueWithCustomValues() { + bind("server.jetty.max-queue-capacity=5150", "server.jetty.min-threads=200", "server.jetty.max-threads=1000", + "server.jetty.thread-idle-timeout=10000"); + QueuedThreadPool queuedThreadPool = this.factory.create(); + assertThat(queuedThreadPool.getMinThreads()).isEqualTo(200); + assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(1000); + assertThat(queuedThreadPool.getIdleTimeout()).isEqualTo(10000); + assertThat(getQueue(queuedThreadPool, BlockingArrayQueue.class).getMaxCapacity()).isEqualTo(5150); + } + + private void bind(String... inlinedProperties) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties); + new Binder(ConfigurationPropertySources.get(this.environment)).bind("server", + Bindable.ofInstance(this.serverProperties)); + } + + static > T getQueue(QueuedThreadPool queuedThreadPool, + Class expectedQueueClass) { + Method getQueue = ReflectionUtils.findMethod(QueuedThreadPool.class, "getQueue"); + ReflectionUtils.makeAccessible(getQueue); + Object obj = ReflectionUtils.invokeMethod(getQueue, queuedThreadPool); + assertThat(obj).isInstanceOf(expectedQueueClass); + return expectedQueueClass.cast(obj); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java index 112c2efb4f..92f0f1491b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java @@ -22,14 +22,18 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.Test; import reactor.netty.http.server.HttpServer; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; +import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; @@ -53,6 +57,7 @@ import org.springframework.web.server.adapter.ForwardedHeaderTransformer; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -63,6 +68,7 @@ import static org.mockito.Mockito.verify; * @author Brian Clozel * @author Raheela Aslam * @author Madhura Bhave + * @author Chris Bono */ class ReactiveWebServerFactoryAutoConfigurationTests { @@ -242,6 +248,36 @@ class ReactiveWebServerFactoryAutoConfigurationTests { }); } + @Test + void jettyDefaultThreadPoolFactoryUsedToCreateThreadPoolOnWebServerFactory() { + new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(Tomcat.class, HttpServer.class)) + .withUserConfiguration(DoubleRegistrationJettyServerCustomizerConfiguration.class, + HttpHandlerConfiguration.class) + .withPropertyValues("server.port=0", "server.jetty.max-queue-capacity:5150").run((context) -> { + assertThat(context.getBean(JettyThreadPoolFactory.class)) + .isInstanceOf(JettyConstrainedQueuedThreadPoolFactory.class); + JettyReactiveWebServerFactory factory = context.getBean(JettyReactiveWebServerFactory.class); + assertThat(factory.getThreadPool()).isNotNull(); + }); + } + + @Test + void jettyCustomThreadPoolFactoryUsedToCreateThreadPoolOnWebServerFactory() { + new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(Tomcat.class, HttpServer.class)) + .withUserConfiguration(JettyCustomThreadPoolFactoryConfiguration.class, HttpHandlerConfiguration.class) + .withPropertyValues("server.port=0").run((context) -> { + JettyReactiveWebServerFactory factory = context.getBean(JettyReactiveWebServerFactory.class); + JettyThreadPoolFactory threadPoolFactory = context.getBean("jettyCustomThreadPoolFactory", + JettyThreadPoolFactory.class); + verify(threadPoolFactory, times(1)).create(); + assertThat(factory.getThreadPool()).isSameAs(JettyCustomThreadPoolFactoryConfiguration.threadPool); + }); + } + @Test void undertowBuilderCustomizerBeanIsAddedToFactory() { new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebApplicationContext::new) @@ -468,6 +504,20 @@ class ReactiveWebServerFactoryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JettyCustomThreadPoolFactoryConfiguration { + + static ThreadPool threadPool = new QueuedThreadPool(); + + @Bean + JettyThreadPoolFactory jettyCustomThreadPoolFactory() { + JettyThreadPoolFactory threadPoolFactory = mock(JettyThreadPoolFactory.class); + when(threadPoolFactory.create()).thenReturn(threadPool); + return threadPoolFactory; + } + + } + @Configuration(proxyBeanMethods = false) static class UndertowBuilderCustomizerConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java index cb7306bb0c..6a292d0c16 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java @@ -28,6 +28,8 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.Test; import reactor.netty.http.server.HttpServer; @@ -35,12 +37,14 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; @@ -64,6 +68,7 @@ import org.springframework.web.servlet.FrameworkServlet; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -76,6 +81,7 @@ import static org.mockito.Mockito.verify; * @author Stephane Nicoll * @author Raheela Aslam * @author Madhura Bhave + * @author Chris Bono */ class ServletWebServerFactoryAutoConfigurationTests { @@ -181,6 +187,40 @@ class ServletWebServerFactoryAutoConfigurationTests { }); } + @Test + void jettyDefaultThreadPoolFactoryUsedToCreateThreadPoolOnWebServerFactory() { + WebApplicationContextRunner runner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withClassLoader(new FilteredClassLoader(Tomcat.class, HttpServer.class)) + .withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class)) + .withPropertyValues("server.port:0", "server.jetty.max-queue-capacity:5150"); + runner.run((context) -> { + assertThat(context.getBean(JettyThreadPoolFactory.class)) + .isInstanceOf(JettyConstrainedQueuedThreadPoolFactory.class); + JettyServletWebServerFactory factory = context.getBean(JettyServletWebServerFactory.class); + assertThat(factory.getThreadPool()).isNotNull(); // its null by default so + // this verifies the + // factory was used + }); + } + + @Test + void jettyCustomThreadPoolFactoryUsedToCreateThreadPoolOnWebServerFactory() { + WebApplicationContextRunner runner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withClassLoader(new FilteredClassLoader(Tomcat.class, HttpServer.class)) + .withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class)) + .withUserConfiguration(JettyCustomThreadPoolFactoryConfiguration.class) + .withPropertyValues("server.port:0"); + runner.run((context) -> { + JettyServletWebServerFactory factory = context.getBean(JettyServletWebServerFactory.class); + JettyThreadPoolFactory threadPoolFactory = context.getBean("jettyCustomThreadPoolFactory", + JettyThreadPoolFactory.class); + verify(threadPoolFactory, times(1)).create(); + assertThat(factory.getThreadPool()).isSameAs(JettyCustomThreadPoolFactoryConfiguration.threadPool); + }); + } + @Test void undertowDeploymentInfoCustomizerBeanIsAddedToFactory() { WebApplicationContextRunner runner = new WebApplicationContextRunner( @@ -579,6 +619,20 @@ class ServletWebServerFactoryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JettyCustomThreadPoolFactoryConfiguration { + + static ThreadPool threadPool = new QueuedThreadPool(); + + @Bean + JettyThreadPoolFactory jettyCustomThreadPoolFactory() { + JettyThreadPoolFactory threadPoolFactory = mock(JettyThreadPoolFactory.class); + when(threadPoolFactory.create()).thenReturn(threadPool); + return threadPoolFactory; + } + + } + @Configuration(proxyBeanMethods = false) static class UndertowBuilderCustomizerConfiguration { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyThreadPoolFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyThreadPoolFactory.java new file mode 100644 index 0000000000..0bcc936542 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyThreadPoolFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2019 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 org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.ThreadPool; + +/** + * Factory to create a thread pool for use by a Jetty {@link Server}. + * + * @author Chris Bono + * @since 2.3.0 + * @param type of ThreadPool that the factory returns + */ +@FunctionalInterface +public interface JettyThreadPoolFactory { + + /** + * Creates a thread pool. + * @return a Jetty thread pool or null to use the default Jetty {@link Server} thread + * pool. + */ + T create(); + +}