eclipse-vertx / eclipse-vertx/vert.x

[Vert.x 5] Regression: setInstances(N) shares a single EventLoop for all instances when using ThreadingModel.VIRTUAL_THREAD

Open
#5,924 2 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.