using Fleet.Core; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MoShiro.Backend.Analysis; namespace MoShiro.Backend.Application; /// /// 持久路径:分钟窗口就绪(窗口结束 + 宽限,或迟到的块到达)之后从原始块计算 → 写入 metric_1m → 标记降采样脏桶; /// 然后把脏桶合并成 5 分钟、1 小时层。 /// public sealed partial class AnalysisScheduler : BackgroundService { private const int BatchSize = 50; private readonly IMetricStore _metrics; private readonly WindowComputer _computer; private readonly BackendOptions _options; private readonly BackendCounters _counters; private readonly TimeProvider _time; private readonly ILogger _logger; public AnalysisScheduler(IMetricStore metrics, WindowComputer computer, BackendOptions options, BackendCounters counters, TimeProvider time, ILogger logger) { _metrics = metrics; _computer = computer; _options = options; _counters = counters; _time = time; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { int worked; do { worked = await RunOnceAsync(stoppingToken); } while (worked >= BatchSize); await Task.Delay(_options.AnalysisPollInterval, _time, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return; } catch (Exception ex) { // 后台循环不能因为一次失败而退出 LogFailed(ex); await Task.Delay(_options.AnalysisPollInterval, _time, stoppingToken); } } } /// 处理一批到期的窗口和脏桶,返回处理的窗口数(也供测试直接调用)。 public async Task RunOnceAsync(CancellationToken ct) { long now = _time.GetUtcNow().ToUnixTimeMilliseconds(); IReadOnlyList due = await _metrics.DequeueDueAsync(now, BatchSize, ct); foreach (QueueItem item in due) { await ComputeWindowAsync(item, ct); _counters.CountWindowAnalyzed(); } if (due.Count > 0) { await _metrics.CompleteAsync(due, ct); } await RollupAsync(ct); return due.Count; } private async ValueTask ComputeWindowAsync(QueueItem item, CancellationToken ct) { long width = MetricLevel.M1.WidthMs(); WindowComputer.Computed computed = await _computer.ComputeAsync(item.AgentId, item.Series, item.WindowStart, item.WindowStart + width, width, null, ct); if (!computed.Buckets.TryGetValue(item.WindowStart, out MetricSet? set)) { return; } await _metrics.UpsertAsync(MetricLevel.M1, [new MetricRow(item.AgentId, item.Series, item.WindowStart, computed.AnyPartial, AccumulatorCodec.Encode(set))], ct); await MarkUpperLevelsAsync(item.AgentId, item.Series, item.WindowStart, ct); } internal async ValueTask MarkUpperLevelsAsync(NodeId agent, string series, long windowStart, CancellationToken ct) { await _metrics.MarkRollupDirtyAsync(MetricLevel.M5, agent, series, Floor(windowStart, MetricLevel.M5), ct); } /// 降采样:读取下一级的累加器状态,精确合并后写回。 private async ValueTask RollupAsync(CancellationToken ct) { foreach ((MetricLevel level, NodeId agent, string series, long bucketStart) in await _metrics.TakeRollupDirtyAsync(100, ct)) { MetricLevel source = level == MetricLevel.M5 ? MetricLevel.M1 : MetricLevel.M5; long end = bucketStart + level.WidthMs(); IReadOnlyList rows = await _metrics.ReadAsync(source, agent, series, bucketStart, end, ct); if (rows.Count == 0) { continue; } var merged = new MetricSet(); bool partial = false; foreach (MetricRow row in rows) { if (AccumulatorCodec.Decode(row.Metrics).TryGet(out MetricSet? set, out _)) { merged.Merge(set); partial |= row.Partial; } } await _metrics.UpsertAsync(level, [new MetricRow(agent, series, bucketStart, partial, AccumulatorCodec.Encode(merged))], ct); if (level == MetricLevel.M5) { await _metrics.MarkRollupDirtyAsync(MetricLevel.H1, agent, series, Floor(bucketStart, MetricLevel.H1), ct); } } } internal static long Floor(long ms, MetricLevel level) => Math.DivRem(ms, level.WidthMs(), out _) * level.WidthMs(); [LoggerMessage(Level = LogLevel.Error, Message = "analysis iteration failed")] private partial void LogFailed(Exception exception); }