Files
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

188 lines
7.6 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;
}
// 热层空了(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;
}
/// <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 = 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<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.Listener, 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);
}
// 每个拨号方一个序列:按序列分组,每组一次查询
foreach (IGrouping<string, LineKey> bySeries in lines.GroupBy(l => l.Series))
{
NodeId[] agents = [.. bySeries.Select(l => l.Listener).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.Listener == 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;
}
}