178 lines
7.3 KiB
C#
178 lines
7.3 KiB
C#
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);
|
|
|
|
/// <summary>从原始块现算的查询全局最多同时 2 个。</summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>按数据源计划读取一条序列的累加器行(热层、10s/1m/5m/1h 层,或从原始块现算)。</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 这条序列最后一个样本的时刻:先看热层(1 秒精度,保留 1 小时),没有再看 1 分钟层最近 7 天(精度是窗口宽度)。没有任何数据返回 null。
|
|
/// </summary>
|
|
public async ValueTask<long?> 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;
|
|
}
|
|
|
|
IReadOnlyList<MetricRow> rows = await _metrics.ReadAsync(MetricLevel.M1, agent, series, nowMs - (7 * Steps.Day), nowMs + 60_000, ct);
|
|
return rows.Count == 0 ? null : rows.Max(r => r.WindowStart) + 60_000;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 这条序列最早的数据在什么时候:各层(10 秒、1 分钟、5 分钟、1 小时)里最早的窗口,加上热层里还没落到持久层的部分。
|
|
/// 展示页据此只提供“有数据”的时间范围。没有任何数据返回 null。
|
|
/// </summary>
|
|
public async ValueTask<long?> 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 = earliest is null ? at : Math.Min(earliest.Value, 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 = earliest is null ? h : Math.Min(earliest.Value, h);
|
|
}
|
|
|
|
return earliest;
|
|
}
|
|
|
|
public async ValueTask<Result<IReadOnlyList<SourceRow>>> LoadAsync(SourcePlan plan, QueryRange range, NodeId agent, string series, CancellationToken ct)
|
|
{
|
|
switch (plan.Source)
|
|
{
|
|
case DataSource.Hot:
|
|
return Result<IReadOnlyList<SourceRow>>.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<IReadOnlyList<SourceRow>>.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<IReadOnlyList<SourceRow>>.Ok([.. computed.Buckets.Select(b => new SourceRow(b.Key, b.Value, computed.AnyPartial))]);
|
|
}
|
|
finally
|
|
{
|
|
_gate.Exit();
|
|
}
|
|
|
|
default:
|
|
IReadOnlyList<MetricRow> rows = await _metrics.ReadAsync(ToLevel(plan.Source), agent, series, range.FromMs, range.ToMs, ct);
|
|
return Result<IReadOnlyList<SourceRow>>.Ok(Decode(rows));
|
|
}
|
|
}
|
|
|
|
/// <summary>多条线路一起读取(首页排行)。热层与原始块现算逐条读取。</summary>
|
|
public async ValueTask<Result<Dictionary<LineKey, IReadOnlyList<SourceRow>>>> LoadManyAsync(
|
|
SourcePlan plan, QueryRange range, IReadOnlyCollection<LineKey> lines, CancellationToken ct)
|
|
{
|
|
var result = new Dictionary<LineKey, IReadOnlyList<SourceRow>>();
|
|
if (plan.Source is DataSource.Hot or DataSource.Raw)
|
|
{
|
|
foreach (LineKey line in lines)
|
|
{
|
|
Result<IReadOnlyList<SourceRow>> one = await LoadAsync(plan, range, line.Agent, line.Series, ct);
|
|
if (!one.TryGet(out IReadOnlyList<SourceRow>? rows, out FleetError? error))
|
|
{
|
|
return Result<Dictionary<LineKey, IReadOnlyList<SourceRow>>>.Fail(error);
|
|
}
|
|
|
|
result[line] = rows;
|
|
}
|
|
|
|
return Result<Dictionary<LineKey, IReadOnlyList<SourceRow>>>.Ok(result);
|
|
}
|
|
|
|
// 每个 Probe 一个序列:按序列分组,每组一次查询
|
|
foreach (IGrouping<string, LineKey> bySeries in lines.GroupBy(l => l.Series))
|
|
{
|
|
NodeId[] agents = [.. bySeries.Select(l => l.Agent).Distinct()];
|
|
IReadOnlyList<MetricRow> all = await _metrics.ReadAgentsAsync(ToLevel(plan.Source), agents, bySeries.Key, range.FromMs, range.ToMs, ct);
|
|
foreach (IGrouping<NodeId, MetricRow> group in all.GroupBy(r => r.AgentId))
|
|
{
|
|
LineKey key = bySeries.First(l => l.Agent == group.Key);
|
|
result[key] = Decode([.. group]);
|
|
}
|
|
}
|
|
|
|
foreach (LineKey line in lines)
|
|
{
|
|
result.TryAdd(line, []);
|
|
}
|
|
|
|
return Result<Dictionary<LineKey, IReadOnlyList<SourceRow>>>.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<SourceRow> Decode(IReadOnlyList<MetricRow> rows)
|
|
{
|
|
var result = new List<SourceRow>(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;
|
|
}
|
|
}
|