Copilot commented on code in PR #16370:
URL: https://github.com/apache/dubbo/pull/16370#discussion_r3540902980
##########
dubbo-plugin/dubbo-plugin-loom/src/test/java/org/apache/dubbo/common/threadpool/support/loom/VirtualThreadPoolTest.java:
##########
@@ -94,4 +107,63 @@ void getExecutor3() throws Exception {
latch.await();
assertThat(latch.getCount(), is(0L));
}
+
+ /**
+ * Verifies that in pooled mode, a warm virtual thread can be reused for a
second task,
+ * making ThreadLocal-cached values visible across consecutive submissions.
+ *
+ * <p>This is the key property that justifies the pooled mode (see issue
#16042): libraries
+ * like FastJSON and Aerospike Java client store large byte-buffers in
ThreadLocals. If threads
+ * are reused, those buffers survive across requests (reducing GC
pressure). If every task
+ * gets a fresh thread the cache is useless.
+ *
+ * <p>The test submits two tasks sequentially to a pooled executor with
corePoolSize=1.
+ * The first task sets a ThreadLocal value; the second task checks whether
the same thread
+ * ran it and whether the ThreadLocal value is still present.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void getExecutor4_threadLocalReuseInPooledMode() throws Exception {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path?" +
THREADS_VIRTUAL_CORE + "=1&"
+ + THREAD_NAME_KEY + "=pool-reuse-test");
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
Review Comment:
`ThreadPoolExecutor` with a `SynchronousQueue` can still create a new
(non-core) thread for task #2 if the existing core worker hasn't reached the
blocking `take()` yet when `execute()` is called. That makes this test's
implicit assumption (same thread reused) timing-dependent and potentially flaky.
To make the test deterministic, cap `maximumPoolSize` to `1` for this test
instance so the pool cannot spawn an extra thread between the two submissions.
##########
dubbo-plugin/dubbo-plugin-loom/src/test/java/org/apache/dubbo/common/threadpool/support/loom/VirtualThreadPoolBenchmarkTest.java:
##########
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.common.threadpool.support.loom;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.threadpool.ThreadPool;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static
org.apache.dubbo.common.constants.CommonConstants.THREADS_VIRTUAL_CORE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Benchmark-methodology correctness tests for {@link VirtualThreadPool}.
+ *
+ * <h2>Background (issue #16174)</h2>
+ *
+ * <p>The benchmark in issue #16042 / PR #16055 contained a synchronization
bug: the timing code
+ * awaited {@code countDownLatch1} (the <em>start gate</em>) rather than
{@code countDownLatch2}
+ * (the <em>completion latch</em>). This meant the elapsed time was measured
before all tasks had
+ * finished, making the pooled executor appear faster than the non-pooled one
even though both
+ * modes complete all tasks in roughly the same wall-clock time.
+ *
+ * <h2>Correct two-latch pattern</h2>
+ *
+ * <pre>{@code
+ * CountDownLatch startGate = new CountDownLatch(1); // latch 1 -
release all tasks together
+ * CountDownLatch completionLatch = new CountDownLatch(N); // latch 2 - await
ALL task completions
+ *
+ * for (int i = 0; i < N; i++) {
+ * executor.execute(() -> {
+ * startGate.await(); // wait until everyone is ready
+ * doWork();
+ * completionLatch.countDown(); // signal completion
+ * });
+ * }
+ *
+ * long t0 = System.nanoTime();
+ * startGate.countDown(); // release all tasks simultaneously
+ * completionLatch.await(); // MUST await the COMPLETION latch, NOT
the start gate
+ * long elapsed = System.nanoTime() - t0;
+ * }</pre>
+ *
+ * <p>These tests verify that:
+ * <ol>
+ * <li>Both pooled and non-pooled executors complete <em>all</em> tasks
before the timing window
+ * closes (i.e. they never return early due to the wrong latch being
awaited).
+ * <li>The task completion count exactly equals the number of submitted
tasks in both modes.
+ * </ol>
+ */
+public class VirtualThreadPoolBenchmarkTest {
+
+ private static final int TASK_COUNT = 200;
+
+ /**
+ * Verifies that the non-pooled (default) executor runs all tasks to
completion when measured
+ * with the corrected two-latch pattern.
+ *
+ * <p>The start gate ({@code startGate}) releases all submitted tasks at
the same time so they
+ * compete for the scheduler simultaneously. The completion latch ({@code
completionLatch}) is
+ * decremented by every task when it finishes. Only after
<em>completionLatch</em> reaches zero
+ * do we stop the clock, ensuring the elapsed time reflects actual
end-to-end execution.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void unpooledExecutor_allTasksComplete_withCorrectTwoLatchPattern() throws
InterruptedException {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path");
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
+ runBenchmark(executor, TASK_COUNT, "unpooled");
+ }
Review Comment:
The executor returned by `VirtualThreadPool#getExecutor` is an
`ExecutorService` (either `ThreadPoolExecutor` or JDK's
`ThreadPerTaskExecutor`). Not closing it can leave pooled virtual threads and
ThreadLocal state alive beyond the test, which may interfere with other tests
and increases resource usage.
Use try-with-resources to ensure the executor is closed after the benchmark
run.
##########
dubbo-plugin/dubbo-plugin-loom/src/test/java/org/apache/dubbo/common/threadpool/support/loom/VirtualThreadPoolBenchmarkTest.java:
##########
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.common.threadpool.support.loom;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.threadpool.ThreadPool;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static
org.apache.dubbo.common.constants.CommonConstants.THREADS_VIRTUAL_CORE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Benchmark-methodology correctness tests for {@link VirtualThreadPool}.
+ *
+ * <h2>Background (issue #16174)</h2>
+ *
+ * <p>The benchmark in issue #16042 / PR #16055 contained a synchronization
bug: the timing code
+ * awaited {@code countDownLatch1} (the <em>start gate</em>) rather than
{@code countDownLatch2}
+ * (the <em>completion latch</em>). This meant the elapsed time was measured
before all tasks had
+ * finished, making the pooled executor appear faster than the non-pooled one
even though both
+ * modes complete all tasks in roughly the same wall-clock time.
+ *
+ * <h2>Correct two-latch pattern</h2>
+ *
+ * <pre>{@code
+ * CountDownLatch startGate = new CountDownLatch(1); // latch 1 -
release all tasks together
+ * CountDownLatch completionLatch = new CountDownLatch(N); // latch 2 - await
ALL task completions
+ *
+ * for (int i = 0; i < N; i++) {
+ * executor.execute(() -> {
+ * startGate.await(); // wait until everyone is ready
+ * doWork();
+ * completionLatch.countDown(); // signal completion
+ * });
+ * }
+ *
+ * long t0 = System.nanoTime();
+ * startGate.countDown(); // release all tasks simultaneously
+ * completionLatch.await(); // MUST await the COMPLETION latch, NOT
the start gate
+ * long elapsed = System.nanoTime() - t0;
+ * }</pre>
+ *
+ * <p>These tests verify that:
+ * <ol>
+ * <li>Both pooled and non-pooled executors complete <em>all</em> tasks
before the timing window
+ * closes (i.e. they never return early due to the wrong latch being
awaited).
+ * <li>The task completion count exactly equals the number of submitted
tasks in both modes.
+ * </ol>
+ */
+public class VirtualThreadPoolBenchmarkTest {
+
+ private static final int TASK_COUNT = 200;
+
+ /**
+ * Verifies that the non-pooled (default) executor runs all tasks to
completion when measured
+ * with the corrected two-latch pattern.
+ *
+ * <p>The start gate ({@code startGate}) releases all submitted tasks at
the same time so they
+ * compete for the scheduler simultaneously. The completion latch ({@code
completionLatch}) is
+ * decremented by every task when it finishes. Only after
<em>completionLatch</em> reaches zero
+ * do we stop the clock, ensuring the elapsed time reflects actual
end-to-end execution.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void unpooledExecutor_allTasksComplete_withCorrectTwoLatchPattern() throws
InterruptedException {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path");
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
+ runBenchmark(executor, TASK_COUNT, "unpooled");
+ }
+
+ /**
+ * Verifies that the pooled executor also runs all tasks to completion
when measured with the
+ * corrected two-latch pattern.
+ *
+ * <p>Previously, a flawed benchmark awaited the start gate a second time
instead of the
+ * completion latch, returning immediately after releasing tasks. This
test would have caught
+ * that bug because {@code completedTasks} would be far less than {@code
TASK_COUNT}.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void pooledExecutor_allTasksComplete_withCorrectTwoLatchPattern() throws
InterruptedException {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path?" +
THREADS_VIRTUAL_CORE + "="
+ + Runtime.getRuntime().availableProcessors());
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
+ runBenchmark(executor, TASK_COUNT, "pooled");
+ }
+
+ /**
+ * Validates the two-latch timing pattern itself: awaiting the completion
latch means elapsed
+ * time is always >= the time to complete all tasks (trivially verifiable
because the task
+ * counter equals {@code taskCount} when the method returns).
+ *
+ * @param executor the executor under test
+ * @param taskCount number of tasks to submit
+ * @param executorLabel human-readable label for assertion messages
+ */
+ private static void runBenchmark(Executor executor, int taskCount, String
executorLabel)
+ throws InterruptedException {
+ // latch 1: start gate - holds all tasks until released together
(simulates concurrent load)
+ CountDownLatch startGate = new CountDownLatch(1);
+ // latch 2: completion latch - counts down when each task finishes
+ CountDownLatch completionLatch = new CountDownLatch(taskCount);
+
+ AtomicInteger completedTasks = new AtomicInteger(0);
+
+ for (int i = 0; i < taskCount; i++) {
+ executor.execute(() -> {
+ try {
+ // Wait until all tasks are queued and the start gate
opens.
+ startGate.await();
+ // Simulate a minimal unit of work (e.g. an RPC handler
body).
+ simulateWork();
+ completedTasks.incrementAndGet();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ // Always signal completion so the benchmark can drain.
+ // IMPORTANT: this must countDown on completionLatch
(latch 2),
+ // NOT on startGate (latch 1). Decrementing latch 1 here
was the
+ // bug in the original benchmark reported in issue #16174.
+ completionLatch.countDown();
+ }
+ });
+ }
+
+ long startNanos = System.nanoTime();
+ // Release all tasks simultaneously.
+ startGate.countDown();
+ // Correct: await completionLatch (latch 2), NOT startGate (latch 1).
+ // Awaiting startGate here would return immediately (it is already at
0) and make
+ // the measurement appear artificially fast - exactly the flaw in
#16174.
+ completionLatch.await();
Review Comment:
`completionLatch.await()` has no timeout, so if something regresses (e.g.,
tasks never start, rejection, deadlock), this test can hang the whole suite
instead of failing quickly. Adding a bounded await keeps CI reliable while
still validating the two-latch pattern.
##########
dubbo-plugin/dubbo-plugin-loom/src/test/java/org/apache/dubbo/common/threadpool/support/loom/VirtualThreadPoolBenchmarkTest.java:
##########
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.common.threadpool.support.loom;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.threadpool.ThreadPool;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+
+import static
org.apache.dubbo.common.constants.CommonConstants.THREADS_VIRTUAL_CORE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Benchmark-methodology correctness tests for {@link VirtualThreadPool}.
+ *
+ * <h2>Background (issue #16174)</h2>
+ *
+ * <p>The benchmark in issue #16042 / PR #16055 contained a synchronization
bug: the timing code
+ * awaited {@code countDownLatch1} (the <em>start gate</em>) rather than
{@code countDownLatch2}
+ * (the <em>completion latch</em>). This meant the elapsed time was measured
before all tasks had
+ * finished, making the pooled executor appear faster than the non-pooled one
even though both
+ * modes complete all tasks in roughly the same wall-clock time.
+ *
+ * <h2>Correct two-latch pattern</h2>
+ *
+ * <pre>{@code
+ * CountDownLatch startGate = new CountDownLatch(1); // latch 1 -
release all tasks together
+ * CountDownLatch completionLatch = new CountDownLatch(N); // latch 2 - await
ALL task completions
+ *
+ * for (int i = 0; i < N; i++) {
+ * executor.execute(() -> {
+ * startGate.await(); // wait until everyone is ready
+ * doWork();
+ * completionLatch.countDown(); // signal completion
+ * });
+ * }
+ *
+ * long t0 = System.nanoTime();
+ * startGate.countDown(); // release all tasks simultaneously
+ * completionLatch.await(); // MUST await the COMPLETION latch, NOT
the start gate
+ * long elapsed = System.nanoTime() - t0;
+ * }</pre>
+ *
+ * <p>These tests verify that:
+ * <ol>
+ * <li>Both pooled and non-pooled executors complete <em>all</em> tasks
before the timing window
+ * closes (i.e. they never return early due to the wrong latch being
awaited).
+ * <li>The task completion count exactly equals the number of submitted
tasks in both modes.
+ * </ol>
+ */
+public class VirtualThreadPoolBenchmarkTest {
+
+ private static final int TASK_COUNT = 200;
+
+ /**
+ * Verifies that the non-pooled (default) executor runs all tasks to
completion when measured
+ * with the corrected two-latch pattern.
+ *
+ * <p>The start gate ({@code startGate}) releases all submitted tasks at
the same time so they
+ * compete for the scheduler simultaneously. The completion latch ({@code
completionLatch}) is
+ * decremented by every task when it finishes. Only after
<em>completionLatch</em> reaches zero
+ * do we stop the clock, ensuring the elapsed time reflects actual
end-to-end execution.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void unpooledExecutor_allTasksComplete_withCorrectTwoLatchPattern() throws
InterruptedException {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path");
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
+ runBenchmark(executor, TASK_COUNT, "unpooled");
+ }
+
+ /**
+ * Verifies that the pooled executor also runs all tasks to completion
when measured with the
+ * corrected two-latch pattern.
+ *
+ * <p>Previously, a flawed benchmark awaited the start gate a second time
instead of the
+ * completion latch, returning immediately after releasing tasks. This
test would have caught
+ * that bug because {@code completedTasks} would be far less than {@code
TASK_COUNT}.
+ */
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void pooledExecutor_allTasksComplete_withCorrectTwoLatchPattern() throws
InterruptedException {
+ URL url = URL.valueOf("dubbo://10.20.130.230:20880/context/path?" +
THREADS_VIRTUAL_CORE + "="
+ + Runtime.getRuntime().availableProcessors());
+ ThreadPool threadPool = new VirtualThreadPool();
+ Executor executor = threadPool.getExecutor(url);
+
+ runBenchmark(executor, TASK_COUNT, "pooled");
+ }
Review Comment:
The pooled-mode test also creates an `ExecutorService` that should be closed
to avoid leaving a live `ThreadPoolExecutor` (and its core virtual threads)
running after the test completes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]