feat: 增强多实例分布式部署兼容

- 增加定时任务分布式锁并覆盖 chatlog、文档导入和 Agent HITL 过期扫描

- 增强 Redis MQ 多实例 consumer 标识、pending reclaim 和单条处理能力

- 增加文档导入状态 Redis 广播和 Agent HITL 跨节点路由确认
This commit is contained in:
2026-05-29 18:27:46 +08:00
parent cc3bb9cff0
commit 0f4d10c43c
39 changed files with 2703 additions and 17 deletions

View File

@@ -0,0 +1,97 @@
package tech.easyflow.ai.documentimport.task;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import tech.easyflow.ai.entity.Document;
import tech.easyflow.ai.mapper.DocumentMapper;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* {@link DocumentImportTaskStatusStreamService} 回归测试。
*/
public class DocumentImportTaskStatusStreamServiceTest {
/**
* 验证文档状态变更会向 Redis 广播文档 ID。
*
* @throws Exception 反射注入异常
*/
@Test
public void publishAfterCommitShouldBroadcastDocumentId() throws Exception {
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
DocumentImportTaskStatusStreamService service = new DocumentImportTaskStatusStreamService();
setField(service, "documentMapper", mockDocumentMapper());
setField(service, "sseThreadPool", directExecutor());
setField(service, "stringRedisTemplate", redisTemplate);
setField(service, "statusBroadcastProperties", statusBroadcastProperties());
service.publishAfterCommit(BigInteger.valueOf(101));
Mockito.verify(redisTemplate).convertAndSend("easyflow:document-import:test-status", "101");
}
/**
* 验证收到 Redis 广播后会重新查询文档状态。
*
* @throws Exception 反射注入异常
*/
@Test
public void publishLocalShouldReloadDocumentStatus() throws Exception {
AtomicReference<BigInteger> selectedIdRef = new AtomicReference<BigInteger>();
DocumentImportTaskStatusStreamService service = new DocumentImportTaskStatusStreamService();
setField(service, "documentMapper", mockDocumentMapper(selectedIdRef));
setField(service, "sseThreadPool", directExecutor());
setField(service, "stringRedisTemplate", Mockito.mock(StringRedisTemplate.class));
setField(service, "statusBroadcastProperties", statusBroadcastProperties());
service.publishLocal(BigInteger.valueOf(202));
Assert.assertEquals(BigInteger.valueOf(202), selectedIdRef.get());
}
private DocumentImportStatusBroadcastProperties statusBroadcastProperties() {
DocumentImportStatusBroadcastProperties properties = new DocumentImportStatusBroadcastProperties();
properties.setStatusBroadcastChannel("easyflow:document-import:test-status");
return properties;
}
private DocumentMapper mockDocumentMapper() {
return mockDocumentMapper(new AtomicReference<BigInteger>());
}
private DocumentMapper mockDocumentMapper(AtomicReference<BigInteger> selectedIdRef) {
DocumentMapper mapper = Mockito.mock(DocumentMapper.class);
Mockito.when(mapper.selectOneById(ArgumentMatchers.any())).thenAnswer(invocation -> {
Object id = invocation.getArgument(0);
selectedIdRef.set((BigInteger) id);
Document document = new Document();
document.setId((BigInteger) id);
document.setCollectionId(BigInteger.valueOf(1));
return document;
});
return mapper;
}
private ThreadPoolTaskExecutor directExecutor() {
ThreadPoolTaskExecutor executor = Mockito.mock(ThreadPoolTaskExecutor.class);
Mockito.doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(executor).execute(ArgumentMatchers.any(Runnable.class));
return executor;
}
private void setField(Object target, String fieldName, Object value) throws Exception {
Field field = DocumentImportTaskStatusStreamService.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}