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:
root
2026-06-26 10:11:48 +08:00
parent 24422d0ce4
commit 0657126f60

View File

@@ -1,6 +1,7 @@
package com.njcn.middle.cache.template; package com.njcn.middle.cache.template;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.njcn.middle.cache.autoconfig.RedisCacheProperties; import com.njcn.middle.cache.autoconfig.RedisCacheProperties;
import com.njcn.middle.cache.constant.CacheConstant; import com.njcn.middle.cache.constant.CacheConstant;
import com.njcn.middle.cache.support.CacheKeyBuilder; 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 org.springframework.data.redis.core.script.RedisScript;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -349,4 +353,75 @@ class RedisCacheEnhanceTemplateTest {
verify(valueOps).set("cache:user:1", CacheConstant.NULL_SENTINEL, verify(valueOps).set("cache:user:1", CacheConstant.NULL_SENTINEL,
props.getNullTtlSeconds(), TimeUnit.SECONDS); 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()); // 降级不写缓存
}
} }