Description
Jetty version(s)
12.0.15
Java version/vendor
openjdk-23.0.1
OS type/version
Windows 10
Description
When configuring a QueuedThreadPool
for the Server
, we may have one practical dilemma:
If we limit the server by a number of requests, i.e. using new QoSHandler().setMaxRequestCount(1)
, we also need to decide what number of threads to allow. If our handler is single-threaded, our educated guess is to reserve one thread per request.
However, Jetty Server also runs internal threads in this pool. The implementation in org.eclipse.jetty.util.thread.ThreadPoolBudget#check
makes sure there is enough space for all these so-called "leased" threads. My concerns are:
- There is no API (or a constant) to retrieve the number of (expected) leased threads, so we can use it to add to the
QueuedThreadPool
'smaxThreads
value. Alternatively, there is no way to specify how many "handler threads" there should be, so that the leased threads would be created automatically. - The Thread Pool documentation (mentioned in QueuedThreadPool throws error on quick successive execute calls if initialized with BlockingArrayQueue or ArrayBlockingQueue #13004) claims that "the server implementation may produce a number of tasks that must be run by the thread pool". It's not explicitly explained when and how many threads may need to run at any time. If we specify our
maxThreads
asleased threads + handler threads
, the maximum number of available handler threads may decrease, even below zero.
In practice, we'll usually set the maxThreads
to a value like handler threads + 8
, but this is not a portable solution, and is possibly wasteful. If Jetty implementation changes the number of leased threads in a future version, hard-coded constants may cause our application to misbehave.
Additionally, there is one specific bug of QoSHandler
, which can be reproduced if the maxThreads
is set to exactly leased threads + 1
(adding one more thread fixes the problem).
The steps are to set maxThreads
to 4 (assuming there are 3 leased threads), and configure:
qosHandler.setMaxRequestCount(1);
qosHandler.setMaxSuspendedRequestCount(0);
This should mean there is exactly one thread available to process exactly one request, with no queue for pending requests.
Expected output should be 1 OK response 200
and an instant rejection of 2 other requests with an error response 503
.
Actual result: 2 responses 200
and 1 response 503
.
Started
Started Handle
Thread 1 ended: 503
Finished Handle
Started Handle
Thread 2 ended: 200
Finished Handle
Thread 3 ended: 200
And actually, it's not even necessarily exactly 1 success result. With 20 threads, I reproduced 2 errors and 18 successes.
Plus I'd like to confirm one more thing: if I replace new Handler.Abstract()
by new Handler.Abstract.NonBlocking()
in the same code, the maxRequestCount
of QoSHandler
is completely ignored, as if maxSuspendedRequestCount
were infinite. Hopefully that's by design.
Thanks.
How to reproduce?
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.QoSHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public static void main(String[] args) {
final int leasedThreads = 3;
QueuedThreadPool queuedThreadPool = new QueuedThreadPool(leasedThreads + 1, 1, 2000);
queuedThreadPool.setDaemon(true);
Server server = new Server(queuedThreadPool);
ServerConnector connector = new ServerConnector(server);
connector.setHost("localhost");
connector.setPort(8081);
server.setConnectors(new Connector[]{connector});
QoSHandler qosHandler = new QoSHandler();
qosHandler.setMaxRequestCount(1);
qosHandler.setMaxSuspendedRequestCount(0);
qosHandler.setHandler(new Handler.Abstract(){
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
try {
System.out.println("Started Handle");
Thread.sleep(1000);
System.out.println("Finished Handle");
callback.succeeded();
return true;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
server.setHandler(qosHandler);
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("Started");
for (int i = 0; i < 3; i++) {
final int index = i + 1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
try {
HttpResponse<String> response = HttpClient.newHttpClient().send(
HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8081"))
.GET()
.build(),
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Thread " + index + " ended: " + response.statusCode());
} catch (IOException | InterruptedException e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}).start();
}
}