using Fleet.Core; using MoShiro.Backend.Analysis; using MoShiro.Protocol; namespace MoShiro.Backend.Application; public readonly record struct SourceRow(long Start, MetricSet Set, bool Partial); /// 从原始块现算的查询全局最多同时 2 个。 public sealed class RawQueryGate : IDisposable { private readonly SemaphoreSlim _slots = new(2, 2); public bool TryEnter() => _slots.Wait(0); public void Exit() => _slots.Release(); public void Dispose() => _slots.Dispose(); } /// 按数据源计划读取一条序列的累加器行(热层、10s/1m/5m/1h 层,或从原始块现算)。 public sealed class RowLoader { private readonly IMetricStore _metrics; private readonly HotLayer _hot; private readonly WindowComputer _computer; private readonly RawQueryGate _gate; public RowLoader(IMetricStore metrics, HotLayer hot, WindowComputer computer, RawQueryGate gate) { _metrics = metrics; _hot = hot; _computer = computer; _gate = gate; } /// /// 这条序列最后一个样本的时刻:先看热层(1 秒精度,保留 1 小时),没有再看 1 分钟层最近 7 天(精度是窗口宽度)。没有任何数据返回 null。 /// public async ValueTask LatestSampleMsAsync(NodeId agent, string series, long nowMs, CancellationToken ct) { long? hot = _hot.GetRange(agent, series, nowMs - Steps.Hour, nowMs + 10_000).Select(r => (long?)r.Key).Max(); if (hot is { } at) { return at + 1000; } // 热层空了(Backend 刚重启、热层还没攒起来)就看持久层:各层里最晚的窗口取最大。只看 1 分钟层会落后——它比 5 分钟层更新得慢时, // 窗口就被定在好几个小时以前,页面显示的全是旧数据。 long? latest = null; foreach (MetricLevel level in new[] { MetricLevel.S10, MetricLevel.M1, MetricLevel.M5 }) { if (await _metrics.LatestAsync(level, agent, series, ct) is { } start) { long end = Math.Min(start + level.WidthMs(), nowMs); latest = latest is null ? end : Math.Max(latest.Value, end); } } return latest; } /// /// 这条序列最早的数据在什么时候:各层(10 秒、1 分钟、5 分钟、1 小时)里最早的窗口,加上热层里还没落到持久层的部分。 /// 展示页据此只提供“有数据”的时间范围。没有任何数据返回 null。 /// public async ValueTask EarliestSampleMsAsync(NodeId agent, string series, long nowMs, CancellationToken ct) { // 从粗到细:粗的层窗口很宽(最宽 1 小时),起点会比真实的第一个样本早; // 如果更细的层的起点落在粗层第一个窗口里面,就用更细的(更准确);落在窗口之外说明更早的数据只剩粗层里有。 long? earliest = null; long width = 0; foreach (MetricLevel level in new[] { MetricLevel.H1, MetricLevel.M5, MetricLevel.M1, MetricLevel.S10 }) { if (await _metrics.EarliestAsync(level, agent, series, ct) is not { } at) { continue; } if (earliest is null || at < earliest.Value + width) { earliest = at; width = level.WidthMs(); } } long? hot = _hot.GetRange(agent, series, nowMs - Steps.Hour, nowMs + 10_000).Select(r => (long?)r.Key).Min(); if (hot is { } h && (earliest is null || h < earliest.Value + width)) { earliest = h; } return earliest; } public async ValueTask>> LoadAsync(SourcePlan plan, QueryRange range, NodeId agent, string series, CancellationToken ct) { switch (plan.Source) { case DataSource.Hot: return Result>.Ok([.. _hot.GetRange(agent, series, range.FromMs, range.ToMs).Select(r => new SourceRow(r.Key, r.Value, false))]); case DataSource.Raw: if (!_gate.TryEnter()) { return Result>.Fail(MoShiroErrors.TooManyRequests, "too many queries are reading raw data; retry shortly or use a coarser step"); } try { WindowComputer.Computed computed = await _computer.ComputeAsync(agent, series, range.FromMs, range.ToMs, plan.StepMs, null, ct); return Result>.Ok([.. computed.Buckets.Select(b => new SourceRow(b.Key, b.Value, computed.AnyPartial))]); } finally { _gate.Exit(); } default: IReadOnlyList rows = await _metrics.ReadAsync(ToLevel(plan.Source), agent, series, range.FromMs, range.ToMs, ct); return Result>.Ok(Decode(rows)); } } /// 多条线路一起读取(首页排行)。热层与原始块现算逐条读取。 public async ValueTask>>> LoadManyAsync( SourcePlan plan, QueryRange range, IReadOnlyCollection lines, CancellationToken ct) { var result = new Dictionary>(); if (plan.Source is DataSource.Hot or DataSource.Raw) { foreach (LineKey line in lines) { Result> one = await LoadAsync(plan, range, line.Listener, line.Series, ct); if (!one.TryGet(out IReadOnlyList? rows, out FleetError? error)) { return Result>>.Fail(error); } result[line] = rows; } return Result>>.Ok(result); } // 每个拨号方一个序列:按序列分组,每组一次查询 foreach (IGrouping bySeries in lines.GroupBy(l => l.Series)) { NodeId[] agents = [.. bySeries.Select(l => l.Listener).Distinct()]; IReadOnlyList all = await _metrics.ReadAgentsAsync(ToLevel(plan.Source), agents, bySeries.Key, range.FromMs, range.ToMs, ct); foreach (IGrouping group in all.GroupBy(r => r.AgentId)) { LineKey key = bySeries.First(l => l.Listener == group.Key); result[key] = Decode([.. group]); } } foreach (LineKey line in lines) { result.TryAdd(line, []); } return Result>>.Ok(result); } private static MetricLevel ToLevel(DataSource source) => source switch { DataSource.S10 => MetricLevel.S10, DataSource.M1 => MetricLevel.M1, DataSource.M5 => MetricLevel.M5, _ => MetricLevel.H1, }; private static List Decode(IReadOnlyList rows) { var result = new List(rows.Count); foreach (MetricRow row in rows) { // 损坏的行跳过(体现为缺口),不让一行坏数据拖垮整个查询 if (AccumulatorCodec.Decode(row.Metrics).TryGet(out MetricSet? set, out _)) { result.Add(new SourceRow(row.WindowStart, set, row.Partial)); } } return result; } }