Skip to content

Commit 852734b

Browse files
cbo-indeedsnicoll
authored andcommitted
Add support for configuring Jetty's backing queue
See gh-19494
1 parent a6fdbdc commit 852734b

File tree

9 files changed

+399
-2
lines changed

9 files changed

+399
-2
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,11 @@ public static class Jetty {
10411041
*/
10421042
private Integer minThreads = 8;
10431043

1044+
/**
1045+
* Maximum capacity of the thread pools backing queue.
1046+
*/
1047+
private Integer maxQueueCapacity;
1048+
10441049
/**
10451050
* Maximum thread idle time.
10461051
*/
@@ -1106,6 +1111,14 @@ public Integer getMaxThreads() {
11061111
return this.maxThreads;
11071112
}
11081113

1114+
public void setMaxQueueCapacity(Integer maxQueueCapacity) {
1115+
this.maxQueueCapacity = maxQueueCapacity;
1116+
}
1117+
1118+
public Integer getMaxQueueCapacity() {
1119+
return this.maxQueueCapacity;
1120+
}
1121+
11091122
public void setThreadIdleTimeout(Duration threadIdleTimeout) {
11101123
this.threadIdleTimeout = threadIdleTimeout;
11111124
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.embedded;
18+
19+
import java.time.Duration;
20+
import java.util.concurrent.BlockingQueue;
21+
import java.util.concurrent.SynchronousQueue;
22+
23+
import org.eclipse.jetty.util.BlockingArrayQueue;
24+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
25+
26+
import org.springframework.boot.autoconfigure.web.ServerProperties;
27+
import org.springframework.boot.autoconfigure.web.ServerProperties.Jetty;
28+
import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory;
29+
30+
/**
31+
* A {@link JettyThreadPoolFactory} that creates a thread pool that uses a backing queue
32+
* with a max capacity if the {@link Jetty#maxQueueCapacity} is specified. If the max
33+
* capacity is not specified then the factory will return null thus allowing the standard
34+
* Jetty server thread pool to be used.
35+
*
36+
* @author Chris Bono
37+
* @since 2.3.0
38+
*/
39+
public class JettyConstrainedQueuedThreadPoolFactory implements JettyThreadPoolFactory<QueuedThreadPool> {
40+
41+
private ServerProperties serverProperties;
42+
43+
public JettyConstrainedQueuedThreadPoolFactory(ServerProperties serverProperties) {
44+
this.serverProperties = serverProperties;
45+
}
46+
47+
/**
48+
* <p>
49+
* Creates a {@link QueuedThreadPool} with the following settings (if
50+
* {@link Jetty#maxQueueCapacity} is specified):
51+
* <ul>
52+
* <li>min threads set to {@link Jetty#minThreads} or {@code 8} if not specified.
53+
* <li>max threads set to {@link Jetty#maxThreads} or {@code 200} if not specified.
54+
* <li>idle timeout set to {@link Jetty#threadIdleTimeout} or {@code 60000} if not
55+
* specified.</li>
56+
* <li>if {@link Jetty#maxQueueCapacity} is zero its backing queue will be a
57+
* {@link SynchronousQueue} otherwise it will be a {@link BlockingArrayQueue} whose
58+
* max capacity is set to the max queue depth.
59+
* </ul>
60+
* @return thread pool as described above or {@code null} if
61+
* {@link Jetty#maxQueueCapacity} is not specified.
62+
*/
63+
@Override
64+
public QueuedThreadPool create() {
65+
66+
Integer maxQueueCapacity = this.serverProperties.getJetty().getMaxQueueCapacity();
67+
68+
// Max depth is not specified - let Jetty server use its defaults in this case
69+
if (maxQueueCapacity == null) {
70+
return null;
71+
}
72+
73+
BlockingQueue<Runnable> queue;
74+
if (maxQueueCapacity == 0) {
75+
/**
76+
* This queue will cause jetty to reject requests whenever there is no idle
77+
* thread available to handle them. If this queue is used, it is strongly
78+
* recommended to set _minThreads equal to _maxThreads. Jetty's
79+
* QueuedThreadPool class may not behave like a regular java thread pool and
80+
* may not add threads properly when a SynchronousQueue is used.
81+
*/
82+
queue = new SynchronousQueue<>();
83+
}
84+
else {
85+
/**
86+
* Create a queue of fixed size. This queue will not grow. If a request
87+
* arrives and the queue is empty, the client will see an immediate
88+
* "connection reset" error.
89+
*/
90+
queue = new BlockingArrayQueue<>(maxQueueCapacity);
91+
}
92+
93+
Integer maxThreadCount = this.serverProperties.getJetty().getMaxThreads();
94+
Integer minThreadCount = this.serverProperties.getJetty().getMinThreads();
95+
Duration threadIdleTimeout = this.serverProperties.getJetty().getThreadIdleTimeout();
96+
97+
return new QueuedThreadPool((maxThreadCount != null) ? maxThreadCount : 200,
98+
(minThreadCount != null) ? minThreadCount : 8,
99+
(threadIdleTimeout != null) ? (int) threadIdleTimeout.toMillis() : 60000, queue);
100+
}
101+
102+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424
import org.springframework.beans.factory.ObjectProvider;
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
27+
import org.springframework.boot.autoconfigure.web.ServerProperties;
28+
import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory;
29+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2730
import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory;
2831
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
32+
import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory;
2933
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
3034
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
3135
import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
@@ -101,6 +105,7 @@ TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory(
101105
@Configuration(proxyBeanMethods = false)
102106
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
103107
@ConditionalOnClass({ org.eclipse.jetty.server.Server.class })
108+
@EnableConfigurationProperties(ServerProperties.class)
104109
static class EmbeddedJetty {
105110

106111
@Bean
@@ -109,10 +114,17 @@ JettyResourceFactory jettyServerResourceFactory() {
109114
return new JettyResourceFactory();
110115
}
111116

117+
@Bean
118+
@ConditionalOnMissingBean
119+
JettyThreadPoolFactory jettyThreadPoolFactory(ServerProperties serverProperties) {
120+
return new JettyConstrainedQueuedThreadPoolFactory(serverProperties);
121+
}
122+
112123
@Bean
113124
JettyReactiveWebServerFactory jettyReactiveWebServerFactory(JettyResourceFactory resourceFactory,
114-
ObjectProvider<JettyServerCustomizer> serverCustomizers) {
125+
JettyThreadPoolFactory threadPoolFactory, ObjectProvider<JettyServerCustomizer> serverCustomizers) {
115126
JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory();
127+
serverFactory.setThreadPool(threadPoolFactory.create());
116128
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
117129
serverFactory.setResourceFactory(resourceFactory);
118130
return serverFactory;

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@
3232
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3333
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3434
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
35+
import org.springframework.boot.autoconfigure.web.ServerProperties;
36+
import org.springframework.boot.autoconfigure.web.embedded.JettyConstrainedQueuedThreadPoolFactory;
37+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3538
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
3639
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
40+
import org.springframework.boot.web.embedded.jetty.JettyThreadPoolFactory;
3741
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
3842
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
3943
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
@@ -90,12 +94,20 @@ TomcatServletWebServerFactory tomcatServletWebServerFactory(
9094
@Configuration(proxyBeanMethods = false)
9195
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
9296
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
97+
@EnableConfigurationProperties(ServerProperties.class)
9398
static class EmbeddedJetty {
9499

95100
@Bean
96-
JettyServletWebServerFactory JettyServletWebServerFactory(
101+
@ConditionalOnMissingBean
102+
JettyThreadPoolFactory jettyThreadPoolFactory(ServerProperties serverProperties) {
103+
return new JettyConstrainedQueuedThreadPoolFactory(serverProperties);
104+
}
105+
106+
@Bean
107+
JettyServletWebServerFactory JettyServletWebServerFactory(JettyThreadPoolFactory threadPoolFactory,
97108
ObjectProvider<JettyServerCustomizer> serverCustomizers) {
98109
JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
110+
factory.setThreadPool(threadPoolFactory.create());
99111
factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
100112
return factory;
101113
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
* @author Andrew McGhie
7676
* @author HaiTao Zhang
7777
* @author Rafiullah Hamedy
78+
* @author Chris Bono
7879
*/
7980
class ServerPropertiesTests {
8081

@@ -237,6 +238,12 @@ void testCustomizeJettyIdleTimeout() {
237238
assertThat(this.properties.getJetty().getThreadIdleTimeout()).hasSeconds(10);
238239
}
239240

241+
@Test
242+
void testCustomizeJettyMaxQueueCapacity() {
243+
bind("server.jetty.max-queue-capacity", "5150");
244+
assertThat(this.properties.getJetty().getMaxQueueCapacity()).isEqualTo(5150);
245+
}
246+
240247
@Test
241248
void testCustomizeUndertowServerOption() {
242249
bind("server.undertow.options.server.ALWAYS_SET_KEEP_ALIVE", "true");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.embedded;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.concurrent.BlockingQueue;
21+
import java.util.concurrent.SynchronousQueue;
22+
23+
import org.eclipse.jetty.util.BlockingArrayQueue;
24+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
import org.springframework.boot.autoconfigure.web.ServerProperties;
29+
import org.springframework.boot.context.properties.bind.Bindable;
30+
import org.springframework.boot.context.properties.bind.Binder;
31+
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
32+
import org.springframework.mock.env.MockEnvironment;
33+
import org.springframework.test.context.support.TestPropertySourceUtils;
34+
import org.springframework.util.ReflectionUtils;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
/**
39+
* Tests for {@link JettyConstrainedQueuedThreadPoolFactory}.
40+
*
41+
* @author Chris Bono
42+
*/
43+
class JettyConstrainedQueuedThreadPoolFactoryTest {
44+
45+
private MockEnvironment environment;
46+
47+
private ServerProperties serverProperties;
48+
49+
private JettyConstrainedQueuedThreadPoolFactory factory;
50+
51+
@BeforeEach
52+
void setup() {
53+
this.environment = new MockEnvironment();
54+
this.serverProperties = new ServerProperties();
55+
ConfigurationPropertySources.attach(this.environment);
56+
this.factory = new JettyConstrainedQueuedThreadPoolFactory(this.serverProperties);
57+
}
58+
59+
@Test
60+
void factoryReturnsNullWhenMaxCapacityNotSpecified() {
61+
bind("server.jetty.max-queue-capacity=");
62+
assertThat(this.factory.create()).isNull();
63+
}
64+
65+
@Test
66+
void factoryReturnsSynchronousQueueWhenMaxCapacityIsZero() {
67+
bind("server.jetty.max-queue-capacity=0");
68+
QueuedThreadPool queuedThreadPool = this.factory.create();
69+
assertThat(getQueue(queuedThreadPool, SynchronousQueue.class)).isNotNull();
70+
}
71+
72+
@Test
73+
void factoryReturnsBlockingArrayQueueWithDefaultsWhenOnlyMaxCapacityIsSet() {
74+
bind("server.jetty.max-queue-capacity=5150");
75+
QueuedThreadPool queuedThreadPool = this.factory.create();
76+
assertThat(queuedThreadPool.getMinThreads()).isEqualTo(8);
77+
assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(200);
78+
assertThat(queuedThreadPool.getIdleTimeout()).isEqualTo(60000);
79+
assertThat(getQueue(queuedThreadPool, BlockingArrayQueue.class).getMaxCapacity()).isEqualTo(5150);
80+
}
81+
82+
@Test
83+
void factoryReturnsBlockingArrayQueueWithCustomValues() {
84+
bind("server.jetty.max-queue-capacity=5150", "server.jetty.min-threads=200", "server.jetty.max-threads=1000",
85+
"server.jetty.thread-idle-timeout=10000");
86+
QueuedThreadPool queuedThreadPool = this.factory.create();
87+
assertThat(queuedThreadPool.getMinThreads()).isEqualTo(200);
88+
assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(1000);
89+
assertThat(queuedThreadPool.getIdleTimeout()).isEqualTo(10000);
90+
assertThat(getQueue(queuedThreadPool, BlockingArrayQueue.class).getMaxCapacity()).isEqualTo(5150);
91+
}
92+
93+
private void bind(String... inlinedProperties) {
94+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties);
95+
new Binder(ConfigurationPropertySources.get(this.environment)).bind("server",
96+
Bindable.ofInstance(this.serverProperties));
97+
}
98+
99+
static <T extends BlockingQueue<Runnable>> T getQueue(QueuedThreadPool queuedThreadPool,
100+
Class<T> expectedQueueClass) {
101+
Method getQueue = ReflectionUtils.findMethod(QueuedThreadPool.class, "getQueue");
102+
ReflectionUtils.makeAccessible(getQueue);
103+
Object obj = ReflectionUtils.invokeMethod(getQueue, queuedThreadPool);
104+
assertThat(obj).isInstanceOf(expectedQueueClass);
105+
return expectedQueueClass.cast(obj);
106+
}
107+
108+
}

0 commit comments

Comments
 (0)