test(cache): 补关键路径覆盖(loader 异常/double-check/泛型重载/sliding 回源/降级)
补齐 review 指出的覆盖缺口,均为既有正确行为的回归保护:loader 抛异常时锁 finally 释放 + 异常透传 + 不写缓存、抢锁后 double-check 命中跳过回源、getOrLoad 的 TypeReference 泛型重载往返、sliding 开启时回源新写入不重复续期、等待超时降级不写缓存。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.njcn.middle.cache.template;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
import com.njcn.middle.cache.autoconfig.RedisCacheProperties;
|
||||
import com.njcn.middle.cache.constant.CacheConstant;
|
||||
import com.njcn.middle.cache.support.CacheKeyBuilder;
|
||||
@@ -17,6 +18,8 @@ import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
@@ -25,6 +28,7 @@ import java.util.function.Supplier;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -349,4 +353,75 @@ class RedisCacheEnhanceTemplateTest {
|
||||
verify(valueOps).set("cache:user:1", CacheConstant.NULL_SENTINEL,
|
||||
props.getNullTtlSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// ---- #11: 补关键路径覆盖(loader 异常 / double-check / TypeReference / sliding 回源 / 降级不写缓存) ----
|
||||
|
||||
@Test
|
||||
void getOrLoadLoaderThrowsReleasesLockAndPropagates() {
|
||||
when(valueOps.get("cache:user:1")).thenReturn(null);
|
||||
when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class)))
|
||||
.thenReturn(true);
|
||||
RuntimeException boom = new RuntimeException("boom");
|
||||
RuntimeException thrown = assertThrows(RuntimeException.class,
|
||||
() -> template.getOrLoad("user:1", User.class, 60, () -> {
|
||||
throw boom;
|
||||
}));
|
||||
assertSame(boom, thrown); // 异常原样透传
|
||||
verify(redis).execute(any(RedisScript.class), anyList(), any()); // 锁在 finally 释放
|
||||
verify(valueOps, never()).set(anyString(), anyString(), anyLong(), any()); // 未写缓存
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrLoadDoubleCheckHitSkipsLoader() {
|
||||
props.setJitterRatio(0);
|
||||
when(valueOps.get("cache:user:1"))
|
||||
.thenReturn(null) // 首次 tryGet miss
|
||||
.thenReturn(JSON.toJSONString(new User(1L, "a"))); // 抢锁后 double-check 命中
|
||||
when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class)))
|
||||
.thenReturn(true);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
User got = template.getOrLoad("user:1", User.class, 60, () -> {
|
||||
calls.incrementAndGet();
|
||||
return new User(9L, "x");
|
||||
});
|
||||
assertEquals("a", got.getName());
|
||||
assertEquals(0, calls.get()); // double-check 命中,未回源
|
||||
verify(redis).execute(any(RedisScript.class), anyList(), any()); // 锁仍释放
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrLoadTypeReferenceRoundTrip() {
|
||||
props.setJitterRatio(0);
|
||||
when(valueOps.get("cache:list:1")).thenReturn(null);
|
||||
when(valueOps.setIfAbsent(eq("cache:list:1:__lock__"), anyString(), any(Duration.class)))
|
||||
.thenReturn(true);
|
||||
List<Integer> loaded = Arrays.asList(1, 2, 3);
|
||||
List<Integer> got = template.getOrLoad("list:1",
|
||||
new TypeReference<List<Integer>>() {
|
||||
}, 60, () -> loaded);
|
||||
assertEquals(loaded, got);
|
||||
verify(valueOps).set("cache:list:1", JSON.toJSONString(loaded), 60L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrLoadSlidingDoesNotRenewOnFreshLoad() {
|
||||
props.setSlidingExpireEnabled(true);
|
||||
props.setJitterRatio(0);
|
||||
when(valueOps.get("cache:user:1")).thenReturn(null);
|
||||
when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class)))
|
||||
.thenReturn(true);
|
||||
template.getOrLoad("user:1", User.class, 60, () -> new User(1L, "a"));
|
||||
// 回源新写入已自带 TTL,不应再触发滑动续期 expire
|
||||
verify(redis, never()).expire(eq("cache:user:1"), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrLoadWaitTimeoutFallbackDoesNotWriteCache() {
|
||||
props.getBreakdown().setWaitIntervalMs(1);
|
||||
props.getBreakdown().setWaitTimeoutMs(5);
|
||||
when(valueOps.get("cache:user:1")).thenReturn(null);
|
||||
when(valueOps.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(false);
|
||||
template.getOrLoad("user:1", User.class, 60, () -> new User(7L, "fb"));
|
||||
verify(valueOps, never()).set(anyString(), anyString(), anyLong(), any()); // 降级不写缓存
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user