vert.xvirtual-threads

Vert.x virtual thread per request


I am testing vert.x with virtual threads (java 21, vert.x 4.5.0).

I wonder how the await method is supposed to be used in the context of a web server and handlers specifically.

Say I have a basic server verticle like this:

public class Server extends AbstractVerticle {

    @Override
    public void start() {
        try {
            Router router = Router.router(vertx);
            router.route("/health").handler(ctx -> {
                await(ctx.response().end());
            });

            vertx.createHttpServer()
                    .requestHandler(router)
                    .listen(8080);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) {
    Vertx.vertx().deployVerticle(new Server());
}

Now if I call the health endpoint the following error is printed: java.lang.IllegalStateException: Cannot be called on a Vert.x event-loop thread

Which makes sense, the await is blocking the event loop.

So my initial guess was to add ThreadingModel.VIRTUAL_THREAD like this:

public static void main(String[] args) {
        Vertx.vertx().deployVerticle(new Server(),
                new DeploymentOptions()
                        .setThreadingModel(ThreadingModel.VIRTUAL_THREAD));

        while (true) {
        }
    }

(note the required while true now because else the application exits immediately)

Now if I call /health the error is not printed anymore. But I am not sure if this does what I expect it to.

I would think that with this I am able to have 1 virtual thread per request, but I am not sure if this is what is actually happening, as there is no documentation about the virtual thread threading model.

So my question is: is this the correct way to start a server where handlers can freely await all futures? Does this start one virtual thread per request? Also, is there a way to get rid of the while true loop?

Edit: After receiving the initial detailed response, I tried to test this behavior, but found that it does not behave as expected:

I edited my handler to be blocking on purpose:

router.route("/health").handler(ctx -> {
                System.out.println("Enter: " + Thread.currentThread().getName());
                await(vertx.executeBlocking(() -> {
                    System.out.println("Blocking: " + Thread.currentThread().getName());
                    Thread.sleep(10000);
                    return null;
                }));
                System.out.println("Exit: " + Thread.currentThread().getName());
                await(ctx.response().end());
            });

Now if I do two concurrent requests I see the following printed:

Enter: vert.x-virtual-thread-2

Blocking: vert.x-virtual-thread-3

Exit: vert.x-virtual-thread-2

Enter: vert.x-virtual-thread-3

Blocking: vert.x-virtual-thread-5

Exit: vert.x-virtual-thread-3

And the requests are executed one after another, meaning my server is blocked! Is my way of sleeping in the handler maybe the problem?

Edit #2:

Setting ordered to false in executeBlocking resolves the issue. I am still not sure why I need to call executeBlocking at all in an virtual-thread based handler, as blocking the virtual thread of the handler should not block the server in my opinion. If anyone has an answer to this I would appreciate a comment, but I will resolve this issue for now.


Solution

  • If ThreadingModel.VIRTUAL_THREAD is specified, then upon deployment and creation of Vertx instance the Context with virtualThreaWorkerPool will be created. This WorkerPool will use an ExecutorService/pool which is created with ThreadFactory which creates virtual threads; in fact Vertx's static VIRTUAL_THREAD_FACTORY is created as Thread.ofVirtual().factory().

    So, your handler will be executed on a virtual thread - one virtual thread per request - exactly as ThreadingModel.VIRTUAL_THREAD promises. You could check it in the handler implementation

    router.route("/health").handler(ctx -> {
        ...
        boolean virtual = Thread.currentThread().isVirtual(); // renders true
        ...
    });
    

    Your while(true) endless loop in the case of ThreadingModel.VIRTUAL_THREAD is an unnecessary CPU hogging.

    while (true) { // spurious wakeups
        synchronized (Server.class) {
            Server.class.wait();
        }
    }
    

    will serve the purpose better. Or, if we love to hate synchronized:

    while (true) {
        LockSupport.park();
    }