From b26a0ebb6a9b06eb050fa234299756b159d937f1 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Fri, 14 Aug 2026 19:12:56 -0400 Subject: [PATCH] Assign a distinct event loop to each virtual thread verticle instance Motivation: Deploying a verticle with setInstances(N) and ThreadingModel.VIRTUAL_THREAD pins every instance to the same event loop, so raising the instance count does not scale I/O: the single event loop saturates while the virtual threads starve. Vert.x 4.x assigned an event loop per instance via eventLoopGroup.next(). The shared event loop was introduced for worker verticle deployments, where the work happens on the worker pool and a single event loop consumes fewer resources. The virtual thread model was changed at the same time, although that rationale does not apply to it. Changes: Build a fresh context for each virtual thread instance so that each one round-robins over the event loop group, like the event-loop model does. The shared event loop remains for worker deployments. --- .../tests/virtualthread/DeploymentTest.java | 18 ++++++++++++++++++ .../impl/deployment/DefaultDeployment.java | 14 +++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/vertx-core-java21-tests/src/test/java/io/vertx/tests/virtualthread/DeploymentTest.java b/vertx-core-java21-tests/src/test/java/io/vertx/tests/virtualthread/DeploymentTest.java index 01e857abc7e..7021da21bca 100644 --- a/vertx-core-java21-tests/src/test/java/io/vertx/tests/virtualthread/DeploymentTest.java +++ b/vertx-core-java21-tests/src/test/java/io/vertx/tests/virtualthread/DeploymentTest.java @@ -13,6 +13,7 @@ import io.vertx.core.*; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; +import io.vertx.core.internal.ContextInternal; import io.vertx.test.core.VertxTestBase; import io.vertx.test.http.HttpTestBase; import org.junit.Assert; @@ -21,6 +22,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -43,6 +45,22 @@ public void start() { await(); } + @Test + public void testInstancesUseDistinctEventLoopThreads() { + int instances = 4; + Vertx vertx = vertx(new VertxOptions().setEventLoopPoolSize(2 * instances)); + Set eventLoops = Collections.synchronizedSet(new HashSet<>()); + vertx.deployVerticle(() -> new AbstractVerticle() { + @Override + public void start() { + CompletableFuture latch = new CompletableFuture<>(); + ((ContextInternal) context).eventLoop().execute(() -> latch.complete(Thread.currentThread().getName())); + eventLoops.add(latch.join()); + } + }, new DeploymentOptions().setInstances(instances).setThreadingModel(ThreadingModel.VIRTUAL_THREAD)).await(); + Assert.assertEquals(instances, eventLoops.size()); + } + @Test public void testExecuteBlocking() { Promise p = Promise.promise(); diff --git a/vertx-core/src/main/java/io/vertx/core/impl/deployment/DefaultDeployment.java b/vertx-core/src/main/java/io/vertx/core/impl/deployment/DefaultDeployment.java index f0a9704a37b..ee8f615273f 100644 --- a/vertx-core/src/main/java/io/vertx/core/impl/deployment/DefaultDeployment.java +++ b/vertx-core/src/main/java/io/vertx/core/impl/deployment/DefaultDeployment.java @@ -152,17 +152,9 @@ public Future deploy(DeploymentContext deployment) { } break; case VIRTUAL_THREAD: - if (workerLoop == null) { - context = contextBuilder - .withThreadingModel(ThreadingModel.VIRTUAL_THREAD) - .build(); - workerLoop = context.nettyEventLoop(); - } else { - context = contextBuilder - .withThreadingModel(ThreadingModel.VIRTUAL_THREAD) - .withEventLoop(workerLoop) - .build(); - } + context = contextBuilder + .withThreadingModel(ThreadingModel.VIRTUAL_THREAD) + .build(); break; default: context = contextBuilder