feat: 增强多实例分布式部署兼容
- 增加定时任务分布式锁并覆盖 chatlog、文档导入和 Agent HITL 过期扫描 - 增强 Redis MQ 多实例 consumer 标识、pending reclaim 和单条处理能力 - 增加文档导入状态 Redis 广播和 Agent HITL 跨节点路由确认
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package tech.easyflow.common.cache;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
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.data.redis.core.ValueOperations;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* {@link DistributedScheduledLockAspect} 回归测试。
|
||||
*/
|
||||
public class DistributedScheduledLockAspectTest {
|
||||
|
||||
/**
|
||||
* 验证未抢到调度锁时跳过原方法。
|
||||
*
|
||||
* @throws Throwable 切面执行异常
|
||||
*/
|
||||
@Test
|
||||
public void aroundShouldSkipTaskWhenLockIsHeld() throws Throwable {
|
||||
RedisLockExecutor executor = createExecutor(false);
|
||||
DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
|
||||
AtomicInteger proceedCount = new AtomicInteger();
|
||||
|
||||
Object result = aspect.around(
|
||||
mockJoinPoint(proceedCount),
|
||||
annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
|
||||
);
|
||||
|
||||
Assert.assertNull(result);
|
||||
Assert.assertEquals(0, proceedCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证抢到调度锁时执行原方法并释放锁。
|
||||
*
|
||||
* @throws Throwable 切面执行异常
|
||||
*/
|
||||
@Test
|
||||
public void aroundShouldProceedAndReleaseWhenLockAcquired() throws Throwable {
|
||||
RedisLockExecutor executor = createExecutor(true);
|
||||
DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
|
||||
AtomicInteger proceedCount = new AtomicInteger();
|
||||
|
||||
Object result = aspect.around(
|
||||
mockJoinPoint(proceedCount),
|
||||
annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
|
||||
);
|
||||
|
||||
Assert.assertEquals("ok", result);
|
||||
Assert.assertEquals(1, proceedCount.get());
|
||||
}
|
||||
|
||||
@DistributedScheduledLock(key = "easyflow:test:scheduled", leaseSeconds = 30L)
|
||||
private void lockedTask() {
|
||||
}
|
||||
|
||||
private Method annotatedMethod(String methodName) throws NoSuchMethodException {
|
||||
Method method = DistributedScheduledLockAspectTest.class.getDeclaredMethod(methodName);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
|
||||
private ProceedingJoinPoint mockJoinPoint(AtomicInteger proceedCount) throws Throwable {
|
||||
ProceedingJoinPoint joinPoint = Mockito.mock(ProceedingJoinPoint.class);
|
||||
Signature signature = Mockito.mock(Signature.class);
|
||||
Mockito.when(signature.toShortString()).thenReturn("lockedTask()");
|
||||
Mockito.when(joinPoint.getSignature()).thenReturn(signature);
|
||||
Mockito.when(joinPoint.proceed()).thenAnswer(invocation -> {
|
||||
proceedCount.incrementAndGet();
|
||||
return "ok";
|
||||
});
|
||||
return joinPoint;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private RedisLockExecutor createExecutor(boolean acquired) throws Exception {
|
||||
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
|
||||
Mockito.when(valueOperations.setIfAbsent(
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.any(Duration.class)
|
||||
)).thenReturn(acquired);
|
||||
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
Mockito.when(redisTemplate.execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.<List<String>>any(),
|
||||
ArgumentMatchers.<Object[]>any()
|
||||
)).thenReturn(1L);
|
||||
|
||||
RedisLockExecutor executor = new RedisLockExecutor();
|
||||
Field field = RedisLockExecutor.class.getDeclaredField("stringRedisTemplate");
|
||||
field.setAccessible(true);
|
||||
field.set(executor, redisTemplate);
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package tech.easyflow.common.cache;
|
||||
|
||||
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.data.redis.core.ValueOperations;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link RedisLockExecutor} 回归测试。
|
||||
*/
|
||||
public class RedisLockExecutorTest {
|
||||
|
||||
/**
|
||||
* 验证锁被占用时返回 null,便于调度任务跳过本轮执行。
|
||||
*
|
||||
* @throws Exception 反射注入异常
|
||||
*/
|
||||
@Test
|
||||
public void tryAcquireShouldReturnNullWhenLockIsHeld() throws Exception {
|
||||
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = mockValueOperations(false);
|
||||
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
|
||||
RedisLockExecutor executor = new RedisLockExecutor();
|
||||
setRedisTemplate(executor, redisTemplate);
|
||||
|
||||
RedisLockExecutor.LockHandle handle = executor.tryAcquire(
|
||||
"easyflow:test:lock",
|
||||
Duration.ZERO,
|
||||
Duration.ofSeconds(30)
|
||||
);
|
||||
|
||||
Assert.assertNull(handle);
|
||||
Mockito.verify(valueOperations).setIfAbsent(
|
||||
ArgumentMatchers.eq("easyflow:test:lock"),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.eq(Duration.ofSeconds(30))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证锁获取成功后释放会执行 owner token 校验脚本。
|
||||
*
|
||||
* @throws Exception 反射注入异常
|
||||
*/
|
||||
@Test
|
||||
public void acquiredHandleShouldReleaseLockWithOwnerToken() throws Exception {
|
||||
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = mockValueOperations(true);
|
||||
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
Mockito.when(redisTemplate.execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.<List<String>>any(),
|
||||
ArgumentMatchers.<Object[]>any()
|
||||
)).thenReturn(1L);
|
||||
|
||||
RedisLockExecutor executor = new RedisLockExecutor();
|
||||
setRedisTemplate(executor, redisTemplate);
|
||||
|
||||
RedisLockExecutor.LockHandle handle = executor.tryAcquire(
|
||||
"easyflow:test:lock",
|
||||
Duration.ZERO,
|
||||
Duration.ofSeconds(30)
|
||||
);
|
||||
|
||||
Assert.assertNotNull(handle);
|
||||
handle.release();
|
||||
Mockito.verify(redisTemplate).execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.eq(List.of("easyflow:test:lock")),
|
||||
ArgumentMatchers.<Object[]>any()
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ValueOperations<String, String> mockValueOperations(boolean acquired) {
|
||||
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
|
||||
Mockito.when(valueOperations.setIfAbsent(
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.any(Duration.class)
|
||||
)).thenReturn(acquired);
|
||||
return valueOperations;
|
||||
}
|
||||
|
||||
private void setRedisTemplate(RedisLockExecutor executor, StringRedisTemplate redisTemplate) throws Exception {
|
||||
Field field = RedisLockExecutor.class.getDeclaredField("stringRedisTemplate");
|
||||
field.setAccessible(true);
|
||||
field.set(executor, redisTemplate);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user