WangzJi commented on code in PR #8023:
URL: https://github.com/apache/incubator-seata/pull/8023#discussion_r3151744014


##########
pom.xml:
##########
@@ -130,6 +131,16 @@
     </dependencyManagement>
 
     <profiles>
+        <!-- profile: onlyBuildOnJDK21+ -->
+        <profile>
+            <id>JDK21Plus</id>
+            <activation>
+                <jdk>[21,)</jdk>
+            </activation>
+            <modules>
+                <module>threadpool-loom</module>

Review Comment:
   **[Critical] `seata-threadpool-loom` is only added to the reactor build 
modules and to `server/pom.xml`. It is NOT added to `all/pom.xml` (and 
therefore not to `seata-spring-boot-starter`, which depends on `seata-all`).**
   
   Result: an end user pulling `seata-spring-boot-starter` on JDK 21+ never 
gets the virtual-thread provider on the classpath. 
`transport.threadpool=virtual` (and `auto` on JDK 25+) silently falls back to 
platform threads via `ThreadPoolExecutorFactory.loadOptional` returning `null`, 
defeating the PR's stated goal for the most common client entry point.
   
   Suggested fix: add a JDK21+ profile to `all/pom.xml` (and ideally 
`seata-spring-boot-starter/pom.xml`) that pulls in `seata-threadpool-loom`, 
mirroring `server/pom.xml:452-464`.



##########
threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java:
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.seata.common.thread;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Virtual-thread-backed thread pool implementation.
+ */
+public class VirtualThreadPoolExecutor extends ThreadPoolExecutor {
+
+    public VirtualThreadPoolExecutor(
+            String threadPrefix,
+            int corePoolSize,
+            int maximumPoolSize,
+            long keepAliveTime,
+            TimeUnit unit,
+            BlockingQueue<Runnable> workQueue,
+            boolean daemon,
+            RejectedExecutionHandler rejectedHandler) {
+        super(
+                corePoolSize,
+                maximumPoolSize,
+                keepAliveTime,
+                unit,
+                workQueue,
+                VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, 
daemon),
+                rejectedHandler);

Review Comment:
   **[Critical] Implementation contradicts the PR description.**
   
   The PR description says:
   > For regular executors, virtual mode ignores `maximumPoolSize` and 
`keepAliveTime`. Uses `Integer.MAX_VALUE` and `SynchronousQueue` for non-cached 
virtual-thread execution.
   
   But the actual code preserves the caller-provided `corePoolSize`, 
`maximumPoolSize`, `keepAliveTime`, and `workQueue`. So this is functionally a 
bounded platform-style pool with virtual workers — none of the unbounded 
virtual-thread benefits are realized, and 
`VirtualThreadPoolProviderTest.testVirtualThreadPoolExecutorKeepsConfiguredBounds`
 actually locks in the wrong behavior with an assertion.
   
   Please pick one and align all three (PR description / impl / tests):
   1. Implement the design as described (`Integer.MAX_VALUE` + 
`SynchronousQueue`, ignore caller bounds), OR
   2. Update the PR description and Javadoc to clarify that virtual mode still 
respects the caller's bounds.
   
   This was also flagged independently by the Copilot review on 2026-04-10.



##########
threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java:
##########
@@ -0,0 +1,98 @@
+/*
+ * 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.seata.common.thread;
+
+import org.apache.seata.common.ConfigurationKeys;
+import org.apache.seata.common.DefaultValues;
+import org.apache.seata.config.Configuration;
+import org.apache.seata.config.ConfigurationFactory;
+
+import java.util.function.IntSupplier;
+import java.util.function.Supplier;
+
+/**
+ * Runtime helper used to resolve the thread pool mode.
+ */
+final class ThreadPoolRuntimeEnvironment {
+
+    private static final Supplier<String> DEFAULT_THREAD_POOL_TYPE_SUPPLIER =
+            ThreadPoolRuntimeEnvironment::loadConfiguredThreadPoolType;
+    private static final IntSupplier DEFAULT_JDK_FEATURE_SUPPLIER = 
ThreadPoolRuntimeEnvironment::javaFeatureVersion;
+
+    private static volatile Supplier<String> threadPoolTypeSupplier = 
DEFAULT_THREAD_POOL_TYPE_SUPPLIER;
+    private static volatile IntSupplier jdkFeatureSupplier = 
DEFAULT_JDK_FEATURE_SUPPLIER;
+
+    private ThreadPoolRuntimeEnvironment() {}
+
+    static ThreadPoolType resolveThreadPoolType() {
+        ThreadPoolType configuredType = 
ThreadPoolType.from(threadPoolTypeSupplier.get());
+        if (configuredType == ThreadPoolType.PLATFORM) {
+            return ThreadPoolType.PLATFORM;
+        }
+        int jdkFeature = jdkFeatureSupplier.getAsInt();
+        if (configuredType == ThreadPoolType.VIRTUAL) {
+            return jdkFeature >= 21 ? ThreadPoolType.VIRTUAL : 
ThreadPoolType.PLATFORM;

Review Comment:
   **[Critical] Silent fallback when user explicitly requests virtual but JDK 
is too old.**
   
   This class has no `Logger` at all. When an operator sets 
`transport.threadpool=virtual` on JDK <21, this line silently returns 
`PLATFORM` with zero log output. Capacity-planning assumptions become wrong 
with no breadcrumb to discover the issue.
   
   The same issue applies to `ThreadPoolExecutorFactory.loadOptional` (line 
174), which swallows `EnhancedServiceNotFoundException` without logging — so 
when `transport.threadpool=virtual` is set on JDK 21+ but 
`seata-threadpool-loom` is missing from the classpath (very likely on the 
client side per the comment on `pom.xml:141`), there is again no log.
   
   Suggested fix: add a `Logger`, and emit a one-shot WARN on the first 
downgrade for each of:
   - `virtual` requested on JDK <21 (this line)
   - `virtual` requested but provider missing 
(`ThreadPoolExecutorFactory.loadOptional`)
   - unknown configured value coerced to `AUTO` (`ThreadPoolType.from`)
   
   Use an `AtomicBoolean` to dedupe the warning.



##########
threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java:
##########
@@ -0,0 +1,31 @@
+/*
+ * 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.seata.common.thread;
+
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * Virtual-thread-backed scheduled thread pool implementation.
+ */
+public class VirtualScheduledThreadPoolExecutor extends 
ScheduledThreadPoolExecutor {
+
+    public VirtualScheduledThreadPoolExecutor(
+            String threadPrefix, int corePoolSize, boolean daemon, 
RejectedExecutionHandler rejectedHandler) {
+        super(corePoolSize, 
VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, daemon), 
rejectedHandler);

Review Comment:
   **[Critical] `ScheduledThreadPoolExecutor` + virtual threads is an 
anti-pattern that re-introduces the very problem this PR aims to solve (see 
issue #6724 "Prevent virtual thread pinned").**
   
   `ScheduledThreadPoolExecutor` keeps `corePoolSize` worker threads parked 
indefinitely on `DelayedWorkQueue.take()` (a native park inside a 
`ReentrantLock` region). Virtual threads parking there will pin their carrier 
threads for the lifetime of the pool, so:
   1. The expected virtual-thread benefits (lightweight, scalable concurrency) 
are zero.
   2. Native code paths in registry clients (Nacos/etcd/JNI) can pin carriers 
indefinitely.
   3. Netty-adjacent paths lose `FastThreadLocalThread` and fall back to the 
slower `InternalThreadLocalMap`.
   
   Impact is wide: `RegistryHeartBeats`, 
`DefaultCoordinator.{retryRollbacking,asyncCommitting,xaTwoPhaseTimeoutChecker}`,
 `CommonFenceConfig`, etc. — 12+ migrated call sites all go through this path.
   
   Recommended: have `VirtualThreadPoolProvider.newScheduledThreadPoolExecutor` 
delegate to the platform implementation (or document explicitly that scheduled 
executors stay on platform threads even when virtual mode is selected).



##########
seata-spring-boot-starter/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataSagaAutoConfiguration.java:
##########
@@ -118,20 +118,13 @@ public ThreadPoolExecutor sagaAsyncThreadPoolExecutor(
                 SagaAsyncThreadPoolProperties properties,
                 @Qualifier(SAGA_REJECTED_EXECUTION_HANDLER_BEAN_NAME)
                         RejectedExecutionHandler rejectedExecutionHandler) {
-            ThreadPoolExecutorFactoryBean threadFactory = new 
ThreadPoolExecutorFactoryBean();
-            
threadFactory.setBeanName("sagaStateMachineThreadPoolExecutorFactory");
-            threadFactory.setThreadNamePrefix("sagaAsyncExecute-");
-            threadFactory.setCorePoolSize(properties.getCorePoolSize());
-            threadFactory.setMaxPoolSize(properties.getMaxPoolSize());
-            threadFactory.setKeepAliveSeconds(properties.getKeepAliveTime());
-
-            return new ThreadPoolExecutor(
+            return ThreadPoolExecutorFactory.newThreadPoolExecutor(
+                    "sagaAsyncExecute",
                     properties.getCorePoolSize(),
                     properties.getMaxPoolSize(),
                     properties.getKeepAliveTime(),
                     TimeUnit.SECONDS,
                     new LinkedBlockingQueue<>(),
-                    threadFactory,
                     rejectedExecutionHandler);

Review Comment:
   **[Critical] Behavioral regression: Saga async threads change from 
non-daemon to daemon.**
   
   The previous code used Spring's `ThreadPoolExecutorFactoryBean`, whose 
default `ThreadFactory` (`CustomizableThreadFactory`) creates **non-daemon** 
threads unless explicitly set otherwise. 
`ThreadPoolExecutorFactory.newThreadPoolExecutor(...)` (this overload, no 
`daemon` arg) defaults to **`daemon=true`**.
   
   Impact: in-flight Saga state-machine async tasks no longer block JVM 
shutdown. On a graceful shutdown, running saga branches can be force-terminated 
mid-execution, potentially leaving the saga in an inconsistent state.
   
   Suggested fix: use the overload that takes an explicit `daemon=false` to 
preserve prior semantics:
   
   ```java
   return ThreadPoolExecutorFactory.newThreadPoolExecutor(
           "sagaAsyncExecute",
           properties.getCorePoolSize(),
           properties.getMaxPoolSize(),
           properties.getKeepAliveTime(),
           TimeUnit.SECONDS,
           new LinkedBlockingQueue<>(),
           false,  // daemon: preserve prior non-daemon behavior so shutdown 
waits for in-flight saga branches
           rejectedExecutionHandler);
   ```
   
   This was also flagged in an existing PR inline review comment.



-- 
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]

Reply via email to