task-62: 本地任务实体缓存共享组件(容量 LRU/TTL 即时回收/定时清理)接入 8 模块

This commit is contained in:
2026-08-30 18:17:52 +08:00
parent dff38a04c7
commit 8d91e599ab
11 changed files with 319 additions and 182 deletions
@@ -5,6 +5,7 @@ import com.nanri.aiimage.config.TaskPressureProperties;
import com.nanri.aiimage.modules.shopdatacrawl.model.dto.ShopDataCrawlShopPayloadDto;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import com.nanri.aiimage.modules.task.service.TaskScopePayloadStorageService;
import com.nanri.aiimage.modules.task.util.TaskEntityLocalCache;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
@@ -54,7 +55,8 @@ class ShopDataCrawlTaskCacheServiceTest {
stringRedisTemplate,
objectMapper,
properties,
taskScopePayloadStorageService);
taskScopePayloadStorageService,
new TaskEntityLocalCache(properties, objectMapper));
lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
lenient().when(valueOperations.multiGet(any())).thenReturn(List.of());
@@ -0,0 +1,151 @@
package com.nanri.aiimage.modules.task.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.TaskPressureProperties;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* Task 62:为本地任务实体缓存增加最大条目数、TTL 和定时清理。
* TaskEntityLocalCache 为共享任务链路提供有界本地实体缓存:容量超限按
* cachedAtMillis LRU 淘汰最旧条目(内存有界,不发生无界增长);TTL 内
* 返回实体副本、过期条目读取时即时回收;定时清理 removeExpired 批量
* 释放过期条目;同一任务重复保存只占一条(幂等)、删除即时释放;
* 空/非法输入安全跳过不创建无效条目。
*/
class TaskEntityLocalCacheTest {
private TaskPressureProperties properties;
private TaskEntityLocalCache cache;
@BeforeEach
void setUp() {
properties = new TaskPressureProperties();
properties.setLocalTaskEntityCacheMillis(3000);
properties.setLocalTaskEntityCacheCapacity(4);
cache = new TaskEntityLocalCache(properties, new ObjectMapper());
}
private static FileTaskEntity task(long id) {
FileTaskEntity entity = new FileTaskEntity();
entity.setId(id);
entity.setModuleType("TEST");
return entity;
}
@Test
void test_task_062_cache_cleanup_normal_default_path() {
// 正常路径:保存后 TTL 内立即可读回同值实体,缓存条目数精确。
cache.put(1L, task(1L));
TaskEntityLocalCache.Entry entry = cache.get(1L);
assertNotNull(entry, "保存后立即可读");
assertEquals(1L, entry.task().getId(), "读回实体一致");
assertEquals(1, cache.size(), "恰好一条");
}
@Test
void test_task_062_cache_cleanup_normal_multiple_items() {
// 批量场景:容量内多个任务实体全部可读回,数量精确,互不覆盖。
properties.setLocalTaskEntityCacheCapacity(10);
for (long id = 1; id <= 5; id++) {
cache.put(id, task(id));
}
for (long id = 1; id <= 5; id++) {
TaskEntityLocalCache.Entry entry = cache.get(id);
assertNotNull(entry, "任务 " + id + " 可读回");
assertEquals(id, entry.task().getId(), "任务 " + id + " 值正确");
}
assertEquals(5, cache.size(), "5 条全部保留");
}
@Test
void test_task_062_cache_cleanup_normal_repeated_operation_is_idempotent() {
// 幂等:同一任务重复保存只占一条,值稳定,不产生重复条目。
cache.put(1L, task(1L));
cache.put(1L, task(1L));
cache.put(1L, task(1L));
assertEquals(1, cache.size(), "重复保存只占一条");
assertEquals(1L, cache.get(1L).task().getId(), "值稳定");
}
@Test
void test_task_062_cache_cleanup_boundary_empty_input() {
// 空输入:null 任务 / 空 id 任务 / null 查询安全跳过,不创建无效条目。
cache.put(null, task(1L));
cache.put(2L, null);
FileTaskEntity noId = new FileTaskEntity();
cache.put(noId.getId(), noId);
assertNull(cache.get(null), "null 查询返回 null");
assertNull(cache.get(-1L), "非法 id 查询返回 null");
assertEquals(0, cache.size(), "空输入零条目");
}
@Test
void test_task_062_cache_cleanup_boundary_single_item() {
// 单元素:单条保存读回正确,删除后立即释放。
cache.put(9L, task(9L));
assertEquals(9L, cache.get(9L).task().getId(), "单条读回");
cache.evict(9L);
assertNull(cache.get(9L), "删除后释放");
assertEquals(0, cache.size(), "删除后零条目");
}
@Test
void test_task_062_cache_cleanup_boundary_limit_and_overflow() {
// 上限/超限:容量 4 保存 6 条 → 最旧 2 条被 LRU 淘汰,
// 条目数不超过容量上限,最新 4 条仍可读回,无无界增长。
for (long id = 1; id <= 6; id++) {
cache.put(id, task(id));
}
assertEquals(4, cache.size(), "条目数不超过容量上限");
assertNull(cache.get(1L), "最旧条目被淘汰");
assertNull(cache.get(2L), "次旧条目被淘汰");
for (long id = 3; id <= 6; id++) {
assertNotNull(cache.get(id), "任务 " + id + " 仍可读回");
}
}
@Test
void test_task_062_cache_cleanup_invalid_input_rejected() {
// 非法参数:非正任务 id 保存被拒绝,缓存状态不受污染。
FileTaskEntity negative = task(-1L);
FileTaskEntity zero = task(0L);
cache.put(-1L, negative);
cache.put(0L, zero);
assertEquals(0, cache.size(), "非法 id 零条目");
}
@Test
void test_task_062_cache_cleanup_dependency_failure_releases_resources() {
// 依赖失败(时间流逝使缓存条目过期):过期条目读取时即时回收,
// 定时清理批量释放全部过期条目,未过期条目保留,无资源残留。
cache.put(1L, task(1L));
cache.put(2L, task(2L));
long now = System.currentTimeMillis();
assertNull(cache.get(1L, now + 4000), "超过 TTL 读取返回 null 并即时回收");
assertEquals(1, cache.size(), "过期条目读取时已回收");
int removed = cache.removeExpired(now + 4000);
assertEquals(1, removed, "定时清理释放剩余过期条目");
assertEquals(0, cache.size(), "过期条目全部释放");
cache.put(3L, task(3L));
assertEquals(0, cache.removeExpired(now), "未过期条目保留");
assertEquals(1, cache.size(), "未过期条目不受清理影响");
}
}