diff --git a/backend/src/MoShiro.Backend.Analysis/Engine/TestEvaluator.cs b/backend/src/MoShiro.Backend.Analysis/Engine/TestEvaluator.cs
index 05c769d..1d78a4d 100644
--- a/backend/src/MoShiro.Backend.Analysis/Engine/TestEvaluator.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Engine/TestEvaluator.cs
@@ -31,7 +31,7 @@ public sealed class TestResult
///
/// 测试结果的计算:所有阶段的块到达之后,按方案算出指标和曲线。
-/// udp_train 的各个阶段作为码率阶梯:可持续码率、有效吞吐、限速开始的位置。
+/// quic_stream 的各个阶段作为码率阶梯:可持续码率、有效吞吐、限速开始的位置。
///
public static class TestEvaluator
{
@@ -43,7 +43,7 @@ public static class TestEvaluator
{
var result = new TestResult();
EvaluateMetrics(chunks, result);
- if (chunks.Any(c => c.Header.Kind == Kinds.UdpTrain.Id))
+ if (chunks.Any(c => c.Header.Kind == Kinds.QuicStream.Id))
{
EvaluateLadder(chunks, result);
}
@@ -95,7 +95,7 @@ public static class TestEvaluator
// 一个阶段(一档码率)可能被切成好几个块(一个块最多 50000 条记录,100 Mbit/s 的 10 秒要切 3~4 块):
// 阶梯里的一步是一个阶段,不是一个块——同一阶段的块先合并,再算丢包、抖动和实际吞吐。
- foreach (IGrouping phase in chunks.Where(c => c.Header.Kind == Kinds.UdpTrain.Id).GroupBy(c => c.Header.Phase ?? 0).OrderBy(g => g.Key))
+ foreach (IGrouping phase in chunks.Where(c => c.Header.Kind == Kinds.QuicStream.Id).GroupBy(c => c.Header.Phase ?? 0).OrderBy(g => g.Key))
{
RawChunk[] parts = [.. phase.Where(c => c.Find("pkt") is not null).OrderBy(c => c.Header.StartUtcMs)];
if (parts.Length == 0)
diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/HttpMetrics.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/HttpMetrics.cs
new file mode 100644
index 0000000..eb9e448
--- /dev/null
+++ b/backend/src/MoShiro.Backend.Analysis/Metrics/HttpMetrics.cs
@@ -0,0 +1,48 @@
+namespace MoShiro.Backend.Analysis;
+
+/// http 任务(单个 Agent 请求一个 URL)的指标。耗时只统计成功的请求。
+public static class HttpMetrics
+{
+ private static readonly string[] s_req = ["http_request.req"];
+
+ public static readonly MetricDef Success = new(
+ "http.success", AccKind.Ratio, s_req, Ops.HttpSuccess, "HTTP success", "Share of requests that got an answer with an accepted status.", Better.Higher,
+ [new MetricExport("http.success", Stat.Pct, Unit.Pct, 99, 90)]);
+
+ public static readonly MetricDef Dns = Metric.Histogram(
+ "http.dns_ms", s_req, Ops.HttpPhaseMs("dns_us"), "DNS time", "Time to resolve the target host name.", Unit.Ms, [Stat.P95], good: 50, poor: 300);
+
+ public static readonly MetricDef Tcp = Metric.Histogram(
+ "http.tcp_ms", s_req, Ops.HttpPhaseMs("tcp_us"), "TCP connect time", "Time to establish the TCP connection (HTTP/1.1 and HTTP/2).", Unit.Ms, [Stat.P95], good: 100, poor: 400);
+
+ public static readonly MetricDef Ttfb = Metric.Histogram(
+ "http.ttfb_ms", s_req, Ops.HttpPhaseMs("ttfb_us"), "Time to first byte", "Time from starting the request until the response headers arrive, including connection setup.", Unit.Ms,
+ [Stat.P50, Stat.P95, Stat.P99], good: 300, poor: 1000);
+
+ public static readonly MetricDef Total = Metric.Histogram(
+ "http.total_ms", s_req, Ops.HttpPhaseMs("total_us"), "Total time", "Time until the response body was read.", Unit.Ms, [Stat.P50, Stat.P95, Stat.P99], good: 500, poor: 2000);
+
+ public static readonly MetricDef FailTimeout = Metric.Counter(
+ "http.fail.timeout", s_req, Ops.HttpOutcomeCount(HttpOutcome.Timeout), "HTTP timeouts", "Requests that timed out.", new MetricExport("http.fail.timeout", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailDns = Metric.Counter(
+ "http.fail.dns", s_req, Ops.HttpOutcomeCount(HttpOutcome.Dns), "DNS failures", "Requests whose host name did not resolve.", new MetricExport("http.fail.dns", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailReset = Metric.Counter(
+ "http.fail.rst", s_req, Ops.HttpOutcomeCount(HttpOutcome.Reset), "HTTP resets", "Requests answered with a reset or refused.", new MetricExport("http.fail.rst", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailTls = Metric.Counter(
+ "http.fail.tls", s_req, Ops.HttpOutcomeCount(HttpOutcome.Tls), "TLS failures", "Requests that ended in a TLS error.", new MetricExport("http.fail.tls", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailStatus = Metric.Counter(
+ "http.fail.status", s_req, Ops.HttpOutcomeCount(HttpOutcome.BadStatus), "Bad status", "Requests that got an answer with an unaccepted status.", new MetricExport("http.fail.status", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailUnsupported = Metric.Counter(
+ "http.fail.unsupported", s_req, Ops.HttpOutcomeCount(HttpOutcome.Unsupported), "Unsupported version", "Requests that could not use the requested HTTP version (for HTTP/3: no QUIC library, or the server does not offer it).", new MetricExport("http.fail.unsupported", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef FailOther = Metric.Counter(
+ "http.fail.other", s_req, Ops.HttpOutcomeCount(HttpOutcome.Other), "Other failures", "Requests that failed for a reason not listed separately.", new MetricExport("http.fail.other", Stat.Count, Unit.Count));
+
+ public static readonly MetricDef Samples = Metric.Counter(
+ "n.http", s_req, Ops.RowCount, "http samples", "Number of http requests.", new MetricExport("n.http", Stat.Count, Unit.Count), Better.None);
+}
diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/LinkMetrics.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/LinkMetrics.cs
index 403b2bf..3f4908b 100644
--- a/backend/src/MoShiro.Backend.Analysis/Metrics/LinkMetrics.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Metrics/LinkMetrics.cs
@@ -1,10 +1,10 @@
namespace MoShiro.Backend.Analysis;
-/// 线路基础指标(udp_echo / icmp_echo 的回显流)。新增指标 = 加一行声明。
+/// 线路基础指标(quic_ping / icmp_ping 的回显流)。新增指标 = 加一行声明。
public static class LinkMetrics
{
- private static readonly string[] s_udpMsg = ["udp_echo.msg"];
- private static readonly string[] s_udpOrIcmpMsg = ["udp_echo.msg", "icmp_echo.msg"];
+ private static readonly string[] s_udpMsg = ["quic_ping.msg"];
+ private static readonly string[] s_udpOrIcmpMsg = ["quic_ping.msg", "icmp_ping.msg"];
public static readonly MetricDef Rtt = Metric.Histogram(
"rtt", s_udpOrIcmpMsg, Ops.Rtt, "RTT", "Round-trip time with the Agent processing time removed.", Unit.Ms,
@@ -45,5 +45,5 @@ public static class LinkMetrics
"dup", s_udpMsg, Ops.Dup, "Duplicates", "Replies received more than once.", new MetricExport("dup", Stat.Count, Unit.Count));
public static readonly MetricDef SamplesUdp = Metric.Counter(
- "n.udp_echo", s_udpMsg, Ops.RowCount, "udp_echo samples", "Number of udp_echo records.", new MetricExport("n.udp_echo", Stat.Count, Unit.Count), Better.None);
+ "n.quic_ping", s_udpMsg, Ops.RowCount, "quic_ping samples", "Number of quic_ping records.", new MetricExport("n.quic_ping", Stat.Count, Unit.Count), Better.None);
}
diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs
index a0d3066..98fa9df 100644
--- a/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs
@@ -15,6 +15,8 @@ public static class MetricRegistry
TcpMetrics.Rtt, TcpMetrics.Slow, TcpMetrics.Stall, TcpMetrics.StallMax, TcpMetrics.Retrans, TcpMetrics.RetransUp, TcpMetrics.RetransDown, TcpMetrics.Srtt, TcpMetrics.BulkRate, TcpMetrics.BulkPeak, TcpMetrics.SamplesTcp,
SessionMetrics.ConnectSuccess, SessionMetrics.ConnectTcp, SessionMetrics.ConnectTls,
SessionMetrics.FailTimeout, SessionMetrics.FailReset, SessionMetrics.FailTls,
+ HttpMetrics.Success, HttpMetrics.Dns, HttpMetrics.Tcp, HttpMetrics.Ttfb, HttpMetrics.Total,
+ HttpMetrics.FailTimeout, HttpMetrics.FailDns, HttpMetrics.FailReset, HttpMetrics.FailTls, HttpMetrics.FailStatus, HttpMetrics.FailUnsupported, HttpMetrics.FailOther, HttpMetrics.Samples,
SessionMetrics.HoldResets, SessionMetrics.HoldLifetime,
SessionMetrics.TrainLoss, SessionMetrics.TrainJitter, SessionMetrics.TrainRate, SessionMetrics.SamplesConnect,
];
@@ -26,6 +28,7 @@ public static class MetricRegistry
new("udp", "QUIC Datagram (legacy alias)", ["loss.up", "loss.down", "loss.total", "rtt.p50", "rtt.p95", "rtt.p99", "jitter.up.p95", "jitter.down.p95", "loss.run_max", "reorder", "dup"]),
new("tcp", "TCP", ["tcp.rtt.p50", "tcp.rtt.p95", "tcp.rtt.p99", "tcp.rtt.p999", "tcp.rtt.max", "tcp.rtt.tail", "tcp.slow", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans", "tcp.retrans.up", "tcp.retrans.down", "tcp.srtt.p95"]),
new("connect", "Connect", ["connect.success", "connect.tcp_ms.p95", "connect.tls_ms.p95", "connect.fail.timeout", "connect.fail.rst", "connect.fail.tls"]),
+ new("http", "HTTP", ["http.success", "http.dns_ms.p95", "http.tcp_ms.p95", "http.ttfb_ms.p50", "http.ttfb_ms.p95", "http.ttfb_ms.p99", "http.total_ms.p50", "http.total_ms.p95", "http.total_ms.p99", "http.fail.timeout", "http.fail.dns", "http.fail.rst", "http.fail.tls", "http.fail.status", "http.fail.unsupported", "http.fail.other"]),
new("session", "Session", ["hold.resets.per_h", "hold.lifetime_max"]),
new("scores", "Scores", ["score.base", "score.mc", "score.hy2", "score.reality"]),
];
diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/SessionMetrics.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/SessionMetrics.cs
index 66abca1..ce7ca2b 100644
--- a/backend/src/MoShiro.Backend.Analysis/Metrics/SessionMetrics.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Metrics/SessionMetrics.cs
@@ -1,11 +1,11 @@
namespace MoShiro.Backend.Analysis;
-/// TCP 建连、长连接、QUIC Datagram 压测(legacy kind udp_train)的指标。
+/// TCP 建连、长连接、QUIC Datagram 压测(legacy kind quic_stream)的指标。
public static class SessionMetrics
{
- private static readonly string[] s_conn = ["connect.conn"];
- private static readonly string[] s_hold = ["hold.hold"];
- private static readonly string[] s_train = ["udp_train.pkt"];
+ private static readonly string[] s_conn = ["tcp_connect.conn"];
+ private static readonly string[] s_hold = ["tls_hold.hold"];
+ private static readonly string[] s_train = ["quic_stream.pkt"];
public static readonly MetricDef ConnectSuccess = new(
"connect.success", AccKind.Ratio, s_conn, Ops.OutcomeRatio(ConnectOutcome.Ok), "Connect success", "Share of connection attempts that completed.", Better.Higher,
diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs
index 1c27c3c..8b15708 100644
--- a/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs
@@ -3,9 +3,9 @@ namespace MoShiro.Backend.Analysis;
/// TCP 指标:回显的 RTT 与卡顿、内核采样的重传、批量传输的吞吐。
public static class TcpMetrics
{
- private static readonly string[] s_tcpMsg = ["tcp_echo.msg"];
- private static readonly string[] s_tcpInfo = ["tcp_echo.tcp_info", "tcp_bulk.tcp_info"];
- private static readonly string[] s_bulkBytes = ["tcp_bulk.bytes"];
+ private static readonly string[] s_tcpMsg = ["tcp_ping.msg"];
+ private static readonly string[] s_tcpInfo = ["tcp_ping.tcp_info", "tcp_transfer.tcp_info"];
+ private static readonly string[] s_bulkBytes = ["tcp_transfer.bytes"];
public static readonly MetricDef Rtt = Metric.Histogram(
"tcp.rtt", s_tcpMsg, Ops.Rtt, "TCP RTT", "Application-level echo round trip over TCP (includes retransmission delay).", Unit.Ms,
@@ -47,5 +47,5 @@ public static class TcpMetrics
[Stat.Max], Better.Higher);
public static readonly MetricDef SamplesTcp = Metric.Counter(
- "n.tcp_echo", s_tcpMsg, Ops.RowCount, "tcp_echo samples", "Number of tcp_echo records.", new MetricExport("n.tcp_echo", Stat.Count, Unit.Count), Better.None);
+ "n.tcp_ping", s_tcpMsg, Ops.RowCount, "tcp_ping samples", "Number of tcp_ping records.", new MetricExport("n.tcp_ping", Stat.Count, Unit.Count), Better.None);
}
diff --git a/backend/src/MoShiro.Backend.Analysis/Models/Models.cs b/backend/src/MoShiro.Backend.Analysis/Models/Models.cs
index 383d470..425562a 100644
--- a/backend/src/MoShiro.Backend.Analysis/Models/Models.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Models/Models.cs
@@ -32,7 +32,7 @@ public sealed class BaseModel : ServiceModelBase
///
/// Minecraft:TCP 上不会丢包,丢包表现为重传和卡顿(队头阻塞)。
-/// 没有 tcp_echo 数据时,用 udp_echo 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。
+/// 没有 tcp_ping 数据时,用 quic_ping 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。
///
public sealed class McModel : ServiceModelBase
{
@@ -99,7 +99,7 @@ public sealed class McModel : ServiceModelBase
///
/// Hysteria2(Brutal 固定码率):有 hy2_ladder 测试结果时,用可持续码率、有效吞吐和限速开始的位置评分;
-/// 没有测试时,只能用持续的 udp_echo 趋势粗略估算,并标记为 estimated。
+/// 没有测试时,只能用持续的 quic_ping 趋势粗略估算,并标记为 estimated。
///
public sealed class Hy2Model : ServiceModelBase
{
diff --git a/backend/src/MoShiro.Backend.Analysis/Ops/HttpOps.cs b/backend/src/MoShiro.Backend.Analysis/Ops/HttpOps.cs
new file mode 100644
index 0000000..7af5a80
--- /dev/null
+++ b/backend/src/MoShiro.Backend.Analysis/Ops/HttpOps.cs
@@ -0,0 +1,119 @@
+using MoShiro.Protocol;
+
+namespace MoShiro.Backend.Analysis;
+
+/// http 请求的结果,编码与 Agent 的 HttpOutcomes 一致。
+public enum HttpOutcome
+{
+ Ok = 0,
+ Timeout = 1,
+ Reset = 2,
+ Tls = 3,
+ Other = 4,
+ Dns = 5,
+ BadStatus = 6,
+ Unsupported = 7,
+ NotAllowed = 8,
+}
+
+/// http 流(req)的公共读法:权重 w 是这一行代表的网格间隔数(自适应降频后每次请求代表更长的时间)。
+internal readonly struct HttpColumns
+{
+ public HttpColumns(RawStream stream)
+ {
+ Stream = stream;
+ Ts = stream.Find("ts");
+ Outcome = stream.Find("outcome");
+ Weights = stream.Find("w");
+ }
+
+ public RawStream Stream { get; }
+
+ public RawColumn? Ts { get; }
+
+ public RawColumn? Outcome { get; }
+
+ private RawColumn? Weights { get; }
+
+ public bool Has => Ts is not null && Outcome is not null;
+
+ public int Weight(int row) => Weights is not null && Weights.IsValid(row) ? (int)Math.Clamp(Weights.Values[row], 1, 65535) : 1;
+}
+
+/// 成功率(分子是按权重累计的成功次数)或某种结果的次数。
+internal sealed class HttpOutcomeOp : MetricOp
+{
+ private readonly HttpOutcome _code;
+ private readonly bool _asRatio;
+
+ public HttpOutcomeOp(HttpOutcome code, bool asRatio)
+ {
+ _code = code;
+ _asRatio = asRatio;
+ }
+
+ public override string Signature => $"http_outcome/{_code}/{(_asRatio ? "ratio" : "count")}/1";
+
+ public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
+ {
+ var c = new HttpColumns(stream);
+ if (!c.Has)
+ {
+ return;
+ }
+
+ for (int i = 0; i < stream.Count; i++)
+ {
+ bool hit = c.Outcome!.Values[i] == (long)_code;
+ long at = view.ColumnToUtcMs(c.Ts!, c.Ts!.Values[i]);
+ if (_asRatio)
+ {
+ int w = c.Weight(i);
+ sink.Emit(at, hit ? w : 0, w);
+ }
+ else if (hit)
+ {
+ sink.Emit(at, 1);
+ }
+ }
+ }
+}
+
+/// 一个阶段的耗时(毫秒):只统计成功的请求,按权重累计,所以分位数是按时间平均的。
+internal sealed class HttpPhaseMsOp : MetricOp
+{
+ private const int MaxRepeat = 256;
+
+ private readonly string _column;
+
+ public HttpPhaseMsOp(string column)
+ {
+ _column = column;
+ }
+
+ public override string Signature => $"http_phase_ms/{_column}/1";
+
+ public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
+ {
+ var c = new HttpColumns(stream);
+ RawColumn? column = stream.Find(_column);
+ if (!c.Has || column is null)
+ {
+ return;
+ }
+
+ for (int i = 0; i < stream.Count; i++)
+ {
+ if (c.Outcome!.Values[i] != (long)HttpOutcome.Ok || !column.IsValid(i))
+ {
+ continue;
+ }
+
+ long at = view.ColumnToUtcMs(c.Ts!, c.Ts!.Values[i]);
+ for (int k = Math.Min(c.Weight(i), MaxRepeat); k > 0; k--)
+ {
+ sink.Emit(at, column.Values[i] / 1000.0);
+ }
+ }
+ }
+}
diff --git a/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs b/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs
index 0713448..dde5aac 100644
--- a/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs
@@ -37,6 +37,12 @@ public static class Ops
public static MetricOp OutcomeCount(ConnectOutcome code) => new OutcomeOp(code, asRatio: false);
+ public static MetricOp HttpSuccess { get; } = new HttpOutcomeOp(HttpOutcome.Ok, asRatio: true);
+
+ public static MetricOp HttpOutcomeCount(HttpOutcome code) => new HttpOutcomeOp(code, asRatio: false);
+
+ public static MetricOp HttpPhaseMs(string column) => new HttpPhaseMsOp(column);
+
public static MetricOp HoldEvents(HoldEvent holdEvent) => new HoldEventOp(holdEvent);
public static MetricOp HoldLifetime { get; } = new HoldLifetimeOp();
diff --git a/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs b/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs
index 77c379b..ccf0d88 100644
--- a/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs
+++ b/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs
@@ -330,7 +330,7 @@ internal sealed class HoldLifetimeOp : MetricOp
}
}
-/// 单向 UDP 流(udp_train)的丢包:接收端没有记录到 t_recv 即为丢失。
+/// 单向 UDP 流(quic_stream)的丢包:接收端没有记录到 t_recv 即为丢失。
internal sealed class TrainLossOp : MetricOp
{
public override string Signature => "train_loss/1";
diff --git a/backend/src/MoShiro.Backend.Api/ApiModule.cs b/backend/src/MoShiro.Backend.Api/ApiModule.cs
index 552b49b..6e6ddf0 100644
--- a/backend/src/MoShiro.Backend.Api/ApiModule.cs
+++ b/backend/src/MoShiro.Backend.Api/ApiModule.cs
@@ -98,6 +98,7 @@ public static class ApiModule
{
RouteGroupBuilder publicGroup = app.MapGroup("/api/public").AddEndpointFilter(PublicGuard.InvokeAsync);
QueryEndpoints.Map(publicGroup);
+ TargetEndpoints.Map(publicGroup);
LiveEndpoints.Map(publicGroup);
HostEndpoints.Map(publicGroup);
NodeEndpoints.Map(publicGroup);
diff --git a/backend/src/MoShiro.Backend.Api/Contracts/ApiJsonContext.cs b/backend/src/MoShiro.Backend.Api/Contracts/ApiJsonContext.cs
index 0967ec6..d56d60f 100644
--- a/backend/src/MoShiro.Backend.Api/Contracts/ApiJsonContext.cs
+++ b/backend/src/MoShiro.Backend.Api/Contracts/ApiJsonContext.cs
@@ -25,6 +25,7 @@ namespace MoShiro.Backend.Api;
[JsonSerializable(typeof(QuicLineHealthDto))]
[JsonSerializable(typeof(TestDto))]
[JsonSerializable(typeof(PageDto))]
+[JsonSerializable(typeof(PageDto))]
[JsonSerializable(typeof(PageDto))]
[JsonSerializable(typeof(PageDto))]
[JsonSerializable(typeof(PageDto))]
diff --git a/backend/src/MoShiro.Backend.Api/Contracts/PublicDtos.cs b/backend/src/MoShiro.Backend.Api/Contracts/PublicDtos.cs
index 4994440..fe9226a 100644
--- a/backend/src/MoShiro.Backend.Api/Contracts/PublicDtos.cs
+++ b/backend/src/MoShiro.Backend.Api/Contracts/PublicDtos.cs
@@ -219,3 +219,24 @@ public sealed record LiveHostDto(string Id, bool Online, HostDto Host);
/// 主机资源占用的历史(列式,按步长取平均)。
public sealed record HostSeriesDto(string Id, DateTimeOffset From, DateTimeOffset To, string Step, IReadOnlyList T, IReadOnlyDictionary Values);
+
+///
+/// 一个 Agent 上的一个单个 Agent 任务:它测的目标、现在的状态和最近一段时间的时序。
+/// status:running 有最近的数据;waiting 任务已下发、还没有数据;stale 有过数据但最近没有;
+/// rejected Agent 本机拒绝执行(reason 是原因);offline Agent 不在线。
+///
+public sealed record TargetDto(
+ string TaskId,
+ string AgentId,
+ string AgentName,
+ string Kind,
+ string Target,
+ string Status,
+ string? Reason,
+ DateTimeOffset? LastSample,
+ DateTimeOffset From,
+ DateTimeOffset To,
+ string Step,
+ IReadOnlyList T,
+ IReadOnlyDictionary Values,
+ SummaryDto Summary);
diff --git a/backend/src/MoShiro.Backend.Api/Public/TargetEndpoints.cs b/backend/src/MoShiro.Backend.Api/Public/TargetEndpoints.cs
new file mode 100644
index 0000000..b95ccf5
--- /dev/null
+++ b/backend/src/MoShiro.Backend.Api/Public/TargetEndpoints.cs
@@ -0,0 +1,112 @@
+using System.Text.Json;
+using Fleet.Core;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using MoShiro.Backend.Application;
+using MoShiro.Protocol;
+
+namespace MoShiro.Backend.Api;
+
+/// 单个 Agent 任务(Agent 自己去测一个外部地址):每个 Agent 实际在执行的目标、状态和最近的时序。
+public static class TargetEndpoints
+{
+ private const string Tag = "public";
+
+ /// 最近多久内有样本算「在运行」:自适应退避到最慢是几分钟一次,所以要留够余量。
+ private static readonly TimeSpan s_running = TimeSpan.FromMinutes(15);
+
+ /// 每种方法默认看的指标。
+ private static readonly Dictionary s_metrics = new(StringComparer.Ordinal)
+ {
+ ["http_request"] = "http.success,http.ttfb_ms.p50,http.ttfb_ms.p95,http.total_ms.p95,http.dns_ms.p95,http.tcp_ms.p95,http.fail.timeout,http.fail.dns,http.fail.rst,http.fail.tls,http.fail.status,http.fail.unsupported,http.fail.other",
+ ["tcp_connect"] = "connect.success,connect.tcp_ms.p95,connect.tls_ms.p95",
+ ["icmp_ping"] = "rtt.p50,rtt.p95,loss.total",
+ };
+
+ /// 详情页要的指标:卡片上的那些再加上分位数和各种失败计数。
+ private static readonly Dictionary s_fullMetrics = new(StringComparer.Ordinal)
+ {
+ ["http_request"] = "http.success,http.ttfb_ms.p50,http.ttfb_ms.p95,http.ttfb_ms.p99,http.total_ms.p50,http.total_ms.p95,http.total_ms.p99,http.dns_ms.p95,http.tcp_ms.p95,http.fail.timeout,http.fail.dns,http.fail.rst,http.fail.tls,http.fail.status,http.fail.unsupported,http.fail.other,n.http",
+ ["tcp_connect"] = "connect.success,connect.tcp_ms.p95,connect.tls_ms.p95,connect.fail.timeout,connect.fail.rst,connect.fail.tls,n.connect",
+ ["icmp_ping"] = "rtt.p50,rtt.p95,rtt.p99,loss.total",
+ };
+
+ public static void Map(RouteGroupBuilder group) =>
+ group.MapGet("/targets", ListAsync)
+ .Api("targets_list", "Single-Agent tasks with their status and recent series", Tag)
+ .Returns>().ProducesProblems(403, 422, 429, 503);
+
+ /// agent_id 和 task_id 只看一个目标;full=true 返回详情页要的全部指标。
+ private static Task ListAsync(
+ HttpContext http, TargetService targets, QueryService query, BackendOptions options, TimeProvider time,
+ string? range, DateTimeOffset? from, DateTimeOffset? to, string? step, string? agent_id, string? task_id, bool? full) =>
+ QueryHelpers.GuardedAsync(http, async ct =>
+ {
+ IReadOnlyList views = await targets.ListAsync(a => a.Agent.PublicVisible || PublicGuard.CanSeeHidden(http), ct);
+ var items = new List(views.Count);
+ long now = time.GetUtcNow().ToUnixTimeMilliseconds();
+ foreach (TargetView view in views.Where(v => (agent_id is null || v.Agent.Agent.Id.Value == agent_id) && (task_id is null || v.Task.Id.Value == task_id)))
+ {
+ string wanted = (full == true ? s_fullMetrics : s_metrics).GetValueOrDefault(view.Task.Kind, "rtt.p50");
+ Result selection = MetricSelection.Resolve(wanted, null, options.MaxMetricsPerQuery);
+ if (!selection.TryGet(out MetricSelection? sel, out FleetError? error))
+ {
+ return Problems.From(http, error);
+ }
+
+ Result series = await query.TargetSeriesAsync(view.Agent.Agent.Id, view.Task.Id, new RangeRequest(range ?? "3h", from, to, step, null, null), sel, ct);
+ if (!series.TryGet(out TargetSeriesResult? result, out error))
+ {
+ return Problems.From(http, error);
+ }
+
+ long? latest = await query.TargetLatestAsync(view.Agent.Agent.Id, view.Task.Id, ct);
+ (string status, string? reason) = StatusOf(view, latest, now);
+ items.Add(new TargetDto(
+ view.Task.Id.Value, view.Agent.Agent.Id.Value, view.Agent.Agent.Name, view.Task.Kind, view.Task.Target ?? "", status, reason,
+ latest is { } at ? Mappers.Time(at) : null, Mappers.Time(result.Range.FromMs), Mappers.Time(result.Range.ToMs), Steps.Format(result.Range.StepMs),
+ result.T, result.Values, Mappers.ToDto(result.Summary)));
+ }
+
+ return TypedResults.Json(new PageDto(items, null), ApiJsonContext.Default.PageDtoTargetDto);
+ });
+
+ private static (string Status, string? Reason) StatusOf(TargetView view, long? latest, long now)
+ {
+ if (RejectedReason(view) is { } reason)
+ {
+ return ("rejected", reason);
+ }
+
+ if (!view.Agent.Online)
+ {
+ return ("offline", null);
+ }
+
+ if (latest is null)
+ {
+ return ("waiting", null);
+ }
+
+ return (now - latest.Value <= s_running.TotalMilliseconds ? "running" : "stale", null);
+ }
+
+ /// Agent 本机拒绝执行这个任务的原因(心跳里的 rejected[],条目 id 是任务 id)。
+ private static string? RejectedReason(TargetView view)
+ {
+ if (view.Agent.Agent.RejectedJson is not { } json)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize(json, ApiJsonContext.Default.ListRejectedItem)?.FirstOrDefault(r => r.Id == view.Task.Id.Value)?.Reason;
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+}
diff --git a/backend/src/MoShiro.Backend.Application/ApplicationServiceCollectionExtensions.cs b/backend/src/MoShiro.Backend.Application/ApplicationServiceCollectionExtensions.cs
index b53ef08..bc32064 100644
--- a/backend/src/MoShiro.Backend.Application/ApplicationServiceCollectionExtensions.cs
+++ b/backend/src/MoShiro.Backend.Application/ApplicationServiceCollectionExtensions.cs
@@ -32,6 +32,7 @@ public static class ApplicationServiceCollectionExtensions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/backend/src/MoShiro.Backend.Application/Query/QueryResults.cs b/backend/src/MoShiro.Backend.Application/Query/QueryResults.cs
index 6392195..1b020e8 100644
--- a/backend/src/MoShiro.Backend.Application/Query/QueryResults.cs
+++ b/backend/src/MoShiro.Backend.Application/Query/QueryResults.cs
@@ -45,3 +45,12 @@ public sealed record CompareResult(
string Metric,
QueryRange Range,
SourcePlan Plan);
+
+/// 单个 Agent 任务(目标任务)的时序:和线路时序同一套分层与汇总,只是没有线路、事件和评分。
+public sealed record TargetSeriesResult(
+ QueryRange Range,
+ SourcePlan Plan,
+ IReadOnlyList T,
+ IReadOnlyDictionary Values,
+ SummaryResult Summary,
+ IReadOnlyDictionary Versions);
diff --git a/backend/src/MoShiro.Backend.Application/Query/QueryService.cs b/backend/src/MoShiro.Backend.Application/Query/QueryService.cs
index 0bf4a61..47d0af4 100644
--- a/backend/src/MoShiro.Backend.Application/Query/QueryService.cs
+++ b/backend/src/MoShiro.Backend.Application/Query/QueryService.cs
@@ -138,6 +138,61 @@ public sealed class QueryService
return Result.Ok(new SeriesResult(line, p.Range, p.Plan, bucketed.T, values, summary, gaps, events, versions));
}
+ /// 某个 Agent 上一个单个 Agent 任务的时序(序列 target:<任务 id>)。
+ public async ValueTask> TargetSeriesAsync(NodeId agent, TaskId task, RangeRequest request, MetricSelection selection, CancellationToken ct)
+ {
+ Result<(QueryRange Range, SourcePlan Plan)> planned = Plan(request, "3h");
+ if (!planned.TryGet(out (QueryRange Range, SourcePlan Plan) p, out FleetError? error))
+ {
+ return Result.Fail(error);
+ }
+
+ Result> loaded = await _loader.LoadAsync(p.Plan, p.Range, agent, TargetSeries(task), ct);
+ if (!loaded.TryGet(out IReadOnlyList? rows, out error))
+ {
+ return Result.Fail(error);
+ }
+
+ BucketedRows bucketed = BucketedRows.Create(rows, p.Range);
+ int n = p.Range.Points;
+ var values = selection.ExportIds.ToDictionary(id => id, _ => new double?[n], StringComparer.Ordinal);
+ double widthSeconds = p.Plan.LevelWidthMs / 1000.0;
+ for (int i = 0; i < n; i++)
+ {
+ if (bucketed.Sets[i] is not { } set)
+ {
+ continue;
+ }
+
+ Dictionary exported = ExportEvaluator.Evaluate(set, selection.ExportIds, bucketed.RowCounts[i] * widthSeconds);
+ foreach (string id in selection.ExportIds)
+ {
+ if (exported.TryGetValue(id, out double v))
+ {
+ values[id][i] = v;
+ }
+ }
+ }
+
+ var versions = new Dictionary(StringComparer.Ordinal);
+ foreach (string id in selection.ExportIds)
+ {
+ if (MetricRegistry.TryFindExport(id, out MetricDef def, out _))
+ {
+ versions[id] = def.Fingerprint;
+ }
+ }
+
+ SummaryResult summary = Summarize(bucketed.Merge(), bucketed.TotalRows, p.Plan, p.Range, selection, new Dictionary());
+ return Result.Ok(new TargetSeriesResult(p.Range, p.Plan, bucketed.T, values, summary, versions));
+ }
+
+ /// 这个单个 Agent 任务最后一个样本的时刻(Unix 毫秒);还没有数据返回 null。
+ public ValueTask TargetLatestAsync(NodeId agent, TaskId task, CancellationToken ct) =>
+ _loader.LatestSampleMsAsync(agent, TargetSeries(task), _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
+
+ private static string TargetSeries(TaskId task) => "target:" + task.Value;
+
/// 这条线路最早的数据(Unix 毫秒);没有数据返回 null。
public ValueTask EarliestAsync(LineKey line, CancellationToken ct) =>
_loader.EarliestSampleMsAsync(line.Listener, line.Series, _time.GetUtcNow().ToUnixTimeMilliseconds(), ct);
diff --git a/backend/src/MoShiro.Backend.Application/Query/Targets.cs b/backend/src/MoShiro.Backend.Application/Query/Targets.cs
new file mode 100644
index 0000000..ca193c3
--- /dev/null
+++ b/backend/src/MoShiro.Backend.Application/Query/Targets.cs
@@ -0,0 +1,39 @@
+using Fleet.Server;
+
+namespace MoShiro.Backend.Application;
+
+/// 一个 Agent 上正在(或应该)运行的单个 Agent 任务:这个 Agent 的有效任务集里 scope=target 的那些。
+public sealed record TargetView(AgentView Agent, EffectiveTask Task);
+
+/// 单个 Agent 任务目录:按 Agent 展开模板、覆盖和专属任务,列出每个 Agent 实际执行的目标。
+public sealed class TargetService
+{
+ private readonly AgentService _agents;
+ private readonly ITaskStore _tasks;
+
+ public TargetService(AgentService agents, ITaskStore tasks)
+ {
+ _agents = agents;
+ _tasks = tasks;
+ }
+
+ public async ValueTask> ListAsync(Func include, CancellationToken ct)
+ {
+ IReadOnlyList all = await _tasks.ListAsync(ct);
+ var result = new List();
+ foreach (AgentView view in await _agents.ListAsync(ct))
+ {
+ if (view.Agent.Status != NodeStatus.Active || !include(view))
+ {
+ continue;
+ }
+
+ foreach (EffectiveTask task in EffectiveTasks.Compute(all, view.Agent, filterByAgentKinds: false).Where(t => t.Scope == EffectiveTasks.TargetScope))
+ {
+ result.Add(new TargetView(view, task));
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/backend/src/MoShiro.Backend.Application/Tasks/EffectiveTasks.cs b/backend/src/MoShiro.Backend.Application/Tasks/EffectiveTasks.cs
index ae996ef..1fe16b3 100644
--- a/backend/src/MoShiro.Backend.Application/Tasks/EffectiveTasks.cs
+++ b/backend/src/MoShiro.Backend.Application/Tasks/EffectiveTasks.cs
@@ -13,7 +13,7 @@ public sealed record EffectiveTask(
ParamValues Resolved);
///
-/// 有效任务集:线路任务模板 + 针对单个 Agent 的覆盖 + 该 Agent 的目标任务。
+/// 有效任务集:任务模板(线路任务、单个 Agent 的任务)+ 针对单个 Agent 的覆盖 + 只属于该 Agent 的目标任务。
/// 参数经 ParamSpec 校验并补全默认值,Backend 和节点使用同一套规则。
///
public static class EffectiveTasks
@@ -35,23 +35,23 @@ public static class EffectiveTasks
foreach (TaskRecord task in all.OrderBy(t => t.Id.Value, StringComparer.Ordinal))
{
- if (task.Scope == LineScope && task.AgentId is null)
+ if (task.AgentId is null)
{
- // 模板:有覆盖时以覆盖为准(覆盖可以停用或改参数)
+ // 模板(线路任务或单个 Agent 的任务):有覆盖时以覆盖为准(覆盖可以停用、改参数,单个 Agent 的任务还可以换目标)
TaskRecord? over = overrides.GetValueOrDefault(task.Id);
if (over is not null)
{
if (over.Enabled)
{
- Add(result, over.Id, task.Scope, over.Kind, null, Merge(task.Params, over.Params), over.Schedule, agent, filterByAgentKinds);
+ Add(result, over.Id, task.Scope, over.Kind, over.Target ?? task.Target, Merge(task.Params, over.Params), over.Schedule, agent, filterByAgentKinds);
}
}
else if (task.Enabled)
{
- Add(result, task.Id, task.Scope, task.Kind, null, task.Params, task.Schedule, agent, filterByAgentKinds);
+ Add(result, task.Id, task.Scope, task.Kind, task.Target, task.Params, task.Schedule, agent, filterByAgentKinds);
}
}
- else if (task.Scope == TargetScope && task.AgentId == agent.Id && task.Enabled)
+ else if (task.Scope == TargetScope && task.AgentId == agent.Id && task.BaseTaskId is null && task.Enabled)
{
Add(result, task.Id, task.Scope, task.Kind, task.Target, task.Params, task.Schedule, agent, filterByAgentKinds);
}
diff --git a/backend/src/MoShiro.Backend.Application/Tasks/TaskService.cs b/backend/src/MoShiro.Backend.Application/Tasks/TaskService.cs
index 50687fe..70fd4b8 100644
--- a/backend/src/MoShiro.Backend.Application/Tasks/TaskService.cs
+++ b/backend/src/MoShiro.Backend.Application/Tasks/TaskService.cs
@@ -41,7 +41,7 @@ public sealed class TaskService
public ValueTask GetAsync(TaskId id, CancellationToken ct) => _tasks.GetAsync(id, ct);
///
- /// 创建默认的线路任务模板,这样新 Agent 注册后自动获得任务:udp_echo、tcp_echo 持续运行,connect(TCP 建连和 TLS 握手耗时,
+ /// 创建默认的线路任务模板,这样新 Agent 注册后自动获得任务:quic_ping、tcp_ping 持续运行,connect(TCP 建连和 TLS 握手耗时,
/// 每分钟 6 次)也持续运行。每一批默认任务只种一次,用设置里的标记记住;你删掉的默认模板不会被重新建出来,
/// 后来新增的默认任务(connect)在已有部署升级后会补建一次。
///
@@ -49,13 +49,13 @@ public sealed class TaskService
{
if (await _settings.GetAsync("tasks.seeded", ct) is null)
{
- await SeedAsync([Kinds.UdpEcho, Kinds.TcpEcho], ct);
+ await SeedAsync([Kinds.QuicPing, Kinds.TcpPing], ct);
await _settings.SetAsync("tasks.seeded", "1", ct);
}
if (await _settings.GetAsync("tasks.seeded.connect", ct) is null)
{
- await SeedAsync([Kinds.Connect], ct);
+ await SeedAsync([Kinds.TcpConnect], ct);
await _settings.SetAsync("tasks.seeded.connect", "1", ct);
}
}
@@ -223,9 +223,31 @@ public sealed class TaskService
if (scope == EffectiveTasks.TargetScope)
{
- if (agentId is null || !TryNormalizeTarget(target, out string? normalized))
+ // 单个 Agent 的任务:没有 agent_id 是模板(所有 Agent 各自对这个目标执行),有 agent_id 是只属于这个 Agent 的任务,
+ // 或者(带 base_task_id)覆盖模板的参数、停用它、换一个目标
+ if (baseTaskId is { } templateId)
{
- return ValidationError.Fail("target", agentId is null ? "agent_id is required for target tasks" : "must be an IP address with an optional port");
+ TaskRecord? template = await _tasks.GetAsync(templateId, ct);
+ if (agentId is null || template is null || template.AgentId is not null || template.Scope != EffectiveTasks.TargetScope)
+ {
+ return ValidationError.Fail("base_task_id", "must reference a single-agent template and requires agent_id");
+ }
+
+ if (template.Kind != kind)
+ {
+ return ValidationError.Fail("kind", "an override must use the same kind as its template");
+ }
+
+ if (target is null)
+ {
+ return Result.Ok(new ValidatedTask(null));
+ }
+ }
+
+ string? normalized = TargetHost.Normalize(kind, target);
+ if (normalized is null)
+ {
+ return ValidationError.Fail("target", kind == TargetHost.HttpKind ? "must be an http:// or https:// URL" : "must be a host name or IP address with an optional port");
}
return Result.Ok(new ValidatedTask(normalized));
@@ -288,28 +310,4 @@ public sealed class TaskService
return Result.Ok(true);
}
-
- /// 只接受 IP 地址(可带端口),不做 DNS 解析,避免把 Agent 用作解析器。
- public static bool TryNormalizeTarget(string? target, out string? normalized)
- {
- normalized = null;
- if (string.IsNullOrWhiteSpace(target))
- {
- return false;
- }
-
- if (IPAddress.TryParse(target, out IPAddress? ip))
- {
- normalized = ip.ToString();
- return true;
- }
-
- if (IPEndPoint.TryParse(target, out IPEndPoint? endpoint) && endpoint.Port is > 0 and <= 65535)
- {
- normalized = endpoint.ToString();
- return true;
- }
-
- return false;
- }
}
diff --git a/backend/src/MoShiro.Backend.Application/Tests/Plans.cs b/backend/src/MoShiro.Backend.Application/Tests/Plans.cs
index f4c72da..50fb696 100644
--- a/backend/src/MoShiro.Backend.Application/Tests/Plans.cs
+++ b/backend/src/MoShiro.Backend.Application/Tests/Plans.cs
@@ -18,7 +18,7 @@ public static class Plans
private static JsonElement S(string value) => JsonSerializer.SerializeToElement(value, PlanJson.Default.String);
private static PhaseSpec Train(double mbps) =>
- new("udp_train", new Dictionary { ["mbps"] = N(mbps), ["duration_s"] = N(10) }, 10);
+ new("quic_stream", new Dictionary { ["mbps"] = N(mbps), ["duration_s"] = N(10) }, 10);
public static readonly PlanSpec Hy2Ladder = new(
"hy2_ladder",
@@ -31,9 +31,9 @@ public static class Plans
"REALITY session",
"Connection establishment, a short (4 s) bulk transfer for the peak throughput and a held connection.",
[
- new PhaseSpec("connect", new Dictionary { ["per_minute"] = N(20) }, 30),
- new PhaseSpec("tcp_bulk", new Dictionary { ["duration_s"] = N(4), ["direction"] = S("rev") }, 4),
- new PhaseSpec("hold", new Dictionary { ["heartbeat_s"] = N(5) }, 30),
+ new PhaseSpec("tcp_connect", new Dictionary { ["per_minute"] = N(20) }, 30),
+ new PhaseSpec("tcp_transfer", new Dictionary { ["duration_s"] = N(4), ["direction"] = S("rev") }, 4),
+ new PhaseSpec("tls_hold", new Dictionary { ["heartbeat_s"] = N(5) }, 30),
]);
public static readonly IReadOnlyList All = [Hy2Ladder, RealitySession];
diff --git a/backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs b/backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs
index 3c72cb4..1ec6b5b 100644
--- a/backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs
+++ b/backend/src/MoShiro.Backend.Storage.Sqlite/Schema.cs
@@ -3,12 +3,12 @@ using Microsoft.Data.Sqlite;
namespace MoShiro.Backend.Storage.Sqlite;
///
-/// 表结构。版本记录在 PRAGMA user_version。当前只有一个版本:不是这个版本的数据库(包括更早的构建留下的)不会被迁移,
-/// 启动时报错,删除 moshiro.db 和 raw/ 之后重新开始。以后的结构变更按版本号追加内置 SQL。
+/// 表结构。版本记录在 PRAGMA user_version。版本 1 到 2 只改了 Kind 的名字(见 );
+/// 其他版本的数据库(包括更早的构建留下的)不会被迁移,启动时报错,删除 moshiro.db 和 raw/ 之后重新开始。以后的结构变更按版本号追加内置 SQL。
///
internal static class Schema
{
- public const int CurrentVersion = 1;
+ public const int CurrentVersion = 2;
public static void Migrate(SqliteConnection connection)
{
@@ -18,6 +18,21 @@ internal static class Schema
return;
}
+ if (version == 1)
+ {
+ using SqliteTransaction upgrade = connection.BeginTransaction();
+ foreach ((string from, string to) in RenameKinds)
+ {
+ connection.Exec($"UPDATE tasks SET kind = '{to}' WHERE kind = '{from}'");
+ connection.Exec($"UPDATE raw_chunks SET kind = '{to}' WHERE kind = '{from}'");
+ connection.Exec($"UPDATE tests SET plan = REPLACE(plan, '\"{from}\"', '\"{to}\"')");
+ }
+
+ connection.Exec("PRAGMA user_version = 2");
+ upgrade.Commit();
+ return;
+ }
+
if (version != 0)
{
throw new InvalidOperationException($"database schema version {version} is not supported by this build (expected {CurrentVersion}); delete moshiro.db and the raw/ directory to start over");
@@ -25,10 +40,17 @@ internal static class Schema
using SqliteTransaction transaction = connection.BeginTransaction();
connection.Exec(V1);
- connection.Exec("PRAGMA user_version = 1");
+ connection.Exec("PRAGMA user_version = 2");
transaction.Commit();
}
+ /// 版本 2:Kind 改成「协议 + 做什么」的名字。任务和原始数据索引跟着改;已经落盘的原始块头里还是旧名字,按旧名字解不出指标,重算不到它们,已经算好的指标不受影响。
+ private static readonly (string From, string To)[] RenameKinds =
+ [
+ ("udp_echo", "quic_ping"), ("tcp_echo", "tcp_ping"), ("udp_train", "quic_stream"), ("tcp_bulk", "tcp_transfer"),
+ ("connect", "tcp_connect"), ("hold", "tls_hold"), ("icmp_echo", "icmp_ping"),
+ ];
+
///
/// 新增 Kind 或指标都不需要改表结构:指标是 metric_*.metrics 里的一个键,Kind 只是 raw_chunks.kind 的一个取值。
/// 节点只有一张表(agents):监听的节点有地址和端口,不监听的节点只拨出。线路 (agent_id, series) 里的 agent_id 是监听方,series 里是拨号方。
diff --git a/backend/tests/MoShiro.Backend.Analysis.Tests/ModelAndTestEvaluatorTests.cs b/backend/tests/MoShiro.Backend.Analysis.Tests/ModelAndTestEvaluatorTests.cs
index 42d667a..71da243 100644
--- a/backend/tests/MoShiro.Backend.Analysis.Tests/ModelAndTestEvaluatorTests.cs
+++ b/backend/tests/MoShiro.Backend.Analysis.Tests/ModelAndTestEvaluatorTests.cs
@@ -32,8 +32,8 @@ public sealed class ModelAndTestEvaluatorTests
[Fact]
public void Fingerprint_ChangesWhenTheOpChanges()
{
- var a = Metric.Counter("x", ["udp_echo.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 100), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
- var b = Metric.Counter("x", ["udp_echo.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 200), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
+ var a = Metric.Counter("x", ["quic_ping.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 100), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
+ var b = Metric.Counter("x", ["quic_ping.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 200), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
Assert.NotEqual(a.Fingerprint, b.Fingerprint);
}
diff --git a/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs b/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs
index dfbe7e4..ce7e66c 100644
--- a/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs
+++ b/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs
@@ -25,7 +25,7 @@ public sealed class OpTests
[Fact]
public void Rtt_RemovesAgentProcessingTime()
{
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 100, 100, _ => new EchoPacket(false, false, 20_000, 30_000, ProcessUs: 5_000));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, 100, 100, _ => new EchoPacket(false, false, 20_000, 30_000, ProcessUs: 5_000));
var rtt = (HistogramAcc)EvaluateOneBucket(chunk, "rtt").Items["rtt"];
Assert.Equal(100UL, rtt.Count);
Assert.InRange(rtt.Quantile(0.5)!.Value, 50 * 0.955, 50 * 1.045);
@@ -37,8 +37,8 @@ public sealed class OpTests
static EchoPacket At(int i) => new(LostUp: i % 20 < 2, LostDown: false, i % 10 < 2 ? 100_000 : 20_000, 20_000);
// 每 100 ms 一个包,每个权重 1;对照:只取偶数行,每 200 ms 一个包,每个权重 2——代表同样长的时间
- RawChunk dense = ChunkFactory.Echo(s_agent, "udp_echo", T0, 100, 100, At);
- RawChunk sparse = ChunkFactory.Echo(s_agent, "udp_echo", T0, 50, 200, i => At(i * 2), weight: _ => 2);
+ RawChunk dense = ChunkFactory.Echo(s_agent, "quic_ping", T0, 100, 100, At);
+ RawChunk sparse = ChunkFactory.Echo(s_agent, "quic_ping", T0, 50, 200, i => At(i * 2), weight: _ => 2);
MetricSet d = EvaluateOneBucket(dense, "rtt", "loss.up", "loss.total");
MetricSet s = EvaluateOneBucket(sparse, "rtt", "loss.up", "loss.total");
@@ -54,7 +54,7 @@ public sealed class OpTests
public void Loss_SeparatesUpAndDown()
{
RawChunk chunk = ChunkFactory.Echo(
- s_agent, "udp_echo", T0, 100, 100,
+ s_agent, "quic_ping", T0, 100, 100,
i => new EchoPacket(LostUp: i < 10, LostDown: i is >= 10 and < 15, 20_000, 20_000));
MetricSet set = EvaluateOneBucket(chunk, "loss.up", "loss.down", "loss.total");
@@ -77,7 +77,7 @@ public sealed class OpTests
public void Jitter_OnlyOneDirectionVaries_IsAttributedToThatDirection()
{
// 下行时延每隔一个包多 20 ms
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 100, 100, i => new EchoPacket(false, false, 20_000, 20_000 + (i % 2 == 0 ? 20_000 : 0)));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, 100, 100, i => new EchoPacket(false, false, 20_000, 20_000 + (i % 2 == 0 ? 20_000 : 0)));
MetricSet set = EvaluateOneBucket(chunk, "jitter.up", "jitter.down");
Assert.True(((HistogramAcc)set.Items["jitter.up"]).Quantile(0.95) < 0.5);
Assert.InRange(((HistogramAcc)set.Items["jitter.down"]).Quantile(0.95)!.Value, 15, 25);
@@ -95,7 +95,7 @@ public sealed class OpTests
public void LossRuns_ReportsConsecutiveLosses()
{
bool[] lost = [false, true, true, true, false, true, false, false, true, true];
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, lost.Length, 100, i => new EchoPacket(false, lost[i], 20_000, 20_000));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, lost.Length, 100, i => new EchoPacket(false, lost[i], 20_000, 20_000));
var runs = (RunLengthAcc)EvaluateOneBucket(chunk, "loss.run").Items["loss.run"];
Assert.Equal(3UL, runs.Runs);
Assert.Equal(3, runs.Max);
@@ -105,7 +105,7 @@ public sealed class OpTests
[Fact]
public void LossRuns_TrailingRunIsReported()
{
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 4, 100, i => new EchoPacket(false, i >= 2, 20_000, 20_000));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, 4, 100, i => new EchoPacket(false, i >= 2, 20_000, 20_000));
Assert.Equal(2, ((RunLengthAcc)EvaluateOneBucket(chunk, "loss.run").Items["loss.run"]).Max);
}
@@ -114,7 +114,7 @@ public sealed class OpTests
{
bool[] upLost = [false, true, true, false, false, false, true, true];
bool[] downLost = [false, false, true, true, false, true, false, true];
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, upLost.Length, 100,
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, upLost.Length, 100,
i => new EchoPacket(upLost[i], downLost[i], 20_000, 20_000));
MetricSet set = EvaluateOneBucket(chunk, "loss.run.up", "loss.run.down");
@@ -135,7 +135,7 @@ public sealed class OpTests
public void Reorder_CountsLateArrivals_AndIgnoresDuplicates()
{
// 包 3 的回复比包 4 晚到
- RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 6, 100, i => new EchoPacket(false, false, 20_000, i == 3 ? 150_000 : 20_000));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "quic_ping", T0, 6, 100, i => new EchoPacket(false, false, 20_000, i == 3 ? 150_000 : 20_000));
MetricSet set = EvaluateOneBucket(chunk, "reorder", "dup");
Assert.Equal(1UL, ((CounterAcc)set.Items["reorder"]).Value);
Assert.False(set.Items.ContainsKey("dup"));
@@ -146,7 +146,7 @@ public sealed class OpTests
{
// 第 40 个包之后的 10 个包被队头阻塞,全部在 2540 ms 之后几乎同时到达
RawChunk chunk = ChunkFactory.Echo(
- s_agent, "tcp_echo", T0, 100, 50,
+ s_agent, "tcp_ping", T0, 100, 50,
i => new EchoPacket(false, false, 20_000, i is >= 40 and < 50 ? ((2540 + (i - 40) - (50 * i) - 20) * 1000) - 50 : 20_000));
MetricSet set = EvaluateOneBucket(chunk, "tcp.stall", "tcp.stall_max");
Assert.Equal(1UL, ((CounterAcc)set.Items["tcp.stall"]).Value);
@@ -156,7 +156,7 @@ public sealed class OpTests
[Fact]
public void Stall_SteadyArrivals_AreNotStalls()
{
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 100, 50, _ => new EchoPacket(false, false, 20_000, 20_000));
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 100, 50, _ => new EchoPacket(false, false, 20_000, 20_000));
MetricSet set = EvaluateOneBucket(chunk, "tcp.stall");
Assert.False(set.Items.ContainsKey("tcp.stall"));
}
@@ -166,7 +166,7 @@ public sealed class OpTests
{
// 基线 50 ms(单程 25 ms),阈值 = max(90, 50 + max(15, 17.5)) = 90 ms;第 10、20、30 个回显的 RTT 是 200 ms
RawChunk chunk = ChunkFactory.Echo(
- s_agent, "tcp_echo", T0, 100, 50,
+ s_agent, "tcp_ping", T0, 100, 50,
i => new EchoPacket(false, false, 25_000, i % 10 == 0 && i > 0 && i < 40 ? 175_000 : 25_000, ProcessUs: 0));
var slow = (RatioAcc)EvaluateOneBucket(chunk, "tcp.slow").Items["tcp.slow"];
Assert.Equal(100UL, slow.Denominator);
@@ -176,10 +176,10 @@ public sealed class OpTests
[Fact]
public void Slow_ASteadyConnection_HasNoLongTail_AndTooFewSamplesGiveNoObservation()
{
- RawChunk steady = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 100, 50, _ => new EchoPacket(false, false, 25_000, 25_000));
+ RawChunk steady = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 100, 50, _ => new EchoPacket(false, false, 25_000, 25_000));
Assert.Equal(0UL, ((RatioAcc)EvaluateOneBucket(steady, "tcp.slow").Items["tcp.slow"]).Numerator);
- RawChunk few = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 50, i => new EchoPacket(false, false, 25_000, i == 5 ? 500_000 : 25_000));
+ RawChunk few = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 50, i => new EchoPacket(false, false, 25_000, i == 5 ? 500_000 : 25_000));
Assert.False(EvaluateOneBucket(few, "tcp.slow").Items.ContainsKey("tcp.slow"));
}
@@ -188,7 +188,7 @@ public sealed class OpTests
{
// 1000 个回显,其中 5 个 1 s 的长尾:p50 约 50 ms,p99 仍在 50 ms 附近,p99.9 落在长尾上
RawChunk chunk = ChunkFactory.Echo(
- s_agent, "tcp_echo", T0, 1000, 10,
+ s_agent, "tcp_ping", T0, 1000, 10,
i => new EchoPacket(false, false, 25_000, i % 200 == 0 && i > 0 ? 975_000 : 25_000, ProcessUs: 0), durationMs: 20_000);
var rtt = (HistogramAcc)EvaluateOneBucket(chunk, "tcp.rtt").Items["tcp.rtt"];
MetricDef def = TcpMetrics.Rtt;
@@ -213,7 +213,7 @@ public sealed class OpTests
(baseUs + 1_000_000, 0, 40_000u, 2u, 300u),
(baseUs + 2_000_000, 0, 40_000u, 2u, 500u),
]);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
Assert.Equal(2UL, retrans.Numerator);
Assert.Equal(400UL, retrans.Denominator);
@@ -224,7 +224,7 @@ public sealed class OpTests
{
long baseUs = ChunkFactory.DialerUs(0);
RawStream info = ChunkFactory.TcpInfo([(baseUs, 0, 1u, 10u, 100u), (baseUs + 1_000_000, 0, 1u, 0u, 50u), (baseUs + 2_000_000, 0, 1u, 1u, 150u)]);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
Assert.Equal(1UL, retrans.Numerator);
Assert.Equal(100UL, retrans.Denominator);
@@ -234,12 +234,12 @@ public sealed class OpTests
public void Retrans_InterleavedConnections_DoNotCreateFalseRetransmissions()
{
long ts = ChunkFactory.DialerUs(0);
- var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!);
+ var builder = new StreamBuilder(Kinds.TcpPing.FindStream("tcp_info")!);
builder.Add(ts, 0, 40_000, 0, 10, 10, 1000, 0, 1);
builder.Add(ts + 1_000_000, 0, 40_000, 0, 10, 20, 1100, 0, 2);
builder.Add(ts + 2_000_000, 0, 40_000, 0, 10, 11, 1100, 0, 1);
builder.Add(ts + 3_000_000, 0, 40_000, 0, 10, 21, 1200, 0, 2);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100,
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100,
_ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [builder.Build()]);
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
Assert.Equal(2UL, retrans.Numerator);
@@ -256,7 +256,7 @@ public sealed class OpTests
(ts + 1_000_000, 0, 40_000u, 1u, 200u),
(ts + 3_000_000, 0, 40_000u, 3u, 400u),
]);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100,
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100,
_ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
Assert.Equal(3UL, retrans.Numerator);
@@ -267,12 +267,12 @@ public sealed class OpTests
public void Retrans_EqualConnectionIdsOnDifferentSides_AreIndependent()
{
long ts = ChunkFactory.DialerUs(0);
- var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!);
+ var builder = new StreamBuilder(Kinds.TcpPing.FindStream("tcp_info")!);
builder.Add(ts, 0, 40_000, 0, 10, 10, 1000, 0, 1);
builder.Add(ts, 1, 40_000, 0, 10, 100, 2000, 0, 1);
builder.Add(ts + 1_000_000, 0, 40_000, 0, 10, 11, 1100, 0, 1);
builder.Add(ts + 1_000_000, 1, 40_000, 0, 10, 102, 2200, 0, 1);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100,
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100,
_ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [builder.Build()]);
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
Assert.Equal(3UL, retrans.Numerator);
@@ -283,12 +283,12 @@ public sealed class OpTests
public void Retrans_ForwardAndReturnAreSplitBySide()
{
long ts = ChunkFactory.DialerUs(0);
- var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!);
+ var builder = new StreamBuilder(Kinds.TcpPing.FindStream("tcp_info")!);
builder.Add(ts, 0, 40_000, 0, 10, 10, 1000, 0, 1);
builder.Add(ts, 1, 40_000, 0, 10, 100, 2000, 0, 1);
builder.Add(ts + 1_000_000, 0, 40_000, 0, 10, 11, 1100, 0, 1);
builder.Add(ts + 1_000_000, 1, 40_000, 0, 10, 102, 2200, 0, 1);
- RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100,
+ RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_ping", T0, 10, 100,
_ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [builder.Build()]);
var forward = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans.up").Items["tcp.retrans.up"];
var back = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans.down").Items["tcp.retrans.down"];
@@ -322,9 +322,9 @@ public sealed class OpTests
// 10 秒的块,按 1 秒分桶:每个桶 10 个包
RawChunk chunk = ChunkFactory.UdpEchoClean(s_agent, T0);
var buckets = new SortedDictionary();
- WindowEvaluator.Default.Evaluate(chunk, 1000, buckets, new HashSet { "n.udp_echo" });
+ WindowEvaluator.Default.Evaluate(chunk, 1000, buckets, new HashSet { "n.quic_ping" });
Assert.Equal(10, buckets.Count);
- Assert.All(buckets.Values, set => Assert.Equal(10UL, ((CounterAcc)set.Items["n.udp_echo"]).Value));
+ Assert.All(buckets.Values, set => Assert.Equal(10UL, ((CounterAcc)set.Items["n.quic_ping"]).Value));
Assert.Equal(T0, buckets.Keys.First());
}
diff --git a/backend/tests/MoShiro.Backend.Tests/AgentDataPlaneTests.cs b/backend/tests/MoShiro.Backend.Tests/AgentDataPlaneTests.cs
index c2b8959..2a702d1 100644
--- a/backend/tests/MoShiro.Backend.Tests/AgentDataPlaneTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AgentDataPlaneTests.cs
@@ -22,8 +22,8 @@ public sealed class AgentDataPlaneTests : IClassFixture
AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync(rateHz: 30);
DialerSimulator dialer = await agent.ConnectDialerAsync();
- LineTaskSet set = await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
- return (agent, dialer, set.Tasks.Single(t => t.Kind == "udp_echo"));
+ LineTaskSet set = await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping" && t.Port == agent.App.QuicPorts[0]));
+ return (agent, dialer, set.Tasks.Single(t => t.Kind == "quic_ping"));
}
[Fact]
diff --git a/backend/tests/MoShiro.Backend.Tests/AgentEndToEndTests.cs b/backend/tests/MoShiro.Backend.Tests/AgentEndToEndTests.cs
index d489a8d..03ab5aa 100644
--- a/backend/tests/MoShiro.Backend.Tests/AgentEndToEndTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AgentEndToEndTests.cs
@@ -23,8 +23,8 @@ public sealed class AgentEndToEndTests : IClassFixture
AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync(rateHz: 30);
DialerSimulator dialer = await agent.ConnectDialerAsync();
- LineTaskSet set = await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
- return (agent, dialer, set.Tasks.Single(t => t.Kind == "udp_echo"));
+ LineTaskSet set = await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping" && t.Port == agent.App.QuicPorts[0]));
+ return (agent, dialer, set.Tasks.Single(t => t.Kind == "quic_ping"));
}
private async Task AgentViewAsync(AgentHarness agent) => await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id);
@@ -61,7 +61,7 @@ public sealed class AgentEndToEndTests : IClassFixture
}, because: (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/events?agent_id={agent.Id}")).ToJsonString() + $" fragments={agent.App.Counters.Fragments} discarded={agent.App.Counters.FragmentsDiscarded} outbox={agent.App.Link.Outbox.PendingCount}");
JsonNode chunk = stored["items"]![0]!;
Assert.True(chunk["kind"] is not null, stored.ToJsonString());
- Assert.Equal("udp_echo", chunk["kind"]!.GetValue());
+ Assert.Equal("quic_ping", chunk["kind"]!.GetValue());
Assert.True(agent.App.Counters.Fragments >= 1);
Assert.True(agent.App.Counters.PacketsIn >= 40);
}
@@ -76,11 +76,11 @@ public sealed class AgentEndToEndTests : IClassFixture
await using (agent)
await using (dialer)
{
- LineTask tcp = (await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "tcp_echo" && t.Port == agent.App.TcpPorts[0]))).Tasks.Single(t => t.Kind == "tcp_echo");
+ LineTask tcp = (await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "tcp_ping" && t.Port == agent.App.TcpPorts[0]))).Tasks.Single(t => t.Kind == "tcp_ping");
(int sent, int received) = await dialer.RunTcpEchoAsync(tcp, agent.App.TcpPorts[0], 15, TimeSpan.FromMilliseconds(60), tls);
Assert.Equal(15, sent);
Assert.Equal(15, received);
- await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Any(c => c!["kind"]!.GetValue() == "tcp_echo"));
+ await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Any(c => c!["kind"]!.GetValue() == "tcp_ping"));
}
}
@@ -98,8 +98,8 @@ public sealed class AgentEndToEndTests : IClassFixture
// The dialer process restarts: a new session with its own streams; sequence numbers start from 0 again
await using DialerSimulator second = first.Restarted();
await second.ConnectAsync("127.0.0.1", agent.App.ControlPort, agent.TlsPublicKey, agent.App.QuicPorts[0], agent.NodeId);
- LineTaskSet set = await second.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0]));
- LineTask udp2 = set.Tasks.Single(t => t.Kind == "udp_echo");
+ LineTaskSet set = await second.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping" && t.Port == agent.App.QuicPorts[0]));
+ LineTask udp2 = set.Tasks.Single(t => t.Kind == "quic_ping");
Assert.NotEqual(udp.StreamId, udp2.StreamId);
// The previous session's streams are released with it ...
@@ -135,9 +135,9 @@ public sealed class AgentEndToEndTests : IClassFixture
Assert.Equal(agent.Id, dialerView["outgoing"]![0]!["listener_id"]!.GetValue());
// Both get their own task set (own streams)
- LineTaskSet setA = await a.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo"));
- LineTaskSet setB = await b.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo"));
- Assert.NotEqual(setA.Tasks.Single(t => t.Kind == "udp_echo").StreamId, setB.Tasks.Single(t => t.Kind == "udp_echo").StreamId);
+ LineTaskSet setA = await a.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping"));
+ LineTaskSet setB = await b.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping"));
+ Assert.NotEqual(setA.Tasks.Single(t => t.Kind == "quic_ping").StreamId, setB.Tasks.Single(t => t.Kind == "quic_ping").StreamId);
// Drop one: the other stays
using HttpResponseMessage dropped = await _backend.Admin.PostAsync($"/api/admin/agents/{agent.Id}/dialers/{a.DialerId}/disconnect", null);
@@ -172,7 +172,7 @@ public sealed class AgentEndToEndTests : IClassFixture
await using AgentHarness agent = await AgentHarness.StartAsync(_backend, config => config.Limits.MaxTotalMbps = 20);
await agent.OverrideLineTasksAsync();
- var phase = new TestPhase { Kind = "udp_train", DurationSeconds = 1, Params = { ["port"] = System.Text.Json.JsonSerializer.SerializeToElement(agent.App.QuicPorts[0]) } };
+ var phase = new TestPhase { Kind = "quic_stream", DurationSeconds = 1, Params = { ["port"] = System.Text.Json.JsonSerializer.SerializeToElement(agent.App.QuicPorts[0]) } };
var run = new TestRunParams { TestId = new TestId("tst_x"), DialerId = new NodeId("agt_nobody"), Plan = "x", Phases = [phase] };
Assert.Equal("dialer_offline", agent.App.Tests.Start(run).Error.Code.Value);
diff --git a/backend/tests/MoShiro.Backend.Tests/AgentHarness.cs b/backend/tests/MoShiro.Backend.Tests/AgentHarness.cs
index 078d944..7748506 100644
--- a/backend/tests/MoShiro.Backend.Tests/AgentHarness.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AgentHarness.cs
@@ -105,19 +105,19 @@ public sealed class AgentHarness : IAsyncDisposable
public async Task OverrideLineTasksAsync(int? rateHz = null)
{
JsonNode tasks = await Backend.GetJsonAsync(Backend.Admin, "/api/admin/tasks");
- // 先处理 connect(关掉),再改 udp_echo / tcp_echo 的速率:否则中途的带宽预算会多出 connect 那一份
- foreach (JsonNode? task in tasks["items"]!.AsArray().OrderBy(t => t!["kind"]!.GetValue() == "connect" ? 0 : 1))
+ // 先处理 connect(关掉),再改 quic_ping / tcp_ping 的速率:否则中途的带宽预算会多出 connect 那一份
+ foreach (JsonNode? task in tasks["items"]!.AsArray().OrderBy(t => t!["kind"]!.GetValue() == "tcp_connect" ? 0 : 1))
{
- if (task!["scope"]!.GetValue() != "line" || task["agent_id"] is not null || task["kind"]!.GetValue() is not ("udp_echo" or "tcp_echo" or "connect"))
+ if (task!["scope"]!.GetValue() != "line" || task["agent_id"] is not null || task["kind"]!.GetValue() is not ("quic_ping" or "tcp_ping" or "tcp_connect"))
{
continue;
}
string kind = task["kind"]!.GetValue();
- var parameters = new JsonObject { ["port"] = kind == "udp_echo" ? App.QuicPorts[0] : App.TcpPorts[0] };
+ var parameters = new JsonObject { ["port"] = kind == "quic_ping" ? App.QuicPorts[0] : App.TcpPorts[0] };
// 默认的 connect 模板在这些用例里不需要(也占预算):对这个 Agent 关掉它
- bool enabled = kind != "connect";
- if (rateHz is { } hz && kind != "connect")
+ bool enabled = kind != "tcp_connect";
+ if (rateHz is { } hz && kind != "tcp_connect")
{
parameters["adaptive"] = false; // 固定频率:用例按这个速率发包,限速也按它算
parameters["rate_hz"] = hz;
diff --git a/backend/tests/MoShiro.Backend.Tests/AgentMeshTests.cs b/backend/tests/MoShiro.Backend.Tests/AgentMeshTests.cs
index 87a9beb..b38727a 100644
--- a/backend/tests/MoShiro.Backend.Tests/AgentMeshTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AgentMeshTests.cs
@@ -41,7 +41,7 @@ public sealed class AgentMeshTests : IClassFixture
await BackendFixture.EventuallyAsync(async () =>
{
JsonNode chunks = await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={listener.Id}");
- return chunks["items"]!.AsArray().Any(c => c!["kind"]!.GetValue() == "udp_echo" && c["series"]!.GetValue() == "line:" + dialer.Id);
+ return chunks["items"]!.AsArray().Any(c => c!["kind"]!.GetValue() == "quic_ping" && c["series"]!.GetValue() == "line:" + dialer.Id);
}, TimeSpan.FromSeconds(40), "the line should persist measured packet data");
}
}
diff --git a/backend/tests/MoShiro.Backend.Tests/AgentTaskSetEndToEndTests.cs b/backend/tests/MoShiro.Backend.Tests/AgentTaskSetEndToEndTests.cs
index 01c743e..c0c5ba6 100644
--- a/backend/tests/MoShiro.Backend.Tests/AgentTaskSetEndToEndTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AgentTaskSetEndToEndTests.cs
@@ -22,15 +22,15 @@ public sealed class AgentTaskSetEndToEndTests : IClassFixture
await using AgentHarness agent = await AgentHarness.StartAsync(_backend);
await agent.OverrideLineTasksAsync(rateHz: 30);
await using DialerSimulator dialer = await agent.ConnectDialerAsync();
- LineTask udp = (await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "udp_echo" && t.Port == agent.App.QuicPorts[0])))
- .Tasks.Single(t => t.Kind == "udp_echo");
+ LineTask udp = (await dialer.WaitForTasksAsync(s => s.Tasks.Any(t => t.Kind == "quic_ping" && t.Port == agent.App.QuicPorts[0])))
+ .Tasks.Single(t => t.Kind == "quic_ping");
JsonNode tasks = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/tasks");
- JsonNode mine = tasks["items"]!.AsArray().Single(t => t!["agent_id"]?.GetValue() == agent.Id && t["kind"]!.GetValue() == "udp_echo")!;
+ JsonNode mine = tasks["items"]!.AsArray().Single(t => t!["agent_id"]?.GetValue() == agent.Id && t["kind"]!.GetValue() == "quic_ping")!;
using HttpResponseMessage disabled = await _backend.Admin.PatchAsync("/api/admin/tasks/" + mine["id"]!.GetValue(), BackendFixture.Json(new JsonObject { ["enabled"] = false }, "application/merge-patch+json"));
disabled.EnsureSuccessStatusCode();
- LineTaskSet next = await dialer.WaitForTasksAsync(s => s.Tasks.All(t => t.Kind != "udp_echo") && s.Version > 0);
+ LineTaskSet next = await dialer.WaitForTasksAsync(s => s.Tasks.All(t => t.Kind != "quic_ping") && s.Version > 0);
Assert.DoesNotContain(next.Tasks, t => t.Id == udp.Id);
}
}
diff --git a/backend/tests/MoShiro.Backend.Tests/AotSmokeTests.cs b/backend/tests/MoShiro.Backend.Tests/AotSmokeTests.cs
index b091597..9ce1f8b 100644
--- a/backend/tests/MoShiro.Backend.Tests/AotSmokeTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/AotSmokeTests.cs
@@ -58,28 +58,28 @@ public sealed class AotSmokeTests
string lineId = $"{agent.NodeId.Value}~{dialerId}";
await agent.ConnectAsync(baseAddress);
TaskSet tasks = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 2);
- Assert.Contains(tasks.Tasks, t => t.Kind == "udp_echo");
+ Assert.Contains(tasks.Tasks, t => t.Kind == "quic_ping");
// 原始块:二进制通道 → 段文件与索引 → ACK
long minute = 60_000;
long baseMs = ((TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds() / minute) - 3) * minute;
for (int k = 0; k < 6; k++)
{
- Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "udp_echo", baseMs + (k * 10_000L), 100, 100, i => new EchoPacket(false, i % 10 == 0, 20_000, 20_000))));
+ Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "quic_ping", baseMs + (k * 10_000L), 100, 100, i => new EchoPacket(false, i % 10 == 0, 20_000, 20_000))));
}
// 查询:热层的精确汇总,随后是持久层的分钟窗口
string range = $"from={Iso(baseMs)}&to={Iso(baseMs + minute)}&metrics=loss.total,rtt.p95";
JsonNode hot = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1s");
Assert.Equal("hot", hot["source"]!.GetValue());
- Assert.Equal(600, hot["summary"]!["samples"]!["udp_echo"]!.GetValue());
+ Assert.Equal(600, hot["summary"]!["samples"]!["quic_ping"]!.GetValue());
Assert.Equal(10.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue(), 6);
long deadline = TimeProvider.System.GetTimestamp();
JsonNode persisted;
do
{
persisted = await GetAsync(http, $"/api/public/lines/{lineId}/series?{range}&step=1m");
- if (persisted["summary"]!["samples"]!["udp_echo"]?.GetValue() == 600)
+ if (persisted["summary"]!["samples"]!["quic_ping"]?.GetValue() == 600)
{
break;
}
@@ -88,7 +88,7 @@ public sealed class AotSmokeTests
}
while (TimeProvider.System.GetElapsedTime(deadline) < TimeSpan.FromSeconds(15));
Assert.Equal("1m", persisted["source"]!.GetValue());
- Assert.Equal(600, persisted["summary"]!["samples"]!["udp_echo"]!.GetValue());
+ Assert.Equal(600, persisted["summary"]!["samples"]!["quic_ping"]!.GetValue());
// 原始块可以按 ID 下载并解码
JsonNode chunks = await GetAsync(http, $"/api/admin/raw/chunks?agent_id={agent.NodeId.Value}&limit=1");
diff --git a/backend/tests/MoShiro.Backend.Tests/ApiBasicsTests.cs b/backend/tests/MoShiro.Backend.Tests/ApiBasicsTests.cs
index 20b4f0a..83f4018 100644
--- a/backend/tests/MoShiro.Backend.Tests/ApiBasicsTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/ApiBasicsTests.cs
@@ -194,7 +194,7 @@ public sealed class ApiBasicsTests : IClassFixture
Assert.Equal(HttpStatusCode.NotModified, notModified.StatusCode);
JsonNode kinds = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/catalog/kinds");
- JsonNode udp = kinds["items"]!.AsArray().Single(k => k!["id"]!.GetValue() == "udp_echo")!;
+ JsonNode udp = kinds["items"]!.AsArray().Single(k => k!["id"]!.GetValue() == "quic_ping")!;
JsonNode rate = udp["params"]!.AsArray().Single(p => p!["name"]!.GetValue() == "rate_hz")!;
Assert.Equal(1, rate["min"]!.GetValue());
Assert.Equal(200, rate["max"]!.GetValue());
diff --git a/backend/tests/MoShiro.Backend.Tests/BackendFixture.cs b/backend/tests/MoShiro.Backend.Tests/BackendFixture.cs
index 45943b9..5ede417 100644
--- a/backend/tests/MoShiro.Backend.Tests/BackendFixture.cs
+++ b/backend/tests/MoShiro.Backend.Tests/BackendFixture.cs
@@ -11,7 +11,7 @@ using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
-/// 首次启动种入的默认线路任务模板数:udp_echo、tcp_echo、connect。
+/// 首次启动种入的默认线路任务模板数:quic_ping、tcp_ping、connect。
internal static class Defaults
{
public const int TaskCount = 3;
diff --git a/backend/tests/MoShiro.Backend.Tests/DialerEndToEndTests.cs b/backend/tests/MoShiro.Backend.Tests/DialerEndToEndTests.cs
index 3abadf6..b676d34 100644
--- a/backend/tests/MoShiro.Backend.Tests/DialerEndToEndTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/DialerEndToEndTests.cs
@@ -59,7 +59,7 @@ public sealed class DialerEndToEndTests : IClassFixture
TimeSpan.FromSeconds(90));
Assert.True(test["status"]!.GetValue() == "succeeded", test.ToJsonString());
- await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "udp_train") >= 5, because: "the 100 Mbit/s phase (5 s of 1000 B packets, over 50000 records) is split into two fragments");
+ await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "quic_stream") >= 5, because: "the 100 Mbit/s phase (5 s of 1000 B packets, over 50000 records) is split into two fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
@@ -84,7 +84,7 @@ public sealed class DialerEndToEndTests : IClassFixture
},
TimeSpan.FromSeconds(150));
Assert.True(test["status"]!.GetValue() == "succeeded", test.ToJsonString());
- await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "tcp_bulk") >= 1, because: "the bulk phase stores its fragments");
+ await BackendFixture.EventuallyAsync(async () => await ChunkCountAsync(agent, "tcp_transfer") >= 1, because: "the bulk phase stores its fragments");
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
}
}
diff --git a/backend/tests/MoShiro.Backend.Tests/EnrollmentAndTaskSetTests.cs b/backend/tests/MoShiro.Backend.Tests/EnrollmentAndTaskSetTests.cs
index f4a50ad..e780cc8 100644
--- a/backend/tests/MoShiro.Backend.Tests/EnrollmentAndTaskSetTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/EnrollmentAndTaskSetTests.cs
@@ -24,15 +24,15 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
Assert.Equal("active", agent.EnrollmentStatus);
Assert.Equal("active", agent.AuthStatus);
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
- Assert.Equal(["connect", "tcp_echo", "udp_echo"], set.Tasks.Select(t => t.Kind).Order().ToArray());
+ Assert.Equal(["quic_ping", "tcp_connect", "tcp_ping"], set.Tasks.Select(t => t.Kind).Order().ToArray());
Assert.All(set.Tasks, t =>
{
Assert.Equal("line", t.Scope);
Assert.Equal(TaskSchedule.Continuous, t.Schedule);
Assert.NotEmpty(t.Params); // 参数已经补全了默认值
});
- Assert.Equal(10, set.Tasks.Single(t => t.Kind == "udp_echo").Params["rate_hz"].GetInt64());
- Assert.Equal(7444, set.Tasks.Single(t => t.Kind == "tcp_echo").Params["port"].GetInt64());
+ Assert.Equal(10, set.Tasks.Single(t => t.Kind == "quic_ping").Params["rate_hz"].GetInt64());
+ Assert.Equal(7444, set.Tasks.Single(t => t.Kind == "tcp_ping").Params["port"].GetInt64());
Assert.True(set.Version >= 1);
JsonNode view = await AgentAsync(agent.NodeId.Value);
@@ -46,9 +46,9 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
[Fact]
public async Task TaskSet_OnlyContainsKindsTheAgentSupports()
{
- await using AgentSimulator agent = await _backend.ConnectAgentAsync(kinds: ["udp_echo"]);
+ await using AgentSimulator agent = await _backend.ConnectAgentAsync(kinds: ["quic_ping"]);
TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Count >= 1);
- Assert.Equal(["udp_echo"], set.Tasks.Select(t => t.Kind).ToArray());
+ Assert.Equal(["quic_ping"], set.Tasks.Select(t => t.Kind).ToArray());
}
[Fact]
@@ -265,12 +265,12 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
{
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
TaskSet initial = await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
- TaskSpec udp = initial.Tasks.Single(t => t.Kind == "udp_echo");
+ TaskSpec udp = initial.Tasks.Single(t => t.Kind == "quic_ping");
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
{
["scope"] = "line",
- ["kind"] = "udp_echo",
+ ["kind"] = "quic_ping",
["agent_id"] = agent.NodeId.Value,
["base_task_id"] = udp.Id.Value,
["params"] = new JsonObject { ["rate_hz"] = 20 },
@@ -278,15 +278,15 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
Assert.StartsWith("/api/admin/tasks/", created.Headers.Location!.ToString());
- TaskSet updated = await agent.WaitForTaskSetAsync(s => s.Version > initial.Version && s.Tasks.Any(t => t.Kind == "udp_echo" && t.Params["rate_hz"].GetInt64() == 20));
+ TaskSet updated = await agent.WaitForTaskSetAsync(s => s.Version > initial.Version && s.Tasks.Any(t => t.Kind == "quic_ping" && t.Params["rate_hz"].GetInt64() == 20));
Assert.Equal(Defaults.TaskCount, updated.Tasks.Count);
- Assert.Equal(7443, updated.Tasks.Single(t => t.Kind == "udp_echo").Params["port"].GetInt64());
+ Assert.Equal(7443, updated.Tasks.Single(t => t.Kind == "quic_ping").Params["port"].GetInt64());
// 停用覆盖 → 该 Agent 不再运行这条任务(模板对其他 Agent 不受影响)
string overrideId = JsonNode.Parse(await created.Content.ReadAsStringAsync())!["id"]!.GetValue();
using HttpResponseMessage disable = await _backend.Admin.PatchAsync("/api/admin/tasks/" + overrideId, BackendFixture.Json(new JsonObject { ["enabled"] = false }, "application/merge-patch+json"));
Assert.Equal(HttpStatusCode.OK, disable.StatusCode);
- TaskSet disabled = await agent.WaitForTaskSetAsync(s => s.Version > updated.Version && s.Tasks.All(t => t.Kind != "udp_echo"));
+ TaskSet disabled = await agent.WaitForTaskSetAsync(s => s.Version > updated.Version && s.Tasks.All(t => t.Kind != "quic_ping"));
Assert.Equal(Defaults.TaskCount - 1, disabled.Tasks.Count);
}
@@ -295,7 +295,7 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
{
using HttpResponseMessage badParams = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
{
- ["scope"] = "line", ["kind"] = "udp_echo", ["params"] = new JsonObject { ["rate_hz"] = 5000, ["bogus"] = 1 },
+ ["scope"] = "line", ["kind"] = "quic_ping", ["params"] = new JsonObject { ["rate_hz"] = 5000, ["bogus"] = 1 },
}));
Assert.Equal(HttpStatusCode.BadRequest, badParams.StatusCode);
JsonNode problem = JsonNode.Parse(await badParams.Content.ReadAsStringAsync())!;
@@ -308,33 +308,107 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
using HttpResponseMessage overBudget = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
{
- ["scope"] = "line", ["kind"] = "udp_echo", ["params"] = new JsonObject { ["rate_hz"] = 200, ["size"] = 1400 },
+ ["scope"] = "line", ["kind"] = "quic_ping", ["params"] = new JsonObject { ["rate_hz"] = 200, ["size"] = 1400 },
}));
Assert.Equal(HttpStatusCode.BadRequest, overBudget.StatusCode);
Assert.Equal("budget_exceeded", JsonNode.Parse(await overBudget.Content.ReadAsStringAsync())!["code"]!.GetValue());
- using HttpResponseMessage wrongScope = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "icmp_echo" }));
+ using HttpResponseMessage wrongScope = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "icmp_ping" }));
Assert.Equal(HttpStatusCode.BadRequest, wrongScope.StatusCode);
- // 目标任务:只接受 IP(不做 DNS 解析),需要指定 Agent
+ // 单个 Agent 的任务:目标的格式按 Kind 检查(Backend 不解析域名),需要指定 Agent
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
- using HttpResponseMessage dns = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ foreach ((string kind, string badTarget) in new[] { ("icmp_ping", "https://example.com/"), ("icmp_ping", "not a host"), ("http_request", "example.com") })
{
- ["scope"] = "target", ["kind"] = "icmp_echo", ["agent_id"] = agent.NodeId.Value, ["target"] = "example.com",
+ using HttpResponseMessage malformed = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = kind, ["agent_id"] = agent.NodeId.Value, ["target"] = badTarget,
+ }));
+ Assert.Equal(HttpStatusCode.BadRequest, malformed.StatusCode);
+ }
+
+ using HttpResponseMessage page = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = "http_request", ["agent_id"] = agent.NodeId.Value, ["target"] = "https://Example.com/health", ["params"] = new JsonObject { ["version"] = "h3" },
}));
- Assert.Equal(HttpStatusCode.BadRequest, dns.StatusCode);
+ Assert.Equal(HttpStatusCode.Created, page.StatusCode);
+ Assert.Equal("https://example.com/health", JsonNode.Parse(await page.Content.ReadAsStringAsync())!["target"]!.GetValue());
using HttpResponseMessage target = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
{
- ["scope"] = "target", ["kind"] = "icmp_echo", ["agent_id"] = agent.NodeId.Value, ["target"] = "1.1.1.1",
+ ["scope"] = "target", ["kind"] = "icmp_ping", ["agent_id"] = agent.NodeId.Value, ["target"] = "1.1.1.1",
}));
Assert.Equal(HttpStatusCode.Created, target.StatusCode);
- TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Any(t => t.Scope == "target"));
- Assert.Equal("1.1.1.1", set.Tasks.Single(t => t.Scope == "target").Target);
+ TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Any(t => t.Scope == "target" && t.Kind == "icmp_ping"));
+ Assert.Equal("1.1.1.1", set.Tasks.Single(t => t.Scope == "target" && t.Kind == "icmp_ping").Target);
- using HttpResponseMessage onceSchedule = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "udp_echo", ["schedule"] = "every:30" }));
+ using HttpResponseMessage onceSchedule = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject { ["scope"] = "line", ["kind"] = "quic_ping", ["schedule"] = "every:30" }));
Assert.Equal(HttpStatusCode.BadRequest, onceSchedule.StatusCode);
}
+ [Fact]
+ public async Task TargetTemplate_AppliesToEveryAgent_AndAnOverrideCanDisableOrRetargetItForOne()
+ {
+ await using AgentSimulator first = await _backend.ConnectAgentAsync();
+ await using AgentSimulator second = await _backend.ConnectAgentAsync();
+ using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = "tcp_connect", ["target"] = "Example.com:443",
+ }));
+ Assert.Equal(HttpStatusCode.Created, created.StatusCode);
+ JsonNode template = JsonNode.Parse(await created.Content.ReadAsStringAsync())!;
+ string templateId = template["id"]!.GetValue();
+ Assert.Null(template["agent_id"]);
+ try
+ {
+ foreach (AgentSimulator agent in new[] { first, second })
+ {
+ TaskSet set = await agent.WaitForTaskSetAsync(s => s.Tasks.Any(t => t.Kind == "tcp_connect" && t.Scope == "target"));
+ Assert.Equal("example.com:443", set.Tasks.Single(t => t.Kind == "tcp_connect" && t.Scope == "target").Target);
+ }
+
+ // 展示接口列出每个 Agent 实际执行的目标;模拟的 Agent 没有上传数据,所以是「等待数据」
+ JsonNode listed = await _backend.GetJsonAsync(_backend.Admin, "/api/public/targets");
+ string[] both = [first.NodeId.Value, second.NodeId.Value];
+ JsonNode[] mine = [.. listed["items"]!.AsArray().Select(i => i!).Where(i => i["task_id"]!.GetValue() == templateId && both.Contains(i["agent_id"]!.GetValue()))];
+ Assert.Equal(both.Order(), mine.Select(i => i["agent_id"]!.GetValue()).Order());
+ Assert.All(mine, i =>
+ {
+ Assert.Equal("example.com:443", i["target"]!.GetValue());
+ Assert.Equal("waiting", i["status"]!.GetValue());
+ Assert.Null(i["last_sample"]);
+ });
+
+ JsonNode one = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/targets?agent_id={first.NodeId.Value}&task_id={templateId}&full=true");
+ Assert.Equal(first.NodeId.Value, Assert.Single(one["items"]!.AsArray())!["agent_id"]!.GetValue());
+
+ // 覆盖:第一个 Agent 换一个目标,第二个 Agent 停用
+ using HttpResponseMessage retarget = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = "tcp_connect", ["agent_id"] = first.NodeId.Value, ["base_task_id"] = templateId, ["target"] = "192.0.2.9:8443",
+ }));
+ Assert.Equal(HttpStatusCode.Created, retarget.StatusCode);
+ using HttpResponseMessage off = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = "tcp_connect", ["agent_id"] = second.NodeId.Value, ["base_task_id"] = templateId, ["enabled"] = false,
+ }));
+ Assert.Equal(HttpStatusCode.Created, off.StatusCode);
+ TaskSet firstSet = await first.WaitForTaskSetAsync(s => s.Tasks.Any(t => t.Target == "192.0.2.9:8443"));
+ Assert.Single(firstSet.Tasks, t => t.Kind == "tcp_connect" && t.Scope == "target");
+ await second.WaitForTaskSetAsync(s => s.Tasks.All(t => t.Scope != "target"));
+
+ // 覆盖只能指向同类的单个 Agent 模板
+ using HttpResponseMessage wrongKind = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
+ {
+ ["scope"] = "target", ["kind"] = "icmp_ping", ["agent_id"] = first.NodeId.Value, ["base_task_id"] = templateId,
+ }));
+ Assert.Equal(HttpStatusCode.BadRequest, wrongKind.StatusCode);
+ }
+ finally
+ {
+ using HttpResponseMessage deleted = await _backend.Admin.DeleteAsync("/api/admin/tasks/" + templateId);
+ }
+ }
+
[Fact]
public async Task TaskSet_RejectedItemsReportedByTheAgent_AreVisibleToTheAdmin()
{
@@ -344,7 +418,7 @@ public sealed class EnrollmentAndTaskSetTests : IClassFixture
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/tasks", BackendFixture.Json(new JsonObject
{
- ["scope"] = "target", ["kind"] = "connect", ["agent_id"] = agent.NodeId.Value, ["target"] = "192.0.2.1:443",
+ ["scope"] = "target", ["kind"] = "tcp_connect", ["agent_id"] = agent.NodeId.Value, ["target"] = "192.0.2.1:443",
}));
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
await BackendFixture.EventuallyAsync(async () => (await AgentAsync(agent.NodeId.Value))["rejected"]!.AsArray().Count == 1);
diff --git a/backend/tests/MoShiro.Backend.Tests/IngestAndQueryTests.cs b/backend/tests/MoShiro.Backend.Tests/IngestAndQueryTests.cs
index 153d638..5873148 100644
--- a/backend/tests/MoShiro.Backend.Tests/IngestAndQueryTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/IngestAndQueryTests.cs
@@ -29,7 +29,7 @@ public sealed class IngestAndQueryTests : IClassFixture
{
long start = baseMs + (k * 10_000L);
RawChunk udp = ChunkFactory.Echo(
- agent.NodeId, "udp_echo", start, 100, 100,
+ agent.NodeId, "quic_ping", start, 100, 100,
i => new EchoPacket(false, lossEvery > 0 && i % lossEvery == 0, (long)(owdMs * 1000), (long)(owdMs * 1000)));
Assert.True(await agent.SendChunkAsync(udp));
if (tcp)
@@ -37,7 +37,7 @@ public sealed class IngestAndQueryTests : IClassFixture
long dialerBase = ChunkFactory.DialerUs(0);
RawStream info = ChunkFactory.TcpInfo(Enumerable.Range(0, 10).Select(s => (dialerBase + (s * 1_000_000L), (byte)0, 45_000u, (uint)(s * 2), (uint)(s * 400))));
RawChunk tcpChunk = ChunkFactory.Echo(
- agent.NodeId, "tcp_echo", start, 200, 50, _ => new EchoPacket(false, false, (long)(owdMs * 1000), (long)(owdMs * 1000)), extraStreams: [info]);
+ agent.NodeId, "tcp_ping", start, 200, 50, _ => new EchoPacket(false, false, (long)(owdMs * 1000), (long)(owdMs * 1000)), extraStreams: [info]);
Assert.True(await agent.SendChunkAsync(tcpChunk));
}
}
@@ -87,13 +87,13 @@ public sealed class IngestAndQueryTests : IClassFixture
// Anchored at now: the last 5 minutes are empty
JsonNode atNow = await SeriesAsync(id, "range=5m&step=10s&metrics=loss.total");
- Assert.Null(atNow["summary"]!["samples"]!["udp_echo"]);
+ Assert.Null(atNow["summary"]!["samples"]!["quic_ping"]);
// Anchored at the latest sample: the window ends where the data ends and carries all of it
JsonNode anchored = await SeriesAsync(id, "range=5m&step=10s&metrics=loss.total&anchor=latest");
long to = DateTimeOffset.Parse(anchored["to"]!.GetValue(), System.Globalization.CultureInfo.InvariantCulture).ToUnixTimeMilliseconds();
Assert.InRange(to, baseMs + (50 * 1000), baseMs + (80 * 1000));
- Assert.Equal(600, anchored["summary"]!["samples"]!["udp_echo"]!.GetValue());
+ Assert.Equal(600, anchored["summary"]!["samples"]!["quic_ping"]!.GetValue());
Assert.False(anchored["live"]!.GetValue());
// 这个窗口是以“最后一个样本”为终点的绝对区间,但线路恢复之后同一个地址的内容就变了:不能被浏览器永久缓存
@@ -102,7 +102,7 @@ public sealed class IngestAndQueryTests : IClassFixture
// anchor=now is the default behaviour; anything else is rejected
JsonNode explicitNow = await SeriesAsync(id, "range=5m&step=10s&metrics=loss.total&anchor=now");
- Assert.Null(explicitNow["summary"]!["samples"]!["udp_echo"]);
+ Assert.Null(explicitNow["summary"]!["samples"]!["quic_ping"]);
using HttpResponseMessage bad = await _backend.Admin.GetAsync($"/api/public/lines/{_backend.Line(id)}/series?range=5m&anchor=yesterday");
Assert.Equal(System.Net.HttpStatusCode.BadRequest, bad.StatusCode);
}
@@ -162,8 +162,8 @@ public sealed class IngestAndQueryTests : IClassFixture
{
minute = await SeriesAsync(agent.NodeId.Value, range + "&step=1m");
// 窗口会随着后续的块到达而重算:等到 18 个块的样本都计入之后再比较
- return minute["source"]!.GetValue() == "1m" && minute["summary"]!["samples"]!["udp_echo"]?.GetValue() == 1800
- && minute["summary"]!["samples"]!["tcp_echo"]?.GetValue() == 3600;
+ return minute["source"]!.GetValue() == "1m" && minute["summary"]!["samples"]!["quic_ping"]?.GetValue() == 1800
+ && minute["summary"]!["samples"]!["tcp_ping"]?.GetValue() == 3600;
});
JsonNode hotSummary = hot["summary"]!["metrics"]!;
@@ -176,9 +176,9 @@ public sealed class IngestAndQueryTests : IClassFixture
$"{id}: hot={hotSummary[id]} persisted={minuteSummary[id]}");
}
- Assert.Equal(1800, hot["summary"]!["samples"]!["udp_echo"]!.GetValue());
- Assert.Equal(hot["summary"]!["samples"]!["udp_echo"]!.GetValue(), minute["summary"]!["samples"]!["udp_echo"]!.GetValue());
- Assert.Equal(3600, minute["summary"]!["samples"]!["tcp_echo"]!.GetValue());
+ Assert.Equal(1800, hot["summary"]!["samples"]!["quic_ping"]!.GetValue());
+ Assert.Equal(hot["summary"]!["samples"]!["quic_ping"]!.GetValue(), minute["summary"]!["samples"]!["quic_ping"]!.GetValue());
+ Assert.Equal(3600, minute["summary"]!["samples"]!["tcp_ping"]!.GetValue());
// RTT = 2 × 20 ms,直方图的相对误差 ≤ 4.5%
Assert.InRange(minuteSummary["rtt.p95"]!.GetValue(), 40 * 0.955, 40 * 1.045);
@@ -234,7 +234,7 @@ public sealed class IngestAndQueryTests : IClassFixture
Assert.True(goodScore > 90, $"clean line scored {goodScore}");
Assert.True(badScore < goodScore - 30, $"lossy line scored {badScore}");
Assert.Equal(20.0, lines[badIndex]["summary"]!["metrics"]!["loss.total"]!.GetValue(), 6);
- Assert.Equal(1200, lines[goodIndex]["summary"]!["samples"]!["udp_echo"]!.GetValue());
+ Assert.Equal(1200, lines[goodIndex]["summary"]!["samples"]!["quic_ping"]!.GetValue());
// 按指标排序:loss.total 升序时好线路在前
JsonNode byLoss = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/overview?" + range + "&sort=loss.total&order=asc");
@@ -372,12 +372,12 @@ public sealed class IngestAndQueryTests : IClassFixture
await using (agent)
{
string url = $"/api/public/lines/{_backend.Line(agent.NodeId.Value)}/series?from={Iso(baseMs)}&to={Iso(baseMs + Minute)}&step=1m&preset=udp";
- await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url))["summary"]!["samples"]!["udp_echo"]?.GetValue() == 600);
+ await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url))["summary"]!["samples"]!["quic_ping"]?.GetValue() == 600);
// Agent 重连后补传一个迟到的块(落在同一个分钟窗口里)
- RawChunk late = ChunkFactory.Echo(agent.NodeId, "udp_echo", baseMs + 5_000, 100, 10, _ => new EchoPacket(false, false, 20_000, 20_000));
+ RawChunk late = ChunkFactory.Echo(agent.NodeId, "quic_ping", baseMs + 5_000, 100, 10, _ => new EchoPacket(false, false, 20_000, 20_000));
Assert.True(await agent.SendChunkAsync(late));
- await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url))["summary"]!["samples"]!["udp_echo"]?.GetValue() == 700, because: "the late chunk must re-enter the persistent path");
+ await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url))["summary"]!["samples"]!["quic_ping"]?.GetValue() == 700, because: "the late chunk must re-enter the persistent path");
}
}
@@ -394,7 +394,7 @@ public sealed class IngestAndQueryTests : IClassFixture
await BackendFixture.EventuallyAsync(async () =>
{
oneMinute = await SeriesAsync(agent.NodeId.Value, range + "&step=1m");
- return oneMinute["summary"]!["samples"]!["udp_echo"]?.GetValue() == 3000;
+ return oneMinute["summary"]!["samples"]!["quic_ping"]?.GetValue() == 3000;
});
// 5 分钟层由降采样把 5 个 1 分钟窗口精确合并而来
@@ -402,7 +402,7 @@ public sealed class IngestAndQueryTests : IClassFixture
await BackendFixture.EventuallyAsync(async () =>
{
fiveMinutes = await SeriesAsync(agent.NodeId.Value, range + "&step=5m");
- return fiveMinutes["source"]!.GetValue() == "5m" && fiveMinutes["summary"]!["samples"]!["udp_echo"]?.GetValue() == 3000;
+ return fiveMinutes["source"]!.GetValue() == "5m" && fiveMinutes["summary"]!["samples"]!["quic_ping"]?.GetValue() == 3000;
});
Assert.Equal(oneMinute["summary"]!["metrics"]!["loss.total"]!.GetValue(), fiveMinutes["summary"]!["metrics"]!["loss.total"]!.GetValue(), 9);
Assert.Equal(oneMinute["summary"]!["metrics"]!["rtt.p99"]!.GetValue(), fiveMinutes["summary"]!["metrics"]!["rtt.p99"]!.GetValue(), 9);
diff --git a/backend/tests/MoShiro.Backend.Tests/Ipv6Tests.cs b/backend/tests/MoShiro.Backend.Tests/Ipv6Tests.cs
index 0572b53..ad5e9f9 100644
--- a/backend/tests/MoShiro.Backend.Tests/Ipv6Tests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/Ipv6Tests.cs
@@ -30,7 +30,7 @@ public sealed class Ipv6Tests : IClassFixture
{
foreach (string version in new[] { IpVersions.V4, IpVersions.V6 })
{
- RawChunk chunk = ChunkFactory.Echo(agent.NodeId, "udp_echo", start + k * 10_000, 100, 100,
+ RawChunk chunk = ChunkFactory.Echo(agent.NodeId, "quic_ping", start + k * 10_000, 100, 100,
i => new EchoPacket(false, version == IpVersions.V6 && i % 2 == 0, 20_000, 20_000));
chunk.Header.DialerId = new NodeId(_backend.DefaultDialerId);
chunk.Header.IpVersion = version;
@@ -50,7 +50,7 @@ public sealed class Ipv6Tests : IClassFixture
await BackendFixture.EventuallyAsync(async () =>
{
series = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/lines/{line}/series?{range}&step={step}&preset=udp");
- return series["summary"]!["samples"]!["udp_echo"]?.GetValue() == 600;
+ return series["summary"]!["samples"]!["quic_ping"]?.GetValue() == 600;
});
Assert.Equal(loss, series["summary"]!["metrics"]!["loss.total"]!.GetValue(), 6);
}
@@ -99,7 +99,7 @@ public sealed class Ipv6Tests : IClassFixture
{
JsonNode raw = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/raw/chunks?agent_id=" + agent.Id);
return new[] { "line:" + dialer.DialerId, "line:" + dialer.DialerId + ":ipv6" }.All(series =>
- new[] { "udp_echo", "tcp_echo" }.All(kind => raw["items"]!.AsArray().Any(c => c!["series"]!.GetValue() == series && c["kind"]!.GetValue() == kind)));
+ new[] { "quic_ping", "tcp_ping" }.All(kind => raw["items"]!.AsArray().Any(c => c!["series"]!.GetValue() == series && c["kind"]!.GetValue() == kind)));
}, TimeSpan.FromSeconds(45));
string v4 = agent.Id + "~" + dialer.DialerId;
@@ -110,8 +110,8 @@ public sealed class Ipv6Tests : IClassFixture
{
JsonNode series = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/lines/{line}/series?range=1m&step={step}&preset=udp,tcp");
Assert.Equal(family, series["ip_version"]!.GetValue());
- Assert.True(series["summary"]!["samples"]!["udp_echo"]!.GetValue() > 0, series.ToJsonString());
- Assert.True(series["summary"]!["samples"]!["tcp_echo"]!.GetValue() > 0, series.ToJsonString());
+ Assert.True(series["summary"]!["samples"]!["quic_ping"]!.GetValue() > 0, series.ToJsonString());
+ Assert.True(series["summary"]!["samples"]!["tcp_ping"]!.GetValue() > 0, series.ToJsonString());
}
}
diff --git a/backend/tests/MoShiro.Backend.Tests/LiveTests.cs b/backend/tests/MoShiro.Backend.Tests/LiveTests.cs
index d22581b..cf9b596 100644
--- a/backend/tests/MoShiro.Backend.Tests/LiveTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/LiveTests.cs
@@ -34,9 +34,9 @@ public sealed class LiveTests : IClassFixture
await sse.WaitForAsync(e => e.Name == "series" && e.Data!["agent_id"]!.GetValue() == id);
// 新块到达之后:推送新的 1s 桶(列式)和最新的汇总
- RawChunk chunk = ChunkFactory.Echo(agent.NodeId, "udp_echo", Now() - 10_000, 100, 100, i => new EchoPacket(false, i % 10 == 0, 20_000, 20_000));
+ RawChunk chunk = ChunkFactory.Echo(agent.NodeId, "quic_ping", Now() - 10_000, 100, 100, i => new EchoPacket(false, i % 10 == 0, 20_000, 20_000));
Assert.True(await agent.SendChunkAsync(chunk));
- SseEvent series = await sse.WaitForAsync(e => e.Name == "series" && e.Data!["summary"]!["samples"]!["udp_echo"]?.GetValue() >= 100);
+ SseEvent series = await sse.WaitForAsync(e => e.Name == "series" && e.Data!["summary"]!["samples"]!["quic_ping"]?.GetValue() >= 100);
Assert.True(series.Data!["live"]!.GetValue());
Assert.Equal("1s", series.Data["step"]!.GetValue());
Assert.Equal(series.Data["t"]!.AsArray().Count, series.Data["values"]!["loss.total"]!.AsArray().Count);
diff --git a/backend/tests/MoShiro.Backend.Tests/OnDemandTestsTests.cs b/backend/tests/MoShiro.Backend.Tests/OnDemandTestsTests.cs
index 5c50c98..889d7b2 100644
--- a/backend/tests/MoShiro.Backend.Tests/OnDemandTestsTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/OnDemandTestsTests.cs
@@ -82,7 +82,7 @@ public sealed class OnDemandTestsTests : IClassFixture
TestRunParams run = agent.TestRuns.Single();
Assert.Equal(testId, run.TestId.Value);
Assert.Equal([10d, 25d, 50d, 100d], run.Phases.Select(p => p.Params["mbps"].GetDouble()).ToArray());
- Assert.All(run.Phases, p => Assert.Equal("udp_train", p.Kind));
+ Assert.All(run.Phases, p => Assert.Equal("quic_stream", p.Kind));
Assert.All(run.Phases, p => Assert.Equal(1250, p.Params["size"].GetInt64()));
// 同一条线路同时只能跑一个测试
@@ -227,7 +227,7 @@ public sealed class OnDemandTestsTests : IClassFixture
RawChunk chunk = ChunkFactory.UdpEchoClean(agent.NodeId, now - 120_000);
Assert.True(await agent.SendChunkAsync(chunk));
- JsonNode list = await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.NodeId.Value}&kind=udp_echo");
+ JsonNode list = await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.NodeId.Value}&kind=quic_ping");
JsonNode item = list["items"]!.AsArray().Single()!;
Assert.Equal(chunk.Header.ChunkId.ToString(), item["chunk_id"]!.GetValue());
Assert.Equal("line:" + _backend.DefaultDialerId, item["series"]!.GetValue());
diff --git a/backend/tests/MoShiro.Backend.Tests/OperationsAndSecurityTests.cs b/backend/tests/MoShiro.Backend.Tests/OperationsAndSecurityTests.cs
index c49e4b6..fab7802 100644
--- a/backend/tests/MoShiro.Backend.Tests/OperationsAndSecurityTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/OperationsAndSecurityTests.cs
@@ -42,7 +42,7 @@ public sealed class OperationsAndSecurityTests : IClassFixture
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
for (int k = 0; k < 12; k++)
{
- Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "udp_echo", baseMs + (k * 10_000L), 100, 100, _ => new EchoPacket(false, false, 30_000, 30_000))));
+ Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "quic_ping", baseMs + (k * 10_000L), 100, 100, _ => new EchoPacket(false, false, 30_000, 30_000))));
}
string url = $"/api/public/lines/{_backend.Line(agent.NodeId.Value)}/series?from={Iso(baseMs)}&to={Iso(baseMs + (2 * Minute))}&step=1m&metrics=rtt.p95,loss.total";
@@ -50,7 +50,7 @@ public sealed class OperationsAndSecurityTests : IClassFixture
await BackendFixture.EventuallyAsync(async () =>
{
before = await _backend.GetJsonAsync(_backend.Admin, url);
- return before["summary"]!["samples"]!["udp_echo"]?.GetValue() == 1200 && before["source"]!.GetValue() == "1m";
+ return before["summary"]!["samples"]!["quic_ping"]?.GetValue() == 1200 && before["source"]!.GetValue() == "1m";
});
double rtt = before["summary"]!["metrics"]!["rtt.p95"]!.GetValue();
Assert.InRange(rtt, 60 * 0.955, 60 * 1.045);
diff --git a/backend/tests/MoShiro.Backend.Tests/PersistenceTests.cs b/backend/tests/MoShiro.Backend.Tests/PersistenceTests.cs
index 87be2b7..e986bb4 100644
--- a/backend/tests/MoShiro.Backend.Tests/PersistenceTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/PersistenceTests.cs
@@ -21,14 +21,14 @@ public sealed class PersistenceTests : IAsyncLifetime
string id = agent.NodeId.Value;
for (int k = 0; k < 12; k++)
{
- Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "udp_echo", baseMs + (k * 10_000L), 100, 100, i => new EchoPacket(false, i % 4 == 0, 20_000, 20_000))));
+ Assert.True(await agent.SendChunkAsync(ChunkFactory.Echo(agent.NodeId, "quic_ping", baseMs + (k * 10_000L), 100, 100, i => new EchoPacket(false, i % 4 == 0, 20_000, 20_000))));
}
string from = DateTimeOffset.FromUnixTimeMilliseconds(baseMs).UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'");
string to = DateTimeOffset.FromUnixTimeMilliseconds(baseMs + (2 * minute)).UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'");
string url = $"/api/public/lines/{_backend.Line(id)}/series?from={from}&to={to}&metrics=loss.total,rtt.p95";
JsonNode before = await _backend.GetJsonAsync(_backend.Admin, url + "&step=1m");
- await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url + "&step=1m"))["summary"]!["samples"]!["udp_echo"]?.GetValue() == 1200);
+ await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url + "&step=1m"))["summary"]!["samples"]!["quic_ping"]?.GetValue() == 1200);
before = await _backend.GetJsonAsync(_backend.Admin, url + "&step=1m");
await agent.DisposeAsync();
@@ -51,7 +51,7 @@ public sealed class PersistenceTests : IAsyncLifetime
await BackendFixture.EventuallyAsync(async () =>
{
hot = await _backend.GetJsonAsync(admin, url + "&step=1s");
- return hot["source"]!.GetValue() == "hot" && hot["summary"]!["samples"]!["udp_echo"]?.GetValue() == 1200;
+ return hot["source"]!.GetValue() == "hot" && hot["summary"]!["samples"]!["quic_ping"]?.GetValue() == 1200;
});
Assert.Equal(25.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue(), 6);
diff --git a/backend/tests/MoShiro.Backend.Tests/ScheduleTests.cs b/backend/tests/MoShiro.Backend.Tests/ScheduleTests.cs
index c64713c..68bed87 100644
--- a/backend/tests/MoShiro.Backend.Tests/ScheduleTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/ScheduleTests.cs
@@ -72,8 +72,8 @@ public sealed class ScheduleTests : IClassFixture
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
JsonArray items = JsonNode.Parse(await response.Content.ReadAsStringAsync())!["items"]!.AsArray();
string[] kinds = [.. items.Select(i => i!["kind"]!.GetValue())];
- Assert.Contains("connect", kinds);
- Assert.Contains("udp_echo", kinds);
- Assert.Contains("tcp_echo", kinds);
+ Assert.Contains("tcp_connect", kinds);
+ Assert.Contains("quic_ping", kinds);
+ Assert.Contains("tcp_ping", kinds);
}
}
diff --git a/backend/tests/MoShiro.Backend.Tests/StorageTests.cs b/backend/tests/MoShiro.Backend.Tests/StorageTests.cs
index f30a9f0..fcdf763 100644
--- a/backend/tests/MoShiro.Backend.Tests/StorageTests.cs
+++ b/backend/tests/MoShiro.Backend.Tests/StorageTests.cs
@@ -48,8 +48,8 @@ public sealed class StorageTests : IDisposable
public async Task Raw_AppendReadAndDedupe()
{
var raw = new SqliteRawStore(_db);
- RawChunkAppend a = Chunk(s_agent, "udp_echo", 1_000, "2026-01-01");
- RawChunkAppend b = Chunk(s_agent, "udp_echo", 11_000, "2026-01-01");
+ RawChunkAppend a = Chunk(s_agent, "quic_ping", 1_000, "2026-01-01");
+ RawChunkAppend b = Chunk(s_agent, "quic_ping", 11_000, "2026-01-01");
Assert.True(await raw.AppendAsync(a, default));
Assert.True(await raw.AppendAsync(b, default));
@@ -57,7 +57,7 @@ public sealed class StorageTests : IDisposable
IReadOnlyList metas = await raw.FindAsync(new RawQuery(s_agent), default);
Assert.Equal(2, metas.Count);
- Assert.Equal("2026-01-01/agt_storage/udp_echo.seg", metas[0].SegmentPath);
+ Assert.Equal("2026-01-01/agt_storage/quic_ping.seg", metas[0].SegmentPath);
Assert.Equal(0, metas[0].Offset);
Assert.Equal(metas[0].Length, metas[1].Offset); // 同一个段文件里依次追加
Assert.Equal(a.Bytes.ToArray(), await raw.ReadAsync(metas[0], default));
@@ -72,7 +72,7 @@ public sealed class StorageTests : IDisposable
var raw = new SqliteRawStore(_db);
var evil = new NodeId("../../../outside");
Assert.True(await raw.AppendAsync(Chunk(evil, "../etc", 1, "2026-01-01"), default));
- Assert.True(await raw.AppendAsync(Chunk(s_agent, "udp_echo", 2, "2026-01-01", test: new TestId("..\\..\\x"), phase: 0), default));
+ Assert.True(await raw.AppendAsync(Chunk(s_agent, "quic_ping", 2, "2026-01-01", test: new TestId("..\\..\\x"), phase: 0), default));
foreach (string file in Directory.EnumerateFiles(_dir, "*.seg", SearchOption.AllDirectories))
{
@@ -80,7 +80,7 @@ public sealed class StorageTests : IDisposable
}
Assert.False(File.Exists(Path.Combine(_dir, "outside")));
- await Assert.ThrowsAsync(async () => await raw.AppendAsync(Chunk(s_agent, "udp_echo", 3, "../2026"), default));
+ await Assert.ThrowsAsync(async () => await raw.AppendAsync(Chunk(s_agent, "quic_ping", 3, "../2026"), default));
}
[Fact]
@@ -90,19 +90,19 @@ public sealed class StorageTests : IDisposable
var testId = new TestId("tst_one");
for (int i = 0; i < 6; i++)
{
- await raw.AppendAsync(Chunk(s_agent, "udp_echo", i * 10_000L, "2026-01-01"), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_ping", i * 10_000L, "2026-01-01"), default);
}
- await raw.AppendAsync(Chunk(s_agent, "tcp_echo", 5_000, "2026-01-01"), default);
- await raw.AppendAsync(Chunk(new NodeId("agt_other"), "udp_echo", 5_000, "2026-01-01"), default);
- await raw.AppendAsync(Chunk(s_agent, "udp_train", 0, "2026-01-01", test: testId, phase: 1), default);
+ await raw.AppendAsync(Chunk(s_agent, "tcp_ping", 5_000, "2026-01-01"), default);
+ await raw.AppendAsync(Chunk(new NodeId("agt_other"), "quic_ping", 5_000, "2026-01-01"), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_stream", 0, "2026-01-01", test: testId, phase: 1), default);
Assert.Equal(8, (await raw.FindAsync(new RawQuery(s_agent), default)).Count);
- Assert.Equal(6, (await raw.FindAsync(new RawQuery(s_agent, Kind: "udp_echo"), default)).Count);
+ Assert.Equal(6, (await raw.FindAsync(new RawQuery(s_agent, Kind: "quic_ping"), default)).Count);
Assert.Single(await raw.FindAsync(new RawQuery(TestId: testId), default));
- Assert.Equal(3, (await raw.FindAsync(new RawQuery(s_agent, Kind: "udp_echo", FromMs: 10_000, ToMs: 40_000), default)).Count);
- Assert.Equal(2, (await raw.FindAsync(new RawQuery(s_agent, Kind: "udp_echo", Limit: 2), default)).Count);
- Assert.Equal(4, (await raw.FindAsync(new RawQuery(s_agent, Kind: "udp_echo", AfterStartTs: 20_000), default)).Count);
+ Assert.Equal(3, (await raw.FindAsync(new RawQuery(s_agent, Kind: "quic_ping", FromMs: 10_000, ToMs: 40_000), default)).Count);
+ Assert.Equal(2, (await raw.FindAsync(new RawQuery(s_agent, Kind: "quic_ping", Limit: 2), default)).Count);
+ Assert.Equal(4, (await raw.FindAsync(new RawQuery(s_agent, Kind: "quic_ping", AfterStartTs: 20_000), default)).Count);
IReadOnlyList<(NodeId AgentId, string Series)> series = await raw.ListSeriesAsync(0, 100_000, default);
Assert.Equal(2, series.Count); // 测试的块不算持续序列
@@ -112,9 +112,9 @@ public sealed class StorageTests : IDisposable
public async Task Raw_RecoverTruncatesAnUnindexedTail_AndLaterAppendsStayConsistent()
{
var raw = new SqliteRawStore(_db);
- RawChunkAppend first = Chunk(s_agent, "udp_echo", 1, "2026-01-01");
+ RawChunkAppend first = Chunk(s_agent, "quic_ping", 1, "2026-01-01");
await raw.AppendAsync(first, default);
- string segment = Directory.EnumerateFiles(_dir, "udp_echo.seg", SearchOption.AllDirectories).Single();
+ string segment = Directory.EnumerateFiles(_dir, "quic_ping.seg", SearchOption.AllDirectories).Single();
long indexedEnd = new FileInfo(segment).Length;
// 追加之后、写索引之前崩溃:段文件尾部多了没有索引记录的字节
@@ -124,7 +124,7 @@ public sealed class StorageTests : IDisposable
await raw.RecoverAsync(default);
Assert.Equal(indexedEnd, new FileInfo(segment).Length);
- RawChunkAppend second = Chunk(s_agent, "udp_echo", 2, "2026-01-01");
+ RawChunkAppend second = Chunk(s_agent, "quic_ping", 2, "2026-01-01");
await raw.AppendAsync(second, default);
IReadOnlyList metas = await raw.FindAsync(new RawQuery(s_agent), default);
Assert.Equal(first.Bytes.ToArray(), await raw.ReadAsync(metas[0], default));
@@ -137,10 +137,10 @@ public sealed class StorageTests : IDisposable
{
var raw = new SqliteRawStore(_db);
var testId = new TestId("tst_old");
- await raw.AppendAsync(Chunk(s_agent, "udp_echo", 1, "2026-01-01"), default);
- await raw.AppendAsync(Chunk(s_agent, "udp_echo", 2, "2026-03-01"), default);
- await raw.AppendAsync(Chunk(s_agent, "udp_train", 3, "2026-01-01", test: testId, phase: 0), default);
- await raw.AppendAsync(Chunk(s_agent, "udp_train", 4, "2026-02-01", test: new TestId("tst_mid"), phase: 0), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_ping", 1, "2026-01-01"), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_ping", 2, "2026-03-01"), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_stream", 3, "2026-01-01", test: testId, phase: 0), default);
+ await raw.AppendAsync(Chunk(s_agent, "quic_stream", 4, "2026-02-01", test: new TestId("tst_mid"), phase: 0), default);
// 持续任务保留到 2026-02-01 之后,测试保留到 2026-01-15 之后
int removed = await raw.DeleteExpiredAsync(new DateOnly(2026, 2, 1), new DateOnly(2026, 1, 15), default);
@@ -365,6 +365,44 @@ public sealed class StorageTests : IDisposable
Assert.Equal(200, (await settings.GetAllAsync(default)).Count);
}
+ [Fact]
+ public void Schema_UpgradesVersion1_ByRenamingTheKinds()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), "moshiro-schema-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ string path = Path.Combine(dir, "moshiro.db");
+ try
+ {
+ using (var db = new SqliteDatabase(dir))
+ {
+ }
+
+ SqliteConnection.ClearAllPools();
+ using (var connection = new SqliteConnection($"Data Source={path}"))
+ {
+ connection.Open();
+ connection.Exec("INSERT INTO tasks (id, scope, kind, schedule, updated_at) VALUES ('tsk_a', 'line', 'udp_echo', 'continuous', 1), ('tsk_b', 'line', 'connect', 'continuous', 1)");
+ connection.Exec("PRAGMA user_version = 1");
+ }
+
+ SqliteConnection.ClearAllPools();
+ using (var db = new SqliteDatabase(dir))
+ {
+ }
+
+ SqliteConnection.ClearAllPools();
+ using var check = new SqliteConnection($"Data Source={path}");
+ check.Open();
+ Assert.Equal(2, check.Scalar("PRAGMA user_version"));
+ Assert.Equal("quic_ping,tcp_connect", string.Join(',', new[] { "tsk_a", "tsk_b" }.Select(id => check.Scalar($"SELECT kind FROM tasks WHERE id = '{id}'"))));
+ }
+ finally
+ {
+ SqliteConnection.ClearAllPools();
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+
[Fact]
public void Schema_RefusesADatabaseFromANewerBuild()
{
diff --git a/backend/tests/MoShiro.TestSupport/AgentSimulator.cs b/backend/tests/MoShiro.TestSupport/AgentSimulator.cs
index 3fbc8c8..5d4d831 100644
--- a/backend/tests/MoShiro.TestSupport/AgentSimulator.cs
+++ b/backend/tests/MoShiro.TestSupport/AgentSimulator.cs
@@ -109,7 +109,7 @@ public sealed class AgentSimulator : IAsyncDisposable
["tcp_port"] = listens ? 7443 : 0,
["quic_ports"] = listens ? new JsonArray(7443) : new JsonArray(),
["public_host"] = listens ? publicHost : "",
- ["kinds"] = new JsonArray((kinds ?? ["udp_echo", "tcp_echo", "udp_train", "connect", "hold", "tcp_bulk", "icmp_echo"]).Select(k => (JsonNode?)JsonValue.Create(k)).ToArray()),
+ ["kinds"] = new JsonArray((kinds ?? ["quic_ping", "tcp_ping", "quic_stream", "tcp_connect", "tls_hold", "tcp_transfer", "icmp_ping"]).Select(k => (JsonNode?)JsonValue.Create(k)).ToArray()),
},
};
@@ -169,7 +169,7 @@ public sealed class AgentSimulator : IAsyncDisposable
var hello = new HelloParams
{
NodeVersion = "0.0.1-sim",
- Capabilities = [MoShiroCapabilities.Base, MoShiroCapabilities.KindsPrefix + string.Join(',', kinds ?? ["udp_echo", "tcp_echo", "udp_train", "connect", "hold", "tcp_bulk", "icmp_echo"])],
+ Capabilities = [MoShiroCapabilities.Base, MoShiroCapabilities.KindsPrefix + string.Join(',', kinds ?? ["quic_ping", "tcp_ping", "quic_stream", "tcp_connect", "tls_hold", "tcp_transfer", "icmp_ping"])],
NowUtcMs = TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds(),
Meta = meta,
};
diff --git a/backend/tests/MoShiro.TestSupport/ChunkFactory.cs b/backend/tests/MoShiro.TestSupport/ChunkFactory.cs
index 9011337..aa0bb2c 100644
--- a/backend/tests/MoShiro.TestSupport/ChunkFactory.cs
+++ b/backend/tests/MoShiro.TestSupport/ChunkFactory.cs
@@ -71,13 +71,13 @@ public static class ChunkFactory
return new RawChunk(Header(agent, kind, startUtcMs, durationMs, parameters), streams);
}
- /// 10 Hz 的 udp_echo,没有丢包,RTT = 2 × owdMs。
+ /// 10 Hz 的 quic_ping,没有丢包,RTT = 2 × owdMs。
public static RawChunk UdpEchoClean(NodeId agent, long startUtcMs, int count = 100, double owdMs = 20) =>
- Echo(agent, "udp_echo", startUtcMs, count, 100, _ => new EchoPacket(false, false, (long)(owdMs * 1000), (long)(owdMs * 1000)));
+ Echo(agent, "quic_ping", startUtcMs, count, 100, _ => new EchoPacket(false, false, (long)(owdMs * 1000), (long)(owdMs * 1000)));
public static RawStream TcpInfo(IEnumerable<(long TsUs, byte Side, uint SrttUs, uint RetransTotal, uint SegsOut)> samples)
{
- var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!);
+ var builder = new StreamBuilder(Kinds.TcpPing.FindStream("tcp_info")!);
foreach ((long ts, byte side, uint srtt, uint retrans, uint segs) in samples)
{
builder.Add(ts, side, srtt, srtt / 4, 10, retrans, segs, segs * 100L, null);
@@ -88,7 +88,7 @@ public static class ChunkFactory
public static RawChunk Connect(NodeId agent, long startUtcMs, IEnumerable<(long TcpUs, long? TlsUs, ConnectOutcomeCode Outcome)> attempts)
{
- var builder = new StreamBuilder(Kinds.Connect.FindStream("conn")!);
+ var builder = new StreamBuilder(Kinds.TcpConnect.FindStream("conn")!);
int i = 0;
foreach ((long tcp, long? tls, ConnectOutcomeCode outcome) in attempts)
{
@@ -96,16 +96,16 @@ public static class ChunkFactory
i++;
}
- return new RawChunk(Header(agent, "connect", startUtcMs), [builder.Build()]);
+ return new RawChunk(Header(agent, "tcp_connect", startUtcMs), [builder.Build()]);
}
- /// udp_train:Agent 以固定间隔发包,Dialer 记录收到的时刻; 为 N 时每 N 个包丢一个。
+ /// quic_stream:Agent 以固定间隔发包,Dialer 记录收到的时刻; 为 N 时每 N 个包丢一个。
public static RawChunk Train(NodeId agent, long startUtcMs, double mbps, int phase, int seconds = 10, int lossEvery = 0, long owdUs = 20_000)
{
const int size = 1250;
int packets = (int)(mbps * 1_000_000 / 8 / size * seconds);
double intervalUs = seconds * 1_000_000.0 / packets;
- var pkt = new StreamBuilder(Kinds.UdpTrain.FindStream("pkt")!);
+ var pkt = new StreamBuilder(Kinds.QuicStream.FindStream("pkt")!);
for (int i = 0; i < packets; i++)
{
long sendAgent = AgentMonoAnchorUs + (long)(i * intervalUs);
@@ -119,10 +119,10 @@ public static class ChunkFactory
["mbps"] = JsonSerializer.SerializeToElement(mbps, FactoryJson.Default.Double),
["duration_s"] = JsonSerializer.SerializeToElement((double)seconds, FactoryJson.Default.Double),
};
- RawChunkHeader header = Header(agent, "udp_train", startUtcMs, seconds * 1000, parameters);
+ RawChunkHeader header = Header(agent, "quic_stream", startUtcMs, seconds * 1000, parameters);
header.Phase = phase;
header.TestId = new TestId("tst_test");
- return new RawChunk(header, [pkt.Build(), new StreamBuilder(Kinds.UdpTrain.FindStream("step")!).Build()]);
+ return new RawChunk(header, [pkt.Build(), new StreamBuilder(Kinds.QuicStream.FindStream("step")!).Build()]);
}
}
diff --git a/backend/tests/MoShiro.TestSupport/DialerSimulator.cs b/backend/tests/MoShiro.TestSupport/DialerSimulator.cs
index 64ae5f0..84404b7 100644
--- a/backend/tests/MoShiro.TestSupport/DialerSimulator.cs
+++ b/backend/tests/MoShiro.TestSupport/DialerSimulator.cs
@@ -138,7 +138,7 @@ public sealed class DialerSimulator : IAsyncDisposable
}
}
- /// 对一个 udp_echo 线路任务发 个回显包,等回复,然后交出一个列块。返回交给 Agent 的列块的序号范围。
+ /// 对一个 quic_ping 线路任务发 个回显包,等回复,然后交出一个列块。返回交给 Agent 的列块的序号范围。
public async Task<(int Sent, int Received)> RunUdpEchoAsync(LineTask task, int count, TimeSpan interval, CancellationToken ct = default)
{
int size = (int)task.Params["size"].GetInt64();
@@ -166,12 +166,12 @@ public sealed class DialerSimulator : IAsyncDisposable
msg.Add(seq, size, 1, t1, collector.Received.TryGetValue(seq, out long t4) ? t4 : null);
}
- RawChunkHeader header = Header("udp_echo", task.Id, null, null, startUtc, (int)Math.Max(1, count * interval.TotalMilliseconds), task.Params);
+ RawChunkHeader header = Header("quic_ping", task.Id, null, null, startUtc, (int)Math.Max(1, count * interval.TotalMilliseconds), task.Params);
await ShipAsync(new RawChunk(header, [msg.Build()]), ct: ct);
return (count, collector.Received.Count);
}
- /// 对一个 tcp_echo 线路任务建立一条 TCP(可选 TLS)连接,发 条回显消息,然后交出一个列块。
+ /// 对一个 tcp_ping 线路任务建立一条 TCP(可选 TLS)连接,发 条回显消息,然后交出一个列块。
public async Task<(int Sent, int Received)> RunTcpEchoAsync(LineTask task, int tcpPort, int count, TimeSpan interval, bool tls, CancellationToken ct = default)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
@@ -211,7 +211,7 @@ public sealed class DialerSimulator : IAsyncDisposable
await Task.Delay(interval, ct);
}
- RawChunkHeader header = Header("tcp_echo", task.Id, null, null, startUtc, (int)Math.Max(1, count * interval.TotalMilliseconds), task.Params);
+ RawChunkHeader header = Header("tcp_ping", task.Id, null, null, startUtc, (int)Math.Max(1, count * interval.TotalMilliseconds), task.Params);
await ShipAsync(new RawChunk(header, [msg.Build()]), ct: ct);
return (count, received);
}
@@ -394,7 +394,7 @@ public sealed class DialerSimulator : IAsyncDisposable
}
_activePhases[start.StreamId] = start;
- if (start.Kind == "udp_train")
+ if (start.Kind == "quic_stream")
{
_trains[start.StreamId] = new TrainCollector { StartUtcMs = TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds() };
_quic!.Register(start.StreamId, OnDatagram);
@@ -424,7 +424,7 @@ public sealed class DialerSimulator : IAsyncDisposable
return 0;
}
- if (start.Kind == "udp_train" && _trains.TryRemove(end.StreamId, out TrainCollector? train))
+ if (start.Kind == "quic_stream" && _trains.TryRemove(end.StreamId, out TrainCollector? train))
{
await Task.Delay(200, ct); // 最后一批包还在路上
// 单块的记录数有上限(ProtocolLimits.MaxRecordsPerChunk):按序号范围切成多个列块,Agent 一侧按同样的范围取行
@@ -443,12 +443,12 @@ public sealed class DialerSimulator : IAsyncDisposable
var streams = new List { pkt.Build() };
if (fragments == 0)
{
- var step = new StreamBuilder(Kinds.UdpTrain.FindStream("step")!);
+ var step = new StreamBuilder(Kinds.QuicStream.FindStream("step")!);
step.Add(0, (long)start.Params["mbps"].GetDouble(), NowUs() - (start.DurationSeconds * 1_000_000L), NowUs());
streams.Add(step.Build());
}
- RawChunkHeader header = Header("udp_train", null, start.TestId, start.Phase, train.StartUtcMs, start.DurationSeconds * 1000, start.Params);
+ RawChunkHeader header = Header("quic_stream", null, start.TestId, start.Phase, train.StartUtcMs, start.DurationSeconds * 1000, start.Params);
header.Params[FragmentKeys.SeqFrom] = JsonSerializer.SerializeToElement(from);
header.Params[FragmentKeys.SeqTo] = JsonSerializer.SerializeToElement(to);
// 不能在这里等确认:确认是 Agent 的通知,由同一个入站队列顺序处理,而当前正处在 phase_end 的处理函数里