feat: make storage tier thresholds configurable in system settings
Add StorageGreenThresholdPercent (default 30%) and StorageRedThresholdPercent (default 10%) to both SystemSettingsDto and the settings UI. StorageGuardService now reads thresholds from settings instead of hardcoding. Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
co-authored by
Claude Opus 4.8 noreply@anthropic.com
parent
f5ad1dec00
commit
0831b33ea0
@@ -421,6 +421,8 @@ export interface SystemSettings {
|
|||||||
enableStorageGuard: boolean;
|
enableStorageGuard: boolean;
|
||||||
pauseRecordingWhenFreeSpaceBelowMegabytes: number;
|
pauseRecordingWhenFreeSpaceBelowMegabytes: number;
|
||||||
resumeRecordingWhenFreeSpaceAboveMegabytes: number;
|
resumeRecordingWhenFreeSpaceAboveMegabytes: number;
|
||||||
|
storageGreenThresholdPercent: number;
|
||||||
|
storageRedThresholdPercent: number;
|
||||||
enableRetentionCleanup: boolean;
|
enableRetentionCleanup: boolean;
|
||||||
retentionDays: number;
|
retentionDays: number;
|
||||||
retentionDeleteFiles: boolean;
|
retentionDeleteFiles: boolean;
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ const form = reactive<SettingsFormModel>({
|
|||||||
enableStorageGuard: true,
|
enableStorageGuard: true,
|
||||||
pauseRecordingWhenFreeSpaceBelowMegabytes: 1024,
|
pauseRecordingWhenFreeSpaceBelowMegabytes: 1024,
|
||||||
resumeRecordingWhenFreeSpaceAboveMegabytes: 4096,
|
resumeRecordingWhenFreeSpaceAboveMegabytes: 4096,
|
||||||
|
storageGreenThresholdPercent: 30,
|
||||||
|
storageRedThresholdPercent: 10,
|
||||||
enableRetentionCleanup: false,
|
enableRetentionCleanup: false,
|
||||||
retentionDays: 30,
|
retentionDays: 30,
|
||||||
retentionDeleteFiles: false,
|
retentionDeleteFiles: false,
|
||||||
@@ -1097,10 +1099,37 @@ watch(
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row :gutter="16" style="margin-top: 16px">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="绿色水位线:剩余空间高于 (%)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.storageGreenThresholdPercent"
|
||||||
|
:min="5"
|
||||||
|
:max="90"
|
||||||
|
:step="5"
|
||||||
|
:disabled="!form.enableStorageGuard"
|
||||||
|
/>
|
||||||
|
<div class="field-hint">高于此比例时正常录制,低于时拒绝新录制</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="红色水位线:剩余空间低于 (%)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.storageRedThresholdPercent"
|
||||||
|
:min="1"
|
||||||
|
:max="85"
|
||||||
|
:step="5"
|
||||||
|
:disabled="!form.enableStorageGuard"
|
||||||
|
/>
|
||||||
|
<div class="field-hint">低于此比例时暂停所有录制和转码,仅保留上传</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="helper-panel">
|
<div class="helper-panel">
|
||||||
恢复阈值建议高于暂停阈值,避免磁盘空间在临界值附近反复抖动。MP4 转码会额外占用中间 TS 文件空间。
|
恢复阈值建议高于暂停阈值,避免磁盘空间在临界值附近反复抖动。MP4 转码会额外占用中间 TS 文件空间。<br/>
|
||||||
|
绿色/红色水位线控制三级存储保护:<b>绿色</b>(正常录制) → <b>黄色</b>(拒绝新录制,现有继续转码上传) → <b>红色</b>(暂停所有录制转码,仅上传清盘)。
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ public sealed class SystemSettingsDto
|
|||||||
|
|
||||||
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
|
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
|
||||||
|
|
||||||
|
/// <summary>Disk free percentage above which storage is considered healthy (Green tier). Default 30%.</summary>
|
||||||
|
public double StorageGreenThresholdPercent { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>Disk free percentage below which storage is critical (Red tier). Default 10%.</summary>
|
||||||
|
public double StorageRedThresholdPercent { get; set; } = 10;
|
||||||
|
|
||||||
public bool EnableAutoReconnect { get; set; } = true;
|
public bool EnableAutoReconnect { get; set; } = true;
|
||||||
|
|
||||||
public int ReconnectDelayMaxSeconds { get; set; } = 5;
|
public int ReconnectDelayMaxSeconds { get; set; } = 5;
|
||||||
@@ -385,6 +391,12 @@ public sealed class UpdateSystemSettingsRequest
|
|||||||
|
|
||||||
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
|
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
|
||||||
|
|
||||||
|
/// <summary>Disk free percentage above which storage is considered healthy (Green tier). Default 30%.</summary>
|
||||||
|
public double StorageGreenThresholdPercent { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>Disk free percentage below which storage is critical (Red tier). Default 10%.</summary>
|
||||||
|
public double StorageRedThresholdPercent { get; set; } = 10;
|
||||||
|
|
||||||
public bool EnableAutoReconnect { get; set; } = true;
|
public bool EnableAutoReconnect { get; set; } = true;
|
||||||
|
|
||||||
public int ReconnectDelayMaxSeconds { get; set; } = 5;
|
public int ReconnectDelayMaxSeconds { get; set; } = 5;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using LiveRecorder.Application.Models.Cleanup;
|
|||||||
using LiveRecorder.Application.Models.Settings;
|
using LiveRecorder.Application.Models.Settings;
|
||||||
using LiveRecorder.Domain.Entities;
|
using LiveRecorder.Domain.Entities;
|
||||||
using LiveRecorder.Domain.Enums;
|
using LiveRecorder.Domain.Enums;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LiveRecorder.Application.Services;
|
namespace LiveRecorder.Application.Services;
|
||||||
@@ -25,6 +26,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
|||||||
private const string EnableStorageGuardKey = "storage.guard.enabled";
|
private const string EnableStorageGuardKey = "storage.guard.enabled";
|
||||||
private const string PauseRecordingWhenFreeSpaceBelowMegabytesKey = "storage.guard.pause_recording_below_mb";
|
private const string PauseRecordingWhenFreeSpaceBelowMegabytesKey = "storage.guard.pause_recording_below_mb";
|
||||||
private const string ResumeRecordingWhenFreeSpaceAboveMegabytesKey = "storage.guard.resume_recording_above_mb";
|
private const string ResumeRecordingWhenFreeSpaceAboveMegabytesKey = "storage.guard.resume_recording_above_mb";
|
||||||
|
private const string StorageGreenThresholdPercentKey = "storage.guard.green_threshold_percent";
|
||||||
|
private const string StorageRedThresholdPercentKey = "storage.guard.red_threshold_percent";
|
||||||
private const string EnableReconnectKey = "recording.enable_auto_reconnect";
|
private const string EnableReconnectKey = "recording.enable_auto_reconnect";
|
||||||
private const string ReconnectDelayMaxSecondsKey = "recording.reconnect_delay_max_seconds";
|
private const string ReconnectDelayMaxSecondsKey = "recording.reconnect_delay_max_seconds";
|
||||||
private const string ReadWriteTimeoutMillisecondsKey = "recording.read_write_timeout_milliseconds";
|
private const string ReadWriteTimeoutMillisecondsKey = "recording.read_write_timeout_milliseconds";
|
||||||
@@ -140,6 +143,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
|||||||
EnableStorageGuard = bool.TryParse(GetValue(lookup, EnableStorageGuardKey, "true"), out var enableStorageGuard) && enableStorageGuard,
|
EnableStorageGuard = bool.TryParse(GetValue(lookup, EnableStorageGuardKey, "true"), out var enableStorageGuard) && enableStorageGuard,
|
||||||
PauseRecordingWhenFreeSpaceBelowMegabytes = GetIntValue(lookup, PauseRecordingWhenFreeSpaceBelowMegabytesKey, 1024, 0, 1048576),
|
PauseRecordingWhenFreeSpaceBelowMegabytes = GetIntValue(lookup, PauseRecordingWhenFreeSpaceBelowMegabytesKey, 1024, 0, 1048576),
|
||||||
ResumeRecordingWhenFreeSpaceAboveMegabytes = GetIntValue(lookup, ResumeRecordingWhenFreeSpaceAboveMegabytesKey, 4096, 0, 1048576),
|
ResumeRecordingWhenFreeSpaceAboveMegabytes = GetIntValue(lookup, ResumeRecordingWhenFreeSpaceAboveMegabytesKey, 4096, 0, 1048576),
|
||||||
|
StorageGreenThresholdPercent = GetDoubleValue(lookup, StorageGreenThresholdPercentKey, 30, 5, 90),
|
||||||
|
StorageRedThresholdPercent = GetDoubleValue(lookup, StorageRedThresholdPercentKey, 10, 1, 85),
|
||||||
EnableAutoReconnect = bool.TryParse(GetValue(lookup, EnableReconnectKey, "true"), out var enableReconnect) && enableReconnect,
|
EnableAutoReconnect = bool.TryParse(GetValue(lookup, EnableReconnectKey, "true"), out var enableReconnect) && enableReconnect,
|
||||||
ReconnectDelayMaxSeconds = GetIntValue(lookup, ReconnectDelayMaxSecondsKey, 5, 1, 300),
|
ReconnectDelayMaxSeconds = GetIntValue(lookup, ReconnectDelayMaxSecondsKey, 5, 1, 300),
|
||||||
ReadWriteTimeoutMilliseconds = GetIntValue(lookup, ReadWriteTimeoutMillisecondsKey, 15000000, 1000, 60000000),
|
ReadWriteTimeoutMilliseconds = GetIntValue(lookup, ReadWriteTimeoutMillisecondsKey, 15000000, 1000, 60000000),
|
||||||
@@ -307,6 +312,16 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
|||||||
Math.Clamp(request.ResumeRecordingWhenFreeSpaceAboveMegabytes, 0, 1048576).ToString(),
|
Math.Clamp(request.ResumeRecordingWhenFreeSpaceAboveMegabytes, 0, 1048576).ToString(),
|
||||||
now,
|
now,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
await UpsertAsync(
|
||||||
|
StorageGreenThresholdPercentKey,
|
||||||
|
Math.Clamp(request.StorageGreenThresholdPercent, 5, 90).ToString("F1", CultureInfo.InvariantCulture),
|
||||||
|
now,
|
||||||
|
cancellationToken);
|
||||||
|
await UpsertAsync(
|
||||||
|
StorageRedThresholdPercentKey,
|
||||||
|
Math.Clamp(request.StorageRedThresholdPercent, 1, 85).ToString("F1", CultureInfo.InvariantCulture),
|
||||||
|
now,
|
||||||
|
cancellationToken);
|
||||||
await UpsertAsync(EnableReconnectKey, request.EnableAutoReconnect.ToString(), now, cancellationToken);
|
await UpsertAsync(EnableReconnectKey, request.EnableAutoReconnect.ToString(), now, cancellationToken);
|
||||||
await UpsertAsync(ReconnectDelayMaxSecondsKey, request.ReconnectDelayMaxSeconds.ToString(), now, cancellationToken);
|
await UpsertAsync(ReconnectDelayMaxSecondsKey, request.ReconnectDelayMaxSeconds.ToString(), now, cancellationToken);
|
||||||
await UpsertAsync(ReadWriteTimeoutMillisecondsKey, request.ReadWriteTimeoutMilliseconds.ToString(), now, cancellationToken);
|
await UpsertAsync(ReadWriteTimeoutMillisecondsKey, request.ReadWriteTimeoutMilliseconds.ToString(), now, cancellationToken);
|
||||||
@@ -466,6 +481,22 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
|||||||
return Math.Clamp(parsedValue, minimum, maximum);
|
return Math.Clamp(parsedValue, minimum, maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double GetDoubleValue(
|
||||||
|
IReadOnlyDictionary<string, string> lookup,
|
||||||
|
string key,
|
||||||
|
double fallback,
|
||||||
|
double minimum,
|
||||||
|
double maximum)
|
||||||
|
{
|
||||||
|
var raw = GetValue(lookup, key, fallback.ToString(CultureInfo.InvariantCulture));
|
||||||
|
if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(parsedValue, minimum, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<int> GetIntListValue(IReadOnlyDictionary<string, string> lookup, string key)
|
private static IReadOnlyList<int> GetIntListValue(IReadOnlyDictionary<string, string> lookup, string key)
|
||||||
{
|
{
|
||||||
if (!lookup.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
if (!lookup.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ namespace LiveRecorder.Infrastructure.Services;
|
|||||||
public sealed class StorageGuardService : IStorageGuardService
|
public sealed class StorageGuardService : IStorageGuardService
|
||||||
{
|
{
|
||||||
private const long Megabyte = 1024L * 1024L;
|
private const long Megabyte = 1024L * 1024L;
|
||||||
private const double GreenThresholdPercent = 30.0;
|
|
||||||
private const double YellowThresholdPercent = 10.0;
|
|
||||||
private readonly ILogger<StorageGuardService> _logger;
|
private readonly ILogger<StorageGuardService> _logger;
|
||||||
|
|
||||||
public StorageGuardService(ILogger<StorageGuardService> logger)
|
public StorageGuardService(ILogger<StorageGuardService> logger)
|
||||||
@@ -46,13 +44,16 @@ public sealed class StorageGuardService : IStorageGuardService
|
|||||||
var usagePercent = totalBytes > 0 ? (double)usedBytes / totalBytes * 100.0 : 0;
|
var usagePercent = totalBytes > 0 ? (double)usedBytes / totalBytes * 100.0 : 0;
|
||||||
var freePercent = 100.0 - usagePercent;
|
var freePercent = 100.0 - usagePercent;
|
||||||
|
|
||||||
// Determine tier
|
// Determine tier using configurable thresholds
|
||||||
|
var greenThreshold = Math.Clamp(settings.StorageGreenThresholdPercent, 5, 90);
|
||||||
|
var redThreshold = Math.Clamp(settings.StorageRedThresholdPercent, 1, greenThreshold - 1);
|
||||||
|
|
||||||
StorageTier tier;
|
StorageTier tier;
|
||||||
if (freePercent >= GreenThresholdPercent)
|
if (freePercent >= greenThreshold)
|
||||||
{
|
{
|
||||||
tier = StorageTier.Green;
|
tier = StorageTier.Green;
|
||||||
}
|
}
|
||||||
else if (freePercent >= YellowThresholdPercent)
|
else if (freePercent >= redThreshold)
|
||||||
{
|
{
|
||||||
tier = StorageTier.Yellow;
|
tier = StorageTier.Yellow;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user