feat: 切换定时任务至分布式调度底座
- 以执行账本和有界 Worker 承载重负载任务与故障接管 - 接入统一调度 Starter 并增加 MySQL 迁移、指标和回归测试
This commit is contained in:
@@ -11,6 +11,9 @@ import org.springframework.data.redis.core.script.RedisScript;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* {@link RedisLockExecutor} 回归测试。
|
||||
@@ -147,6 +150,96 @@ public class RedisLockExecutorTest {
|
||||
String.valueOf(Duration.ofDays(4).toMillis())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renewingLockShouldRenewBeforeLongRunningCommandCompletes() throws Exception {
|
||||
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = mockValueOperations(true);
|
||||
CountDownLatch renewed = new CountDownLatch(1);
|
||||
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
Mockito.when(redisTemplate.execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.<List<String>>any(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString()
|
||||
)).thenAnswer(invocation -> {
|
||||
renewed.countDown();
|
||||
return 1L;
|
||||
});
|
||||
|
||||
RedisLockExecutor executor = new RedisLockExecutor();
|
||||
setRedisTemplate(executor, redisTemplate);
|
||||
try {
|
||||
executor.executeWithRenewingLock(
|
||||
"easyflow:test:renewing-lock",
|
||||
Duration.ZERO,
|
||||
Duration.ofMillis(60),
|
||||
() -> {
|
||||
try {
|
||||
Assert.assertTrue(renewed.await(1, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError("等待锁续租时被中断", exception);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
executor.shutdownLockRenewalExecutor();
|
||||
}
|
||||
|
||||
Mockito.verify(redisTemplate, Mockito.atLeastOnce()).execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.eq(List.of("easyflow:test:renewing-lock")),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.eq("60"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renewingLockMustNotReturnSuccessAfterRenewalThrows() throws Exception {
|
||||
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = mockValueOperations(true);
|
||||
CountDownLatch renewalAttempted = new CountDownLatch(1);
|
||||
AtomicInteger scriptCalls = new AtomicInteger();
|
||||
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
Mockito.when(redisTemplate.execute(
|
||||
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||
ArgumentMatchers.<List<String>>any(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString()
|
||||
)).thenAnswer(invocation -> {
|
||||
if (scriptCalls.incrementAndGet() == 1) {
|
||||
renewalAttempted.countDown();
|
||||
throw new IllegalStateException("redis unavailable");
|
||||
}
|
||||
return 1L;
|
||||
});
|
||||
|
||||
RedisLockExecutor executor = new RedisLockExecutor();
|
||||
setRedisTemplate(executor, redisTemplate);
|
||||
try {
|
||||
try {
|
||||
executor.executeWithRenewingLock(
|
||||
"easyflow:test:renewal-failure",
|
||||
Duration.ZERO,
|
||||
Duration.ofMillis(60),
|
||||
() -> {
|
||||
try {
|
||||
Assert.assertTrue(renewalAttempted.await(1, TimeUnit.SECONDS));
|
||||
// 等待续租线程把失败结果发布到调用线程;业务任务与续租
|
||||
// 同时完成时,锁仍处于原租约内且 callback 已结束。
|
||||
Thread.sleep(50L);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
});
|
||||
Assert.fail("续租异常后不应返回成功");
|
||||
} catch (IllegalStateException exception) {
|
||||
Assert.assertTrue(exception.getMessage().contains("分布式锁已丢失"));
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownLockRenewalExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ValueOperations<String, String> mockValueOperations(boolean acquired) {
|
||||
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
|
||||
|
||||
Reference in New Issue
Block a user