Skip to content

Add support for configuring embedded Jetty's max queue capacity #19494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,11 @@ public static class Jetty {
*/
private Integer minThreads = 8;

/**
* Maximum capacity of the thread pools backing queue.
*/
private Integer maxQueueCapacity;

/**
* Maximum thread idle time.
*/
Expand Down Expand Up @@ -1098,6 +1103,14 @@ public Integer 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<QueuedThreadPool> {

private ServerProperties serverProperties;

public JettyConstrainedQueuedThreadPoolFactory(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}

/**
* <p>
* Creates a {@link QueuedThreadPool} with the following settings (if
* {@link Jetty#maxQueueCapacity} is specified):
* <ul>
* <li>min threads set to {@link Jetty#minThreads} or {@code 8} if not specified.
* <li>max threads set to {@link Jetty#maxThreads} or {@code 200} if not specified.
* <li>idle timeout set to {@link Jetty#threadIdleTimeout} or {@code 60000} if not
* specified.</li>
* <li>if {@link Jetty#maxQueueCapacity} is zero its backing queue will be a
* {@link SynchronousQueue} otherwise it will be a {@link BlockingArrayQueue} whose
* max capacity is set to the max queue depth.
* </ul>
* @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<Runnable> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,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.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;
Expand Down Expand Up @@ -101,6 +105,7 @@ TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory(
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
@ConditionalOnClass({ org.eclipse.jetty.server.Server.class })
@EnableConfigurationProperties(ServerProperties.class)
static class EmbeddedJetty {

@Bean
Expand All @@ -109,10 +114,17 @@ JettyResourceFactory jettyServerResourceFactory() {
return new JettyResourceFactory();
}

@Bean
@ConditionalOnMissingBean
JettyThreadPoolFactory jettyThreadPoolFactory(ServerProperties serverProperties) {
return new JettyConstrainedQueuedThreadPoolFactory(serverProperties);
}

@Bean
JettyReactiveWebServerFactory jettyReactiveWebServerFactory(JettyResourceFactory resourceFactory,
ObjectProvider<JettyServerCustomizer> serverCustomizers) {
JettyThreadPoolFactory threadPoolFactory, ObjectProvider<JettyServerCustomizer> serverCustomizers) {
JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory();
serverFactory.setThreadPool(threadPoolFactory.create());
serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
serverFactory.setResourceFactory(resourceFactory);
return serverFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@
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;
Expand Down Expand Up @@ -90,12 +94,20 @@ TomcatServletWebServerFactory tomcatServletWebServerFactory(
@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<JettyServerCustomizer> serverCustomizers) {
JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
factory.setThreadPool(threadPoolFactory.create());
factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
* @author Andrew McGhie
* @author HaiTao Zhang
* @author Rafiullah Hamedy
* @author Chris Bono
*/
class ServerPropertiesTests {

Expand Down Expand Up @@ -238,6 +239,12 @@ void testCustomizeJettyIdleTimeout() {
assertThat(this.properties.getJetty().getThreadIdleTimeout()).isEqualTo(Duration.ofSeconds(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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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 extends BlockingQueue<Runnable>> T getQueue(QueuedThreadPool queuedThreadPool,
Class<T> 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);
}

}
Loading