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;
}
}