-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Description
Version
5.0.7
Context
Vert.x Version: 5.0.0 ~ 5.0.7
JDK Version: 25
OS: Linux (Container environment)
Steps to reproduce
Describe the bug
When deploying a Verticle using .setInstances(N) combined with ThreadingModel.VIRTUAL_THREAD in Vert.x 5, all deployed instances share the exact same EventLoop thread, instead of being distributed across the EventLoopGroup in a round-robin fashion.
This causes a severe performance bottleneck where increasing the instance count does not scale I/O throughput, as all instances contend for a single thread for their I/O operations.
This behavior differs from Vert.x 4.5.x, where createVirtualThreadContext called eventLoopGroup.next() for each instance, ensuring proper distribution.
Steps to reproduce
- Configure Vert.x with a sufficient EventLoop pool size (e.g., 12).
- Deploy a simple HTTP Server Verticle with
.setInstances(10)andThreadingModel.VIRTUAL_THREAD. - Inspect the thread dump or log the
Thread.currentThread().getName()inside the request handler.
Result: All 10 instances report the same thread name (e.g., vert.x-eventloop-thread-0).
Note: Even if setEventLoopPoolSize(300) is set, only 1 EventLoop thread is created in the dump because Netty creates threads lazily only when they are actually used.
Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(12));
DeploymentOptions options = new DeploymentOptions()
.setInstances(10)
.setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(() -> new AbstractVerticle() {
@Override
public void start() {
// This prints the SAME thread name for all instances
System.out.println("Instance deployed on: " + Thread.currentThread().getName());
}
}, options);Root Cause Analysis (Code Level)
I have located the exact code causing this issue in io.vertx.core.impl.DefaultDeployment.java (lines approx. 150-161).
Inside the loop iterating over the number of instances:
// DefaultDeployment.java
case VIRTUAL_THREAD:
if (workerLoop == null) {
context = contextBuilder
.withThreadingModel(ThreadingModel.VIRTUAL_THREAD)
.build();
// The first EventLoop is captured here
workerLoop = context.nettyEventLoop();
} else {
context = contextBuilder
.withThreadingModel(ThreadingModel.VIRTUAL_THREAD)
.withEventLoop(workerLoop) // <--- BUG: The captured loop is reused for ALL subsequent instances
.build();
}
break;- The Logic Error: The
workerLoopvariable captures the EventLoop of the first instance and forces it upon all subsequent instances defined bysetInstances(). - Consequence: Even if I request 300 instances, they are all bound to the single
workerLoopselected in the first iteration.
Expected behavior
Each instance created via setInstances(N) should be assigned a distinct EventLoop via eventLoopGroup.next() (Round-Robin), utilizing the full available pool size.
Workaround
Deploying verticles individually in a manual loop bypasses this issue because a new deployment context is created for each call.
// Workaround: Deploy individually
for (int i = 0; i < INSTANCES; i++) {
DeploymentOptions options = new DeploymentOptions()
.setInstances(1) // Deploy 1 at a time
.setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(MyVerticle.class, options);
}Do you have a reproducer?
No response