e2607ab723
Build Backend JAR / build (push) Has been cancelled
- similarasin/appearancepatent 模块全部 Coze 工作流调用改走 direct-LLM(已确认唯一运行路径) - 共享组件改名:CozeTaskQueueGate→TaskQueueGate、CozeGroupResultPropagator→GroupResultPropagator - 状态机改名:biz_task_scope_state 的 coze_* 列→llm_*、stateJson coze 键→llm(V100 迁移已应用生产) - 删除 biz_coze_credential 表、CozeCredential* 类、SimilarAsinCozeClient、AppearancePatentCozeClient→LlmClient - 前端 brand 页 Coze 文案→LLM;Python 后端删除 cozepy 依赖与死配置 - 修复 TaskResultFileJobWorker 启动失败:TaskFileJobConfig 注册 ResultFileJobHandlerRegistry 与 13 个 handler bean(含 validateCoverage 启动校验)
250 lines
12 KiB
Java
250 lines
12 KiB
Java
package com.nanri.aiimage.config;
|
||
|
||
import org.junit.jupiter.api.AfterEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.core.task.TaskExecutor;
|
||
import org.springframework.core.task.TaskRejectedException;
|
||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.Date;
|
||
import java.util.List;
|
||
import java.util.concurrent.CountDownLatch;
|
||
import java.util.concurrent.ExecutorService;
|
||
import java.util.concurrent.TimeUnit;
|
||
import java.util.concurrent.atomic.AtomicBoolean;
|
||
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
|
||
/**
|
||
* Task 74:隔离调度线程池、文件作业线程池和外部任务队列/图片执行池。
|
||
* 三个执行池各自独立配置、独立命名、容量互不影响:调度池
|
||
* (aiimage.scheduling.pool-size,默认 4)与文件作业派发池
|
||
* (aiimage.result-file-job.*,默认 2 线程/队列 200)互不共享线程;
|
||
* 外部任务队列池以虚拟线程 + 信号量限流(默认 12)。容量非法值统一
|
||
* 钳制到最小值;任务失败后信号量名额与调度槽位必须释放,任一池打满
|
||
* 不影响其他池。
|
||
*/
|
||
class ThreadPoolIsolationConfigTest {
|
||
|
||
private final List<AutoCloseable> closeables = new ArrayList<>();
|
||
|
||
@AfterEach
|
||
void tearDown() throws Exception {
|
||
for (AutoCloseable closeable : closeables) {
|
||
closeable.close();
|
||
}
|
||
}
|
||
|
||
private ThreadPoolTaskScheduler newScheduler(int poolSize) {
|
||
ThreadPoolTaskScheduler scheduler =
|
||
(ThreadPoolTaskScheduler) new SchedulingConfig().taskScheduler(poolSize);
|
||
closeables.add(scheduler::destroy);
|
||
return scheduler;
|
||
}
|
||
|
||
private ThreadPoolTaskExecutor newDispatch(int poolSize, int queueCapacity) {
|
||
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) new TaskFileJobConfig()
|
||
.taskFileJobDispatchExecutor(poolSize, queueCapacity);
|
||
closeables.add(executor::destroy);
|
||
return executor;
|
||
}
|
||
|
||
private ExecutorService newTaskQueueVirtual() {
|
||
ExecutorService executor = new TaskFileJobConfig().taskQueueVirtualThreadExecutor();
|
||
closeables.add(() -> executor.shutdownNow());
|
||
return executor;
|
||
}
|
||
|
||
private TaskExecutor newTaskQueue(ExecutorService virtualExecutor, int maxConcurrent) {
|
||
return new TaskFileJobConfig().taskQueueExecutor(virtualExecutor, maxConcurrent, 4, null);
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_normal_default_path() throws Exception {
|
||
// 默认路径:三池按默认容量初始化,正常提交一个任务即可执行。
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(4);
|
||
assertEquals(4, scheduler.getScheduledThreadPoolExecutor().getCorePoolSize(), "调度池默认 4 线程");
|
||
assertTrue(scheduler.getThreadNamePrefix().startsWith("aiimage-scheduling-"),
|
||
"调度池线程名独立前缀");
|
||
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(2, 200);
|
||
assertEquals(2, dispatch.getCorePoolSize());
|
||
assertEquals(2, dispatch.getMaxPoolSize(), "文件作业池 core=max,不随压力扩张");
|
||
assertEquals(200, dispatch.getQueueCapacity());
|
||
|
||
ExecutorService taskQueueVirtual = newTaskQueueVirtual();
|
||
TaskExecutor taskQueue = newTaskQueue(taskQueueVirtual, 12);
|
||
AtomicBoolean ran = new AtomicBoolean(false);
|
||
CountDownLatch done = new CountDownLatch(1);
|
||
taskQueue.execute(() -> {
|
||
ran.set(true);
|
||
done.countDown();
|
||
});
|
||
assertTrue(done.await(5, TimeUnit.SECONDS), "任务队列池默认限流 12,正常提交即执行");
|
||
assertTrue(ran.get());
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_normal_multiple_items() throws Exception {
|
||
// 批量场景:三池同时按各自容量配置,各跑一个任务互不阻塞;
|
||
// 线程名前缀互不相同,线程转储可识别归属池。
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(6);
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(3, 500);
|
||
ExecutorService taskQueueVirtual = newTaskQueueVirtual();
|
||
TaskExecutor taskQueue = newTaskQueue(taskQueueVirtual, 8);
|
||
assertEquals(6, scheduler.getScheduledThreadPoolExecutor().getCorePoolSize());
|
||
assertEquals(3, dispatch.getCorePoolSize());
|
||
assertEquals(500, dispatch.getQueueCapacity());
|
||
|
||
CountDownLatch all = new CountDownLatch(3);
|
||
dispatch.execute(all::countDown);
|
||
scheduler.schedule((Runnable) all::countDown, new Date(System.currentTimeMillis() + 50));
|
||
taskQueue.execute(all::countDown);
|
||
assertTrue(all.await(5, TimeUnit.SECONDS), "三个池同时执行互不阻塞");
|
||
|
||
assertNotEquals(scheduler.getThreadNamePrefix(), dispatch.getThreadNamePrefix(),
|
||
"调度池与文件作业池线程名前缀隔离");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_normal_repeated_operation_is_idempotent() throws Exception {
|
||
// 幂等:同一任务重复提交各自独立执行一次,不合并不丢失,
|
||
// 线程数不因重复提交而扩张。
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(2, 100);
|
||
AtomicInteger count = new AtomicInteger();
|
||
CountDownLatch all = new CountDownLatch(3);
|
||
Runnable task = () -> {
|
||
count.incrementAndGet();
|
||
all.countDown();
|
||
};
|
||
dispatch.execute(task);
|
||
dispatch.execute(task);
|
||
dispatch.execute(task);
|
||
assertTrue(all.await(5, TimeUnit.SECONDS), "同一任务重复提交各自执行一次");
|
||
assertEquals(3, count.get());
|
||
assertEquals(2, dispatch.getMaxPoolSize(), "重复提交不扩张线程数");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_boundary_empty_input() throws Exception {
|
||
// 空输入:容量配置为 0 时统一钳制到最小值,池仍可用。
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(0);
|
||
assertEquals(1, scheduler.getScheduledThreadPoolExecutor().getCorePoolSize(), "调度池 0 钳制到 1");
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(0, 0);
|
||
assertEquals(1, dispatch.getCorePoolSize(), "文件作业池 0 钳制到 1");
|
||
assertEquals(10, dispatch.getQueueCapacity(), "队列 0 钳制到 10");
|
||
ExecutorService taskQueueVirtual = newTaskQueueVirtual();
|
||
TaskExecutor taskQueue = newTaskQueue(taskQueueVirtual, 0);
|
||
CountDownLatch done = new CountDownLatch(1);
|
||
taskQueue.execute(done::countDown);
|
||
assertTrue(done.await(5, TimeUnit.SECONDS), "任务队列池 0 钳制到 1 后仍可执行");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_boundary_single_item() throws Exception {
|
||
// 单元素:单线程池单任务直接完成,不依赖批量路径。
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(1, 10);
|
||
CountDownLatch done = new CountDownLatch(1);
|
||
dispatch.execute(done::countDown);
|
||
assertTrue(done.await(5, TimeUnit.SECONDS), "单线程池单任务直接完成");
|
||
assertEquals(1, dispatch.getCorePoolSize());
|
||
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(1);
|
||
CountDownLatch scheduled = new CountDownLatch(1);
|
||
scheduler.schedule((Runnable) scheduled::countDown, new Date(System.currentTimeMillis() + 30));
|
||
assertTrue(scheduled.await(5, TimeUnit.SECONDS), "单线程调度池单次调度完成");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_boundary_limit_and_overflow() throws Exception {
|
||
// 上限/超限:文件作业池 2 线程 + 队列 10,容量为 12;
|
||
// 第 13 个提交被拒绝(TaskRejectedException),不发生无界堆积。
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(2, 10);
|
||
CountDownLatch blockersRunning = new CountDownLatch(2);
|
||
CountDownLatch releaseBlockers = new CountDownLatch(1);
|
||
CountDownLatch allDone = new CountDownLatch(12);
|
||
for (int i = 0; i < 2; i++) {
|
||
dispatch.execute(() -> {
|
||
blockersRunning.countDown();
|
||
try {
|
||
releaseBlockers.await(10, TimeUnit.SECONDS);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
}
|
||
allDone.countDown();
|
||
});
|
||
}
|
||
assertTrue(blockersRunning.await(5, TimeUnit.SECONDS), "两个运行线程占位");
|
||
for (int i = 0; i < 10; i++) {
|
||
dispatch.execute(allDone::countDown);
|
||
}
|
||
assertThrows(TaskRejectedException.class, () -> dispatch.execute(allDone::countDown),
|
||
"队列满后拒绝新提交,不发生无界堆积");
|
||
releaseBlockers.countDown();
|
||
assertTrue(allDone.await(10, TimeUnit.SECONDS), "已受理的 12 个任务全部完成");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_invalid_input_rejected() throws Exception {
|
||
// 非法参数:负容量统一钳制到最小值(不崩溃、行为确定);
|
||
// null 任务直接被拒绝。
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(-1);
|
||
assertEquals(1, scheduler.getScheduledThreadPoolExecutor().getCorePoolSize(), "负值钳制到最小值");
|
||
ThreadPoolTaskExecutor dispatch = newDispatch(-2, -5);
|
||
assertEquals(1, dispatch.getCorePoolSize());
|
||
assertEquals(10, dispatch.getQueueCapacity());
|
||
|
||
ExecutorService taskQueueVirtual = newTaskQueueVirtual();
|
||
TaskExecutor taskQueue = newTaskQueue(taskQueueVirtual, -3);
|
||
assertThrows(IllegalArgumentException.class, () -> taskQueue.execute(null), "null 任务被拒绝");
|
||
CountDownLatch done = new CountDownLatch(1);
|
||
taskQueue.execute(done::countDown);
|
||
assertTrue(done.await(5, TimeUnit.SECONDS), "非法配置钳制后池仍可用");
|
||
}
|
||
|
||
@Test
|
||
void test_task_074_image_dispatch_job_dependency_failure_releases_resources() throws Exception {
|
||
// 依赖失败:任务队列任务抛异常后信号量名额必须释放(后续任务可执行);
|
||
// 调度任务异常被 error handler 吞掉,调度器继续可用。
|
||
ExecutorService taskQueueVirtual = newTaskQueueVirtual();
|
||
TaskExecutor taskQueue = newTaskQueue(taskQueueVirtual, 2);
|
||
CountDownLatch blockerHeld = new CountDownLatch(1);
|
||
CountDownLatch releaseBlocker = new CountDownLatch(1);
|
||
taskQueue.execute(() -> {
|
||
blockerHeld.countDown();
|
||
try {
|
||
releaseBlocker.await(10, TimeUnit.SECONDS);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
}
|
||
});
|
||
assertTrue(blockerHeld.await(5, TimeUnit.SECONDS), "任务 1 占住一个信号量名额");
|
||
taskQueue.execute(() -> {
|
||
throw new IllegalStateException("task queue down");
|
||
});
|
||
CountDownLatch afterFailure = new CountDownLatch(1);
|
||
taskQueue.execute(afterFailure::countDown);
|
||
assertTrue(afterFailure.await(5, TimeUnit.SECONDS), "失败任务释放名额,后续任务可执行");
|
||
releaseBlocker.countDown();
|
||
|
||
ThreadPoolTaskScheduler scheduler = newScheduler(2);
|
||
AtomicBoolean secondRan = new AtomicBoolean(false);
|
||
CountDownLatch secondDone = new CountDownLatch(1);
|
||
scheduler.schedule(() -> {
|
||
throw new IllegalStateException("scheduled boom");
|
||
}, new Date(System.currentTimeMillis() + 30));
|
||
scheduler.schedule(() -> {
|
||
secondRan.set(true);
|
||
secondDone.countDown();
|
||
}, new Date(System.currentTimeMillis() + 60));
|
||
assertTrue(secondDone.await(5, TimeUnit.SECONDS), "调度任务异常被 error handler 吞掉,调度器继续可用");
|
||
assertTrue(secondRan.get());
|
||
}
|
||
}
|