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 all commits
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 @@ -29,6 +29,7 @@
import java.util.Map;

import io.undertow.UndertowOptions;
import io.undertow.server.handlers.RequestLimitingHandler;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
Expand Down Expand Up @@ -1033,6 +1034,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 +1104,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 Expand Up @@ -1294,6 +1308,21 @@ public static class Undertow {
*/
private Integer workerThreads;

/**
* Number of max concurrent requests. When specified will put a
* {@link RequestLimitingHandler} in place. The default is null. Can only be
* specified in Servlet environments.
*/
private Integer maxRequests;

/**
* Maximum number of requests to queue up when running with a
* {@link RequestLimitingHandler} and the {@link #maxRequests} is reached. The
* default is -1 (unbounded). Can only be specified in Servlet environments and is
* only used when {@link #maxRequests} is specified.
*/
private Integer maxQueueCapacity;

/**
* Whether to allocate buffers outside the Java heap. The default is derived from
* the maximum amount of memory that is available to the JVM.
Expand Down Expand Up @@ -1390,6 +1419,22 @@ public void setWorkerThreads(Integer workerThreads) {
this.workerThreads = workerThreads;
}

public Integer getMaxRequests() {
return this.maxRequests;
}

public void setMaxRequests(Integer maxRequests) {
this.maxRequests = maxRequests;
}

public Integer getMaxQueueCapacity() {
return this.maxQueueCapacity;
}

public void setMaxQueueCapacity(Integer maxQueueCapacity) {
this.maxQueueCapacity = maxQueueCapacity;
}

public Boolean getDirectBuffers() {
return this.directBuffers;
}
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 @@ -78,6 +78,13 @@ public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCust
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "io.undertow.Undertow")
public UndertowServletWebServerFactoryCustomizer undertowServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new UndertowServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class)
@ConditionalOnProperty(value = "server.forward-headers-strategy", havingValue = "framework")
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 @@ -16,6 +16,11 @@

package org.springframework.boot.autoconfigure.web.servlet;

import io.undertow.Handlers;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.RequestLimitingHandler;

import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
Expand All @@ -25,6 +30,7 @@
* Servlet web servers.
*
* @author Andy Wilkinson
* @author Chris Bono
* @since 2.1.7
*/
public class UndertowServletWebServerFactoryCustomizer
Expand All @@ -36,10 +42,55 @@ public UndertowServletWebServerFactoryCustomizer(ServerProperties serverProperti
this.serverProperties = serverProperties;
}

// TODO why was this not being used before now?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this Undertow specific customizer exists but was not being called anywhere. Will this be a problem?


@Override
public void customize(UndertowServletWebServerFactory factory) {

factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo
.setEagerFilterInit(this.serverProperties.getUndertow().isEagerFilterInit()));

addRateLimiterIfMaxRequestsSet(factory);
}

/**
* Installs a {@link RequestLimitingHandler} in the initial handler chain if
* {@link ServerProperties.Undertow#maxRequests} is set. If installed, it will use
* {@link ServerProperties.Undertow#maxQueueCapacity} to control the size of the queue
* in the rate limting handler.
* @param factory the factory to add the deployment info customizer on
*/
private void addRateLimiterIfMaxRequestsSet(UndertowServletWebServerFactory factory) {
Integer maxRequests = this.serverProperties.getUndertow().getMaxRequests();
if (maxRequests == null) {
return;
}
Integer maxQueueCapacity = this.serverProperties.getUndertow().getMaxQueueCapacity();
factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo.addInitialHandlerChainWrapper(
new RequestLimiterHandlerWrapper(maxRequests, (maxQueueCapacity != null) ? maxQueueCapacity : -1)));
}

/**
* Explicit implementation of HandlerWrapper rather than Lambda to make it possible to
* verify the handler wrappers at runtime during tests. This could be instead written
* as a Lambda but then tests can not see what type of handler wrapper it is.
*/
static class RequestLimiterHandlerWrapper implements HandlerWrapper {

Integer maxRequests;

Integer maxQueueCapacity;

RequestLimiterHandlerWrapper(Integer maxRequests, Integer maxQueueCapacity) {
this.maxRequests = maxRequests;
this.maxQueueCapacity = maxQueueCapacity;
}

@Override
public HttpHandler wrap(HttpHandler handler) {
return Handlers.requestLimitingHandler(this.maxRequests, this.maxQueueCapacity, handler);
}

}

}
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 All @@ -252,6 +259,30 @@ void testCustomizeUndertowSocketOption() {
"true");
}

@Test
void testCustomizeUndertowWorkerThreads() {
bind("server.undertow.worker-threads", "150");
assertThat(this.properties.getUndertow().getWorkerThreads()).isEqualTo(150);
}

@Test
void testCustomizeUndertowIoThreads() {
bind("server.undertow.io-threads", "10");
assertThat(this.properties.getUndertow().getIoThreads()).isEqualTo(10);
}

@Test
void testCustomizeUndertowMaxRequests() {
bind("server.undertow.max-requests", "200");
assertThat(this.properties.getUndertow().getMaxRequests()).isEqualTo(200);
}

@Test
void testCustomizeUndertowMaxQueueCapacity() {
bind("server.undertow.max-queue-capacity", "100");
assertThat(this.properties.getUndertow().getMaxQueueCapacity()).isEqualTo(100);
}

@Test
void testCustomizeJettyAccessLog() {
Map<String, String> map = new HashMap<>();
Expand Down
Loading