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.
This commit is contained in:
justme committed 2026-10-21 11:55:36 +08:00
1 parent d5ebf29e57
commit ad5efc62ff
206 files changed
+4202 -4964

No files matched your search

@@ -10,8 +10,8 @@ public interface IObservationSink
}
/// <summary>
/// 原始块的列视图:按列名读取,并把 Probe / Agent 各自的单调时钟换算成 UTC。
/// Agent 时钟 → UTC 使用块头的锚点;Probe 时钟 → Agent 时钟使用估计的偏移。
/// 原始块的列视图:按列名读取,并把 拨号方 / 监听方 各自的单调时钟换算成 UTC。
/// Agent 时钟 → UTC 使用块头的锚点;拨号方 时钟 → Agent 时钟使用估计的偏移。
/// </summary>
public sealed class ChunkView
{
@@ -24,11 +24,11 @@ public sealed class ChunkView
public RawChunkHeader Header => Chunk.Header;
public long AgentToUtcMs(long agentUs) => Header.Clock.UtcMs + FloorDiv(agentUs - Header.Clock.AgentMonoUs, 1000);
public long AgentToUtcMs(long agentUs) => Header.Clock.UtcMs + FloorDiv(agentUs - Header.Clock.ListenerMonoUs, 1000);
public long ProbeToUtcMs(long probeUs) => AgentToUtcMs(probeUs + Header.Clock.ProbeOffsetUs);
public long DialerToUtcMs(long dialerUs) => AgentToUtcMs(dialerUs + Header.Clock.DialerOffsetUs);
public long ColumnToUtcMs(RawColumn column, long value) => column.Role == ColumnRole.TsProbe ? ProbeToUtcMs(value) : AgentToUtcMs(value);
public long ColumnToUtcMs(RawColumn column, long value) => column.Role == ColumnRole.TsDialer ? DialerToUtcMs(value) : AgentToUtcMs(value);
public double ParamNumber(string name, double fallback) =>
Header.Params.TryGetValue(name, out JsonElement v) && v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out double d) ? d : fallback;
@@ -142,7 +142,7 @@ public static class TestEvaluator
step.JitterP95Ms = jh.Quantile(0.95);
}
// 阶段的时长:从发送时间戳算(块头里的时长是 Probe 填的,每个块可能都带着整个阶段的时长,不能相加也不能取并集)
// 阶段的时长:从发送时间戳算(块头里的时长是 拨号方 填的,每个块可能都带着整个阶段的时长,不能相加也不能取并集)
double spanSeconds = SendSpanSeconds(parts) ?? ((parts.Max(c => c.Header.StartUtcMs + c.Header.DurationMs) - parts.Min(c => c.Header.StartUtcMs)) / 1000.0);
step.DeliveredMbps = spanSeconds > 0 ? bytes * 8 / spanSeconds / 1_000_000 : 0;
steps.Add(step);
@@ -215,7 +215,7 @@ public static class TestEvaluator
continue;
}
long utcUs = (clock.UtcMs * 1000) + (send.Values[i] - clock.AgentMonoUs);
long utcUs = (clock.UtcMs * 1000) + (send.Values[i] - clock.ListenerMonoUs);
min = Math.Min(min, utcUs);
max = Math.Max(max, utcUs);
count++;
@@ -11,10 +11,10 @@ public static class LinkMetrics
[Stat.P50, Stat.P95, Stat.P99, Stat.Mean], good: 50, poor: 150);
public static readonly MetricDef LossUp = Metric.Ratio(
"loss.up", s_udpMsg, Ops.Loss(Dir.Up), "Loss (up)", "Packets lost between the Probe and the Agent.", good: 0.5, poor: 3);
"loss.up", s_udpMsg, Ops.Loss(Dir.Up), "Loss (up)", "Packets lost between the 拨号方 and the Agent.", good: 0.5, poor: 3);
public static readonly MetricDef LossDown = Metric.Ratio(
"loss.down", s_udpMsg, Ops.Loss(Dir.Down), "Loss (down)", "Replies lost between the Agent and the Probe, among packets that reached the Agent.", good: 0.5, poor: 3);
"loss.down", s_udpMsg, Ops.Loss(Dir.Down), "Loss (down)", "Replies lost between the Agent and the 拨号方, among packets that reached the Agent.", good: 0.5, poor: 3);
public static readonly MetricDef LossTotal = Metric.Ratio(
"loss.total", s_udpOrIcmpMsg, Ops.Loss(Dir.Total), "Loss (total)", "Packets whose reply never came back.", good: 0.5, poor: 3);
@@ -31,11 +31,11 @@ public static class LinkMetrics
new MetricExport("loss.run_mean", Stat.Mean, Unit.Packets));
public static readonly MetricDef LossRunUp = Metric.Max(
"loss.run.up", s_udpMsg, Ops.LossRunMax(Dir.Up), "Upstream longest loss run", "Longest consecutive Probe-to-Agent Datagram loss run in each chunk; zero means no loss.",
"loss.run.up", s_udpMsg, Ops.LossRunMax(Dir.Up), "Upstream longest loss run", "Longest consecutive 拨号方-to-Agent Datagram loss run in each chunk; zero means no loss.",
Unit.Packets, "loss.run.up_max", good: 0, poor: 10);
public static readonly MetricDef LossRunDown = Metric.Max(
"loss.run.down", s_udpMsg, Ops.LossRunMax(Dir.Down), "Downstream longest loss run", "Longest consecutive Agent-to-Probe Datagram loss run in each chunk, conditioned on the request reaching the Agent; zero means no loss.",
"loss.run.down", s_udpMsg, Ops.LossRunMax(Dir.Down), "Downstream longest loss run", "Longest consecutive Agent-to-拨号方 Datagram loss run in each chunk, conditioned on the request reaching the Agent; zero means no loss.",
Unit.Packets, "loss.run.down_max", good: 0, poor: 10);
public static readonly MetricDef Reorder = Metric.Counter(
@@ -15,7 +15,7 @@ public enum GapOutput
MaxMs,
}
/// <summary>回显流(stream <c>msg</c>)的列:t1/t4 是 Probe 时钟,t2/t3 是 Agent 时钟;缺失的 t2 表示上行丢失,缺失的 t4 表示下行丢失。</summary>
/// <summary>回显流(stream <c>msg</c>)的列:t1/t4 是 拨号方 时钟,t2/t3 是 Agent 时钟;缺失的 t2 表示上行丢失,缺失的 t4 表示下行丢失。</summary>
internal readonly struct EchoColumns
{
public EchoColumns(RawStream stream)
@@ -2,11 +2,11 @@ using MoShiro.Protocol;
namespace MoShiro.Backend.Analysis;
/// <summary>tcp_info 流的辅助:<c>side</c> 为 0 表示 Probe 一侧记录(Probe 时钟),否则为 Agent 一侧。</summary>
/// <summary>tcp_info 流的辅助:<c>side</c> 为 0 表示 拨号方 一侧记录(拨号方 时钟),否则为 Agent 一侧。</summary>
internal static class TcpInfoTime
{
public static long ToUtcMs(ChunkView view, RawColumn ts, RawColumn? side, int row) =>
side is not null && side.Values[row] == 0 ? view.ProbeToUtcMs(ts.Values[row]) : view.AgentToUtcMs(ts.Values[row]);
side is not null && side.Values[row] == 0 ? view.DialerToUtcMs(ts.Values[row]) : view.AgentToUtcMs(ts.Values[row]);
}
/// <summary>两次采样之间累计计数的差值(分子)与另一个累计计数的差值(分母),按 (side, conn) 分别计算;旧数据没有 conn 时按 side 计算。</summary>
@@ -164,7 +164,7 @@ internal sealed class BulkRateOp : MetricOp
}
double mbps = total * 8 / (width * sampleMs / 1000.0) / 1_000_000;
sink.Emit(view.ProbeToUtcMs(bin * binUs), mbps);
sink.Emit(view.DialerToUtcMs(bin * binUs), mbps);
}
}
}
@@ -332,7 +332,7 @@ internal sealed class TrainLossOp : MetricOp
}
}
/// <summary>单向时延变化:t_recv(Probe) − t_send(Agent),去除偏移和漂移。</summary>
/// <summary>单向时延变化:t_recv(拨号方) − t_send(Agent),去除偏移和漂移。</summary>
internal sealed class TrainJitterOp : MetricOp
{
public override string Signature => "train_jitter/1";