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.
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();
}