task-154: 临时目录磁盘容量告警(aiimage.storage.capacity-warn-bytes 阈值、告警含当前容量/阈值、10 分钟频率限制、测量失败容忍不阻塞)+ 8 条测试
This commit is contained in:
@@ -14,6 +14,8 @@ public class StorageProperties {
|
|||||||
private long sourceRetentionHours = 24;
|
private long sourceRetentionHours = 24;
|
||||||
private long resultRetentionHours = 24;
|
private long resultRetentionHours = 24;
|
||||||
private long transientPayloadRetentionHours = 72;
|
private long transientPayloadRetentionHours = 72;
|
||||||
|
/** 临时目录容量告警阈值(字节),超过时记录告警日志(task-154)。 */
|
||||||
|
private long capacityWarnBytes = 50L * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
public String getLocalTempDir() {
|
public String getLocalTempDir() {
|
||||||
String configured = localTempDir == null ? "" : localTempDir.trim();
|
String configured = localTempDir == null ? "" : localTempDir.trim();
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
|
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.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时目录磁盘容量告警(task-154)。
|
||||||
|
*
|
||||||
|
* 容量超过阈值时记录告警日志(当前容量/阈值);告警带频率限制(默认每 10 分钟
|
||||||
|
* 至多一次,防刷屏);测量失败或目录缺失时仅记日志,不抛异常、不阻塞业务。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TempDirCapacityMonitor {
|
||||||
|
|
||||||
|
private static final long WARN_MIN_INTERVAL_MILLIS = 10 * 60 * 1000L;
|
||||||
|
|
||||||
|
private final StorageProperties storageProperties;
|
||||||
|
|
||||||
|
private volatile long lastWarnAtMillis = 0L;
|
||||||
|
|
||||||
|
@Scheduled(cron = "${aiimage.storage.capacity-warn-cron:0 */30 * * * *}")
|
||||||
|
public void checkTempDirCapacity() {
|
||||||
|
File tempDir = FileUtil.file(storageProperties.getLocalTempDir());
|
||||||
|
if (!tempDir.exists() || !tempDir.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long usage = measureUsageBytes(tempDir);
|
||||||
|
if (usage < 0) {
|
||||||
|
log.warn("temp dir capacity measurement failed dir={}", tempDir.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long threshold = storageProperties.getCapacityWarnBytes();
|
||||||
|
if (usage > threshold) {
|
||||||
|
warnWithRateLimit(tempDir, usage, threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 测量目录总字节数;失败返回 -1(不抛)。 */
|
||||||
|
public long measureUsageBytes(File dir) {
|
||||||
|
if (dir == null || !dir.isDirectory()) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
try (Stream<Path> paths = Files.walk(dir.toPath())) {
|
||||||
|
return paths.mapToLong(path -> {
|
||||||
|
try {
|
||||||
|
return Files.isRegularFile(path) ? Files.size(path) : 0L;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}).sum();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.warn("temp dir capacity walk failed dir={}", dir.getAbsolutePath(), ex);
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void warnWithRateLimit(File tempDir, long usage, long threshold) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - lastWarnAtMillis < WARN_MIN_INTERVAL_MILLIS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWarnAtMillis = now;
|
||||||
|
log.warn("temp dir capacity warning: usage={} bytes, threshold={} bytes, dir={}",
|
||||||
|
usage, threshold, tempDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task-154:临时目录磁盘容量告警契约(plan 09)。
|
||||||
|
* 容量超阈值记录告警(当前容量/阈值);未超不告警;告警频率受限(防刷屏);
|
||||||
|
* 测量失败容忍;不抛异常不阻塞业务。
|
||||||
|
*/
|
||||||
|
class TempDirCapacityMonitorTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private StorageProperties properties;
|
||||||
|
private TempDirCapacityMonitor monitor;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
properties = new StorageProperties();
|
||||||
|
properties.setLocalTempDir(tempDir.toString());
|
||||||
|
monitor = new TempDirCapacityMonitor(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usageMeasuredCorrectly() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("a.tmp"), "0123456789", StandardCharsets.UTF_8);
|
||||||
|
Path sub = tempDir.resolve("sub");
|
||||||
|
Files.createDirectories(sub);
|
||||||
|
Files.writeString(sub.resolve("b.tmp"), "12345", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
long usage = monitor.measureUsageBytes(tempDir.toFile());
|
||||||
|
|
||||||
|
assertEquals(15L, usage, "容量按字节递归求和");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyDirUsageIsZero() {
|
||||||
|
assertEquals(0L, monitor.measureUsageBytes(tempDir.toFile()), "空目录容量为 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void warningTriggeredOverThreshold() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("big.tmp"), "x".repeat(100), StandardCharsets.UTF_8);
|
||||||
|
properties.setCapacityWarnBytes(10);
|
||||||
|
ReflectionTestUtils.setField(monitor, "lastWarnAtMillis", 0L);
|
||||||
|
|
||||||
|
// 超阈值 → 告警路径(频率窗口内可告警)
|
||||||
|
monitor.warnWithRateLimit(tempDir.toFile(), 100L, 10L);
|
||||||
|
long lastWarn = (long) ReflectionTestUtils.getField(monitor, "lastWarnAtMillis");
|
||||||
|
assertTrue(lastWarn > 0L, "告警后记录时间戳(频率限制用)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void underThresholdDoesNotWarn() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("small.tmp"), "x", StandardCharsets.UTF_8);
|
||||||
|
properties.setCapacityWarnBytes(10_000);
|
||||||
|
|
||||||
|
monitor.checkTempDirCapacity();
|
||||||
|
|
||||||
|
long lastWarn = (long) ReflectionTestUtils.getField(monitor, "lastWarnAtMillis");
|
||||||
|
assertEquals(0L, lastWarn, "未超阈值不告警");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void warningRateLimited() throws Exception {
|
||||||
|
ReflectionTestUtils.setField(monitor, "lastWarnAtMillis", System.currentTimeMillis());
|
||||||
|
long before = (long) ReflectionTestUtils.getField(monitor, "lastWarnAtMillis");
|
||||||
|
|
||||||
|
monitor.warnWithRateLimit(tempDir.toFile(), 100L, 10L);
|
||||||
|
|
||||||
|
assertEquals(before, ReflectionTestUtils.getField(monitor, "lastWarnAtMillis"),
|
||||||
|
"频率窗口内重复告警被抑制");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void measureFailureIsGraceful() {
|
||||||
|
File missing = tempDir.resolve("nope").toFile();
|
||||||
|
assertEquals(-1L, monitor.measureUsageBytes(missing), "目录缺失测量返回 -1");
|
||||||
|
assertEquals(-1L, monitor.measureUsageBytes(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkDoesNotThrowOnMissingDir() {
|
||||||
|
properties.setLocalTempDir(tempDir.resolve("absent").toString());
|
||||||
|
|
||||||
|
monitor.checkTempDirCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void warningDoesNotBlockBusiness() {
|
||||||
|
// 告警路径不抛异常(业务不阻塞)
|
||||||
|
ReflectionTestUtils.setField(monitor, "lastWarnAtMillis", 0L);
|
||||||
|
monitor.warnWithRateLimit(tempDir.toFile(), 999L, 1L);
|
||||||
|
monitor.checkTempDirCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user