Browse Source

fix(backend): AsyncConfig taskExecutor/taskScheduler 注入守护线程工厂(S-cfc-140 langgraph.pytest.exitHang)

LLM/LangGraph 异步调用面使用 Spring 默认非守护线程工厂,任务挂起(LangGraph 服务
unresponsive)时非守护线程不退出导致进程无法 exit,被外部 timeout 强杀 EXIT=124
(B-135-2 同源)。为 ThreadPoolTaskExecutor/ThreadPoolTaskScheduler 注入带
namePrefix 的守护线程工厂,JVM/测试进程在调用面挂起时仍可正常退出;
AiGateway 有界 connect/read 超时已配置(LangGraphExitHangContractTest 绿锚点),
本提交仅改动线程工厂,不改超时逻辑。

验证:mvn -q -Dtest=LangGraphExitHangContractTest test 4/4 通过(原 2 失败转绿),
mvn -q -DskipTests compile 通过。

S-cfc-140
Sisyphus Agent 4 days ago
parent
commit
a1ead33a1
1 changed files with 22 additions and 0 deletions
  1. 22 0
      cfc-backend/src/main/java/com/etotem/cfc/config/AsyncConfig.java

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/AsyncConfig.java

@@ -7,11 +7,31 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
 
 
 import java.util.concurrent.RejectedExecutionHandler;
 import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 @Configuration
 @Configuration
 public class AsyncConfig {
 public class AsyncConfig {
 
 
+    /**
+     * 守护线程工厂:LLM/LangGraph 异步调用面(taskExecutor/taskScheduler)使用非守护线程时,
+     * 任务挂起(如 LangGraph 服务 unresponsive)会导致进程无法退出,被外部 timeout 强杀(B-135-2
+     * EXIT=124 同源镜像契约 langgraph.pytest.exitHang)。注入守护线程工厂后,JVM/测试进程在任务挂起时
+     * 仍可正常退出。
+     */
+    private static ThreadFactory daemonThreadFactory(final String namePrefix) {
+        final AtomicInteger counter = new AtomicInteger(0);
+        return new ThreadFactory() {
+            @Override
+            public Thread newThread(Runnable r) {
+                Thread t = new Thread(r, namePrefix + counter.incrementAndGet());
+                t.setDaemon(true);
+                return t;
+            }
+        };
+    }
+
     @Bean(name = "taskExecutor")
     @Bean(name = "taskExecutor")
     public ThreadPoolTaskExecutor taskExecutor() {
     public ThreadPoolTaskExecutor taskExecutor() {
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@@ -20,6 +40,7 @@ public class AsyncConfig {
         executor.setQueueCapacity(100);
         executor.setQueueCapacity(100);
         executor.setKeepAliveSeconds(60);
         executor.setKeepAliveSeconds(60);
         executor.setThreadNamePrefix("cfc-async-");
         executor.setThreadNamePrefix("cfc-async-");
+        executor.setThreadFactory(daemonThreadFactory("cfc-async-"));
         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
         executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
         executor.initialize();
         executor.initialize();
         return executor;
         return executor;
@@ -30,6 +51,7 @@ public class AsyncConfig {
         ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
         ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
         scheduler.setPoolSize(4);
         scheduler.setPoolSize(4);
         scheduler.setThreadNamePrefix("cfc-scheduler-");
         scheduler.setThreadNamePrefix("cfc-scheduler-");
+        scheduler.setThreadFactory(daemonThreadFactory("cfc-scheduler-"));
         scheduler.initialize();
         scheduler.initialize();
         return scheduler;
         return scheduler;
     }
     }