eclipse-vertx / eclipse-vertx/vert.x
[Vert.x 5] Regression: setInstances(N) shares a single EventLoop for all instances when using ThreadingModel.VIRTUAL_THREAD
- Dominant language
- Java
- Stars
- 14.7k
- Forks
- 2.1k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 27
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
1. Configure Vert.x with a sufficient EventLoop pool size (e.g., 12).
2. Deploy a simple HTTP Server Verticle with `.setInstances(10)` and `ThreadingModel.VIRTUAL_THREAD`.
3. 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.
```java
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:
```java
// 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 `workerLoop` variable captures the EventLoop of the first instance and forces it upon all subsequent instances defined by `setInstances()`.
* **Consequence:** Even if I request 300 instances, they are all bound to the single `workerLoop` selected 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.
```java
// 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_
Contributor guide
Assessment
This issue has not been assessed yet.