Skip to content

Missing API to increase QueuedThreadPool maxThreads by leased threads + QoSHandler bug of exceeding maxRequestCount by 1 #13187

Open
@scscgit

Description

@scscgit

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:

  1. 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's maxThreads value. Alternatively, there is no way to specify how many "handler threads" there should be, so that the leased threads would be created automatically.
  2. 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 as leased 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();
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugFor general bugs on Jetty side

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions