task-200: 巡检报表任务落地(InspectionSqlReportTask 跑 6 张只读报表 SQL:默认 disabled、分布式锁、单条失败不阻断、只 query)+ 8 条测试
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
|||||||
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||||
|
import com.nanri.aiimage.config.InspectionProperties;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检只读 SQL 报表任务(task-200)。
|
||||||
|
*
|
||||||
|
* 与 InspectionScheduler 同开关/调度风格:aiimage.inspection.enabled 默认 false;启用后按
|
||||||
|
* aiimage.inspection.cron 执行 6 张只读报表 SQL(resources/inspection/*.sql,task-194..199),
|
||||||
|
* 逐条输出行数与耗时;单条失败记录日志不阻断其余;分布式锁防双实例重复;只 query 不改数据。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class InspectionSqlReportTask {
|
||||||
|
|
||||||
|
private static final Duration REPORT_LOCK_TTL = Duration.ofMinutes(10);
|
||||||
|
|
||||||
|
/** 与巡检 SQL 文件一一对应(顺序执行)。 */
|
||||||
|
static final List<String> REPORT_FILES = List.of(
|
||||||
|
"01_orphan_job.sql", "02_orphan_result.sql", "03_task_missing_result.sql",
|
||||||
|
"04_result_missing_file.sql", "05_terminal_task_active_job.sql",
|
||||||
|
"06_over_retention_active_job.sql");
|
||||||
|
|
||||||
|
private final InspectionProperties inspectionProperties;
|
||||||
|
private final DistributedJobLockService distributedJobLockService;
|
||||||
|
private final ObjectProvider<JdbcTemplate> jdbcTemplateProvider;
|
||||||
|
|
||||||
|
/** 测试/无锁构造。 */
|
||||||
|
public InspectionSqlReportTask(InspectionProperties inspectionProperties,
|
||||||
|
ObjectProvider<JdbcTemplate> jdbcTemplateProvider) {
|
||||||
|
this(inspectionProperties, null, jdbcTemplateProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public InspectionSqlReportTask(InspectionProperties inspectionProperties,
|
||||||
|
DistributedJobLockService distributedJobLockService,
|
||||||
|
ObjectProvider<JdbcTemplate> jdbcTemplateProvider) {
|
||||||
|
this.inspectionProperties = inspectionProperties;
|
||||||
|
this.distributedJobLockService = distributedJobLockService;
|
||||||
|
this.jdbcTemplateProvider = jdbcTemplateProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "${aiimage.inspection.cron:0 0 3 * * *}")
|
||||||
|
public void runIfEnabled() {
|
||||||
|
if (!inspectionProperties.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (distributedJobLockService == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DistributedJobLockService.LockHandle lockHandle =
|
||||||
|
distributedJobLockService.tryLock("inspection-sql-report", REPORT_LOCK_TTL);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
log.info("[inspection-sql] skip because another instance holds the report lock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (lockHandle) {
|
||||||
|
runAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 执行全部只读报表 SQL;返回成功执行的报表数(disabled/无 JDBC 返回 0,失败单条不阻断)。 */
|
||||||
|
public int runAll() {
|
||||||
|
if (!inspectionProperties.isEnabled()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
JdbcTemplate jdbcTemplate = jdbcTemplateProvider.getIfAvailable();
|
||||||
|
if (jdbcTemplate == null) {
|
||||||
|
log.warn("[inspection-sql] enabled but no JdbcTemplate bean, skip reports");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int ok = 0;
|
||||||
|
for (String file : REPORT_FILES) {
|
||||||
|
long startedAt = System.nanoTime();
|
||||||
|
try {
|
||||||
|
String sql = loadSql(file);
|
||||||
|
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
|
||||||
|
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
|
||||||
|
log.info("[inspection-sql] report {} rows={} elapsedMs={}",
|
||||||
|
file, rows.size(), elapsedMs);
|
||||||
|
ok++;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
|
||||||
|
log.error("[inspection-sql] report {} failed elapsedMs={} cause={}",
|
||||||
|
file, elapsedMs, ex.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadSql(String file) throws java.io.IOException {
|
||||||
|
ClassPathResource resource = new ClassPathResource("inspection/" + file);
|
||||||
|
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
import ch.qos.logback.core.read.ListAppender;
|
||||||
|
import com.nanri.aiimage.config.InspectionProperties;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task-200:巡检报表任务契约。
|
||||||
|
* 默认 disabled 不执行;启用后跑 6 张只读报表 SQL 并输出行数;单条失败不阻断;只 query 不改数据;
|
||||||
|
* 可重复执行;无锁/JDBC 时安全返回。
|
||||||
|
*/
|
||||||
|
class InspectionSqlReportTaskTest {
|
||||||
|
|
||||||
|
private static ObjectProvider<JdbcTemplate> provider(JdbcTemplate jdbc) {
|
||||||
|
return new ObjectProvider<JdbcTemplate>() {
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getObject() {
|
||||||
|
return jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getObject(Object... args) {
|
||||||
|
return jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getIfAvailable() {
|
||||||
|
return jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getIfUnique() {
|
||||||
|
return jdbc;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListAppender<ILoggingEvent> attach() {
|
||||||
|
Logger logger = (Logger) LoggerFactory.getLogger(InspectionSqlReportTask.class);
|
||||||
|
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||||
|
appender.start();
|
||||||
|
logger.addAppender(appender);
|
||||||
|
return appender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disabledByDefaultRunsNothing() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
assertEquals(false, props.isEnabled(), "巡检默认 disabled");
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
assertEquals(0, task.runAll(), "disabled 不执行");
|
||||||
|
Mockito.verifyNoInteractions(jdbc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enabledRunsAllSixReports() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
assertEquals(6, task.runAll());
|
||||||
|
verify(jdbc, times(6)).queryForList(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportOutputLogsRowCount() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of(Map.of("a", 1), Map.of("a", 2), Map.of("a", 3)));
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
ListAppender<ILoggingEvent> captured = attach();
|
||||||
|
try {
|
||||||
|
task.runAll();
|
||||||
|
String text = String.join("\n", captured.list.stream().map(ILoggingEvent::getFormattedMessage).toList());
|
||||||
|
assertTrue(text.contains("rows=3"), "报表日志应含行数: " + text);
|
||||||
|
assertTrue(text.contains("[inspection-sql] report"), "应含报表前缀");
|
||||||
|
} finally {
|
||||||
|
((Logger) LoggerFactory.getLogger(InspectionSqlReportTask.class)).detachAppender(captured);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void singleReportFailureDoesNotBlockOthers() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
AtomicInteger calls = new AtomicInteger();
|
||||||
|
Mockito.doAnswer(inv -> {
|
||||||
|
if (calls.incrementAndGet() == 3) {
|
||||||
|
throw new RuntimeException("模拟第三条 SQL 失败");
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}).when(jdbc).queryForList(anyString());
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
assertEquals(5, task.runAll(), "单条失败应被捕获,其余继续");
|
||||||
|
verify(jdbc, times(6)).queryForList(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scheduleConfigDefaults() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
assertEquals("0 0 3 * * *", props.getCron());
|
||||||
|
assertEquals(false, props.isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onlyReadQueriesNoWrites() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
task.runAll();
|
||||||
|
verify(jdbc, times(6)).queryForList(anyString());
|
||||||
|
verify(jdbc, never()).execute(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noLockOrJdbcSafelyNoop() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
InspectionSqlReportTask noJdbc = new InspectionSqlReportTask(props, new ObjectProvider<JdbcTemplate>() {
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getObject() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getObject(Object... args) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getIfAvailable() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JdbcTemplate getIfUnique() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertEquals(0, noJdbc.runAll(), "启用但无 JdbcTemplate 应安全跳过");
|
||||||
|
// 无分布式锁实例时 runIfEnabled 不应抛错(测试构造传入 null lock)
|
||||||
|
InspectionSqlReportTask noLock = new InspectionSqlReportTask(props, null, provider(mock(JdbcTemplate.class)));
|
||||||
|
noLock.runIfEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enabledStableAcrossRuns() {
|
||||||
|
InspectionProperties props = new InspectionProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||||
|
Mockito.when(jdbc.queryForList(anyString())).thenReturn(List.of());
|
||||||
|
InspectionSqlReportTask task = new InspectionSqlReportTask(props, provider(jdbc));
|
||||||
|
assertEquals(6, task.runAll());
|
||||||
|
assertEquals(6, task.runAll());
|
||||||
|
verify(jdbc, times(12)).queryForList(anyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user