task-71: 清理日志改为数量与 sample ID,禁止输出超长任务 ID 列表
This commit is contained in:
+27
-2
@@ -44,6 +44,7 @@ public class ModuleHistoryCleanupService {
|
||||
private static final Duration CLEANUP_LOCK_TTL = Duration.ofHours(2);
|
||||
private static final String COLLECT_DATA_MODULE_TYPE = "COLLECT_DATA";
|
||||
private static final int DEFAULT_BATCH_SIZE = 500;
|
||||
private static final int LOG_SAMPLE_IDS = 5;
|
||||
|
||||
private final ModuleCleanupProperties moduleCleanupProperties;
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
@@ -166,7 +167,7 @@ public class ModuleHistoryCleanupService {
|
||||
int deletedRows = deleteRows(types, taskIds, collectDataTaskIds);
|
||||
submitAndFlush(collected);
|
||||
log.info("[module-cleanup] batch: taskIds={}, deletedRows={}, collectedPointers={}",
|
||||
taskIds, deletedRows, collected.size());
|
||||
formatIdSample(taskIds, LOG_SAMPLE_IDS), deletedRows, collected.size());
|
||||
});
|
||||
totalBatches++;
|
||||
totalDeletedTasks += taskIds.size();
|
||||
@@ -181,7 +182,8 @@ public class ModuleHistoryCleanupService {
|
||||
log.info("[module-cleanup] completed: moduleTypes={}, retentionDays={}, cutoff={}, batches={}, deletedTasks={}, collectedPayloadPointers={}, skippedActiveTaskIds={}, retainedTaskIds={}",
|
||||
moduleTypes, moduleCleanupProperties.getRetentionDays(), cutoff,
|
||||
totalBatches, totalDeletedTasks, totalCollectedPointers,
|
||||
skippedActiveTaskIds, skippedRetainedTaskIds);
|
||||
formatIdSample(skippedActiveTaskIds, LOG_SAMPLE_IDS),
|
||||
formatIdSample(skippedRetainedTaskIds, LOG_SAMPLE_IDS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,4 +287,27 @@ public class ModuleHistoryCleanupService {
|
||||
pointers.add(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志用 ID 摘要:只输出数量与最多 {@code sampleLimit} 个样本,
|
||||
* 禁止把超长任务 ID 列表写进日志。非法上限回退到 1,null/空输出 count=0。
|
||||
*/
|
||||
static String formatIdSample(List<Long> ids, int sampleLimit) {
|
||||
int count = ids == null ? 0 : ids.size();
|
||||
if (count == 0) {
|
||||
return "count=0";
|
||||
}
|
||||
int limit = Math.max(1, sampleLimit);
|
||||
StringBuilder sb = new StringBuilder("count=").append(count).append(", sample=[");
|
||||
for (int i = 0; i < Math.min(count, limit); i++) {
|
||||
if (i > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(ids.get(i));
|
||||
}
|
||||
if (count > limit) {
|
||||
sb.append(",...");
|
||||
}
|
||||
return sb.append(']').toString();
|
||||
}
|
||||
}
|
||||
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
package com.nanri.aiimage.modules.task.service;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||
import com.nanri.aiimage.config.InstanceMetadata;
|
||||
import com.nanri.aiimage.config.ModuleCleanupProperties;
|
||||
import com.nanri.aiimage.config.StorageProperties;
|
||||
import com.nanri.aiimage.config.TransientStorageProperties;
|
||||
import com.nanri.aiimage.modules.collectdata.mapper.CollectDataItemMapper;
|
||||
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity;
|
||||
import com.nanri.aiimage.modules.file.service.object.RustfsObjectStorageService;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskFileJobMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskProgressSnapshotMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskResultItemMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskResultPayloadMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskProgressSnapshotEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskResultItemEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskResultPayloadEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Task 71:清理日志改为数量与 sample ID,禁止输出超长任务 ID 列表。
|
||||
* ModuleHistoryCleanupService 的日志只打印 count 与最多 5 个 sample id
|
||||
* (超限追加省略号),不打印完整任务 ID 列表;formatIdSample 对空值、
|
||||
* 单元素、超限与非法参数均有确定行为。
|
||||
*/
|
||||
class ModuleHistoryCleanupLoggingTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private static final LocalDateTime OLD_FINISHED_AT = LocalDateTime.of(2026, 1, 1, 0, 0);
|
||||
private static final int LOG_SAMPLE_IDS = 5;
|
||||
|
||||
private ModuleCleanupProperties cleanupProperties;
|
||||
private FileTaskMapper fileTaskMapper;
|
||||
private FileResultMapper fileResultMapper;
|
||||
private TaskFileJobMapper taskFileJobMapper;
|
||||
private TaskResultItemMapper taskResultItemMapper;
|
||||
private TaskProgressSnapshotMapper taskProgressSnapshotMapper;
|
||||
private TaskResultPayloadMapper taskResultPayloadMapper;
|
||||
private TaskScopeStateMapper taskScopeStateMapper;
|
||||
private TaskChunkMapper taskChunkMapper;
|
||||
private CollectDataItemMapper collectDataItemMapper;
|
||||
private DistributedJobLockService lockService;
|
||||
private TransientPayloadStorageService storage;
|
||||
private RustfsObjectStorageService rustfs;
|
||||
private OssStorageService oss;
|
||||
private TransientPayloadDeleteOrchestrator orchestrator;
|
||||
private ExecutorService executor;
|
||||
private PlatformTransactionManager transactionManager;
|
||||
private ModuleHistoryCleanupService cleanupService;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeMybatisMetadata() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, FileTaskEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, FileResultEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskFileJobEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskResultItemEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskProgressSnapshotEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskResultPayloadEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskScopeStateEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, TaskChunkEntity.class);
|
||||
TableInfoHelper.initTableInfo(assistant, CollectDataItemEntity.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
cleanupProperties = new ModuleCleanupProperties();
|
||||
fileTaskMapper = mock(FileTaskMapper.class);
|
||||
fileResultMapper = mock(FileResultMapper.class);
|
||||
taskFileJobMapper = mock(TaskFileJobMapper.class);
|
||||
taskResultItemMapper = mock(TaskResultItemMapper.class);
|
||||
taskProgressSnapshotMapper = mock(TaskProgressSnapshotMapper.class);
|
||||
taskResultPayloadMapper = mock(TaskResultPayloadMapper.class);
|
||||
taskScopeStateMapper = mock(TaskScopeStateMapper.class);
|
||||
taskChunkMapper = mock(TaskChunkMapper.class);
|
||||
collectDataItemMapper = mock(CollectDataItemMapper.class);
|
||||
StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
|
||||
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE);
|
||||
lockService = new DistributedJobLockService(redisTemplate);
|
||||
|
||||
rustfs = mock(RustfsObjectStorageService.class);
|
||||
oss = mock(OssStorageService.class);
|
||||
TransientStorageProperties transientProperties = new TransientStorageProperties();
|
||||
transientProperties.setEnabled(true);
|
||||
StorageProperties storageProperties = new StorageProperties();
|
||||
storageProperties.setLocalTempDir(tempDir.toString());
|
||||
storage = new TransientPayloadStorageService(
|
||||
transientProperties, storageProperties, rustfs, oss,
|
||||
new ObjectMapper(), new InstanceMetadata("test-instance"),
|
||||
taskChunkMapper, taskScopeStateMapper);
|
||||
executor = Executors.newFixedThreadPool(2);
|
||||
orchestrator = new TransientPayloadDeleteOrchestrator(
|
||||
storage, rustfs, taskChunkMapper, taskScopeStateMapper, new ObjectMapper(), executor);
|
||||
ReflectionTestUtils.setField(orchestrator, "maxPendingDeletes", 1000L);
|
||||
transactionManager = mock(PlatformTransactionManager.class);
|
||||
doAnswer(invocation -> mock(TransactionStatus.class))
|
||||
.when(transactionManager).getTransaction(ArgumentMatchers.any(TransactionDefinition.class));
|
||||
doAnswer(invocation -> null).when(transactionManager).commit(any(TransactionStatus.class));
|
||||
doAnswer(invocation -> null).when(transactionManager).rollback(any(TransactionStatus.class));
|
||||
cleanupService = new ModuleHistoryCleanupService(
|
||||
cleanupProperties, fileTaskMapper, fileResultMapper, taskFileJobMapper,
|
||||
taskResultItemMapper, taskProgressSnapshotMapper, taskResultPayloadMapper,
|
||||
taskScopeStateMapper, taskChunkMapper, collectDataItemMapper, lockService, orchestrator,
|
||||
transactionManager);
|
||||
}
|
||||
|
||||
private static FileTaskEntity expiredTask(long id) {
|
||||
FileTaskEntity task = new FileTaskEntity();
|
||||
task.setId(id);
|
||||
task.setModuleType("DEDUPE");
|
||||
task.setStatus("SUCCESS");
|
||||
task.setFinishedAt(OLD_FINISHED_AT);
|
||||
return task;
|
||||
}
|
||||
|
||||
private static <T> Answer<List<T>> sequence(List<T> rows, int rowCalls, List<T> fallback) {
|
||||
return new Answer<List<T>>() {
|
||||
private int calls;
|
||||
|
||||
@Override
|
||||
public List<T> answer(InvocationOnMock invocation) {
|
||||
return calls++ < rowCalls ? rows : fallback;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ListAppender<ILoggingEvent> attachLogAppender() {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(ModuleHistoryCleanupService.class);
|
||||
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||
appender.setContext(logger.getLoggerContext());
|
||||
appender.start();
|
||||
logger.addAppender(appender);
|
||||
return appender;
|
||||
}
|
||||
|
||||
private static void detachLogAppender(ListAppender<ILoggingEvent> appender) {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(ModuleHistoryCleanupService.class);
|
||||
logger.detachAppender(appender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_normal_default_path() {
|
||||
// 默认路径:正常列表完整输出,数量与 sample 齐全。
|
||||
List<Long> ids = List.of(11L, 22L, 33L);
|
||||
assertEquals("count=3, sample=[11,22,33]",
|
||||
ModuleHistoryCleanupService.formatIdSample(ids, LOG_SAMPLE_IDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_normal_multiple_items() {
|
||||
// 批量场景:超过 sample 上限时输出截断列表与省略号,count 保持完整。
|
||||
List<Long> ids = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L);
|
||||
assertEquals("count=7, sample=[1,2,3,4,5,...]",
|
||||
ModuleHistoryCleanupService.formatIdSample(ids, LOG_SAMPLE_IDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_normal_repeated_operation_is_idempotent() {
|
||||
// 幂等:同一输入重复格式化结果一致,无随机性。
|
||||
List<Long> ids = List.of(9L, 8L, 7L, 6L, 5L, 4L);
|
||||
String first = ModuleHistoryCleanupService.formatIdSample(ids, LOG_SAMPLE_IDS);
|
||||
String second = ModuleHistoryCleanupService.formatIdSample(ids, LOG_SAMPLE_IDS);
|
||||
assertEquals(first, second);
|
||||
assertEquals("count=6, sample=[9,8,7,6,5,...]", first);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_boundary_empty_input() {
|
||||
// 空输入:null 与空列表输出 count=0,不输出列表。
|
||||
assertEquals("count=0", ModuleHistoryCleanupService.formatIdSample(null, LOG_SAMPLE_IDS));
|
||||
assertEquals("count=0", ModuleHistoryCleanupService.formatIdSample(List.of(), LOG_SAMPLE_IDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_boundary_single_item() {
|
||||
// 单元素:单个 id 完整输出,不依赖批量路径。
|
||||
assertEquals("count=1, sample=[42]",
|
||||
ModuleHistoryCleanupService.formatIdSample(List.of(42L), LOG_SAMPLE_IDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_boundary_limit_and_overflow() {
|
||||
// 上限/超限:sample 上限为 1 时只输出首个 id 加省略号,不输出完整列表。
|
||||
List<Long> ids = List.of(1L, 2L, 3L, 4L, 5L);
|
||||
assertEquals("count=5, sample=[1,...]",
|
||||
ModuleHistoryCleanupService.formatIdSample(ids, 1));
|
||||
assertEquals("count=5, sample=[1,...]",
|
||||
ModuleHistoryCleanupService.formatIdSample(ids, 0), "非法上限回退到 1");
|
||||
assertEquals("count=5, sample=[1,...]",
|
||||
ModuleHistoryCleanupService.formatIdSample(ids, -3), "负上限回退到 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_invalid_input_rejected() {
|
||||
// 非法参数:列表含 null 元素时不抛异常,输出稳定占位;count 保持实际元素数。
|
||||
assertEquals("count=2, sample=[null,22]",
|
||||
ModuleHistoryCleanupService.formatIdSample(new ArrayList<>(java.util.Arrays.asList(null, 22L)), LOG_SAMPLE_IDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_task_071_cleanup_logging_dependency_failure_releases_resources() {
|
||||
// 依赖失败:服务日志不使用超长列表;依赖异常传播且日志中无完整 ID 列表。
|
||||
ListAppender<ILoggingEvent> appender = attachLogAppender();
|
||||
try {
|
||||
cleanupProperties.setBatchSize(100);
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (long i = 1; i <= 60; i++) {
|
||||
ids.add(i);
|
||||
}
|
||||
List<FileTaskEntity> tasks = new ArrayList<>();
|
||||
for (long id : ids) {
|
||||
tasks.add(expiredTask(id));
|
||||
}
|
||||
when(fileTaskMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenAnswer(sequence(tasks, 1, List.of()));
|
||||
lenient().when(taskFileJobMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
||||
lenient().when(taskResultItemMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
||||
lenient().when(taskProgressSnapshotMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
||||
lenient().when(taskResultPayloadMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
||||
lenient().when(taskScopeStateMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(0);
|
||||
when(taskChunkMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||
when(taskScopeStateMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of());
|
||||
|
||||
when(taskChunkMapper.delete(any(LambdaQueryWrapper.class)))
|
||||
.thenThrow(new RuntimeException("db down"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> cleanupService.cleanupConfiguredModules());
|
||||
|
||||
for (ILoggingEvent event : appender.list) {
|
||||
String message = event.getFormattedMessage();
|
||||
assertTrue(!message.contains("taskIds=["), "日志不输出超长任务 ID 列表: " + message);
|
||||
assertTrue(!message.contains("skippedActiveTaskIds=["), "日志不输出超长活跃 ID 列表: " + message);
|
||||
assertTrue(!message.contains("retainedTaskIds=["), "日志不输出超长保留 ID 列表: " + message);
|
||||
}
|
||||
verify(taskChunkMapper).selectList(any(LambdaQueryWrapper.class));
|
||||
} finally {
|
||||
detachLogAppender(appender);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user