task-161: 巡检任务开关(aiimage.inspection.enabled 默认 disabled、cron 调度、分布式锁双实例去重、单巡检异常隔离、limit 透传)+ 8 条测试

This commit is contained in:
2026-09-02 07:19:56 +08:00
parent 9712bdf538
commit 035ec9ac79
3 changed files with 293 additions and 0 deletions
@@ -0,0 +1,24 @@
package com.nanri.aiimage.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 巡检任务配置(task-161)。
* 全部巡检默认 disabled;启用后按调度间隔执行;limit 为单次巡检行数上限。
*/
@Data
@Component
@ConfigurationProperties(prefix = "aiimage.inspection")
public class InspectionProperties {
/** 巡检总开关(默认关闭)。 */
private boolean enabled = false;
/** 调度 cron(默认凌晨 3 点)。 */
private String cron = "0 0 3 * * *";
/** 单次巡检行数上限。 */
private int limit = 200;
}
@@ -0,0 +1,81 @@
package com.nanri.aiimage.modules.task.service;
import com.nanri.aiimage.common.service.DistributedJobLockService;
import com.nanri.aiimage.config.InspectionProperties;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.file.service.TempOrphanInspector;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
/**
* 巡检任务调度(task-161)。
*
* 全部巡检由 aiimage.inspection.enabled 开关控制(默认 disabled);
* 启用后按 cron 调度执行;分布式锁防双实例重复输出;单个巡检异常不阻断其余。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class InspectionScheduler {
private static final Duration INSPECTION_LOCK_TTL = Duration.ofMinutes(10);
private final InspectionProperties inspectionProperties;
private final DistributedJobLockService distributedJobLockService;
private final StorageProperties storageProperties;
private final TempOrphanInspector tempOrphanInspector;
private final OrphanJobInspector orphanJobInspector;
private final TaskResultMissingInspector taskResultMissingInspector;
private final ResultFileMissingInspector resultFileMissingInspector;
private final CompletedTaskActiveJobInspector completedTaskActiveJobInspector;
@Scheduled(cron = "${aiimage.inspection.cron:0 0 3 * * *}")
public void runInspections() {
if (!inspectionProperties.isEnabled()) {
return;
}
DistributedJobLockService.LockHandle lockHandle =
distributedJobLockService.tryLock("temp-file-inspection", INSPECTION_LOCK_TTL);
if (lockHandle == null) {
log.info("[inspection] skip because another instance holds the inspection lock");
return;
}
try (lockHandle) {
runAllSafely();
}
}
private void runAllSafely() {
int limit = inspectionProperties.getLimit();
runSafely("orphan-file", () -> {
File tempDir = cn.hutool.core.io.FileUtil.file(storageProperties.getLocalTempDir());
if (tempDir.isDirectory()) {
var report = tempOrphanInspector.inspectOrphanFiles(
tempDir, Instant.now().minus(Duration.ofHours(24)), name -> false);
log.info("[inspection] orphan-file report count={}", report.entries().size());
}
});
runSafely("orphan-job", () -> log.info("[inspection] orphan-job report count={}",
orphanJobInspector.inspectOrphanJobs(limit).entries().size()));
runSafely("task-missing-result", () -> log.info("[inspection] task-missing-result report count={}",
taskResultMissingInspector.inspectTasksMissingResult(limit).entries().size()));
runSafely("result-missing-file", () -> log.info("[inspection] result-missing-file report count={}",
resultFileMissingInspector.inspectResultsMissingFile(limit).entries().size()));
runSafely("terminal-active-job", () -> log.info("[inspection] terminal-active-job report count={}",
completedTaskActiveJobInspector.inspectTerminalTasksWithActiveJobs(limit).entries().size()));
}
private void runSafely(String name, Runnable action) {
try {
action.run();
} catch (Exception ex) {
log.warn("[inspection] {} failed msg={}", name, ex.getMessage(), ex);
}
}
}