backend: serve single-agent task templates with their status and results, rename the kinds in storage

This commit is contained in:
justme committed 2026-10-22 07:02:28 +08:00
1 parent e77880d372
commit 3a9ea46d6f
45 files changed
+763 -216

No files matched your search

@@ -29,7 +29,7 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
{
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<BackendFixture>
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<BackendFixture>
// 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<string>(), System.Globalization.CultureInfo.InvariantCulture).ToUnixTimeMilliseconds();
Assert.InRange(to, baseMs + (50 * 1000), baseMs + (80 * 1000));
Assert.Equal(600, anchored["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
Assert.Equal(600, anchored["summary"]!["samples"]!["quic_ping"]!.GetValue<long>());
Assert.False(anchored["live"]!.GetValue<bool>());
// 这个窗口是以“最后一个样本”为终点的绝对区间,但线路恢复之后同一个地址的内容就变了:不能被浏览器永久缓存
@@ -102,7 +102,7 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
// 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<BackendFixture>
{
minute = await SeriesAsync(agent.NodeId.Value, range + "&step=1m");
// 窗口会随着后续的块到达而重算:等到 18 个块的样本都计入之后再比较
return minute["source"]!.GetValue<string>() == "1m" && minute["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 1800
&& minute["summary"]!["samples"]!["tcp_echo"]?.GetValue<long>() == 3600;
return minute["source"]!.GetValue<string>() == "1m" && minute["summary"]!["samples"]!["quic_ping"]?.GetValue<long>() == 1800
&& minute["summary"]!["samples"]!["tcp_ping"]?.GetValue<long>() == 3600;
});
JsonNode hotSummary = hot["summary"]!["metrics"]!;
@@ -176,9 +176,9 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
$"{id}: hot={hotSummary[id]} persisted={minuteSummary[id]}");
}
Assert.Equal(1800, hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
Assert.Equal(hot["summary"]!["samples"]!["udp_echo"]!.GetValue<long>(), minute["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
Assert.Equal(3600, minute["summary"]!["samples"]!["tcp_echo"]!.GetValue<long>());
Assert.Equal(1800, hot["summary"]!["samples"]!["quic_ping"]!.GetValue<long>());
Assert.Equal(hot["summary"]!["samples"]!["quic_ping"]!.GetValue<long>(), minute["summary"]!["samples"]!["quic_ping"]!.GetValue<long>());
Assert.Equal(3600, minute["summary"]!["samples"]!["tcp_ping"]!.GetValue<long>());
// RTT = 2 × 20 ms,直方图的相对误差 ≤ 4.5%
Assert.InRange(minuteSummary["rtt.p95"]!.GetValue<double>(), 40 * 0.955, 40 * 1.045);
@@ -234,7 +234,7 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
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<double>(), 6);
Assert.Equal(1200, lines[goodIndex]["summary"]!["samples"]!["udp_echo"]!.GetValue<long>());
Assert.Equal(1200, lines[goodIndex]["summary"]!["samples"]!["quic_ping"]!.GetValue<long>());
// 按指标排序: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<BackendFixture>
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<long>() == 600);
await BackendFixture.EventuallyAsync(async () => (await _backend.GetJsonAsync(_backend.Admin, url))["summary"]!["samples"]!["quic_ping"]?.GetValue<long>() == 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<long>() == 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<long>() == 700, because: "the late chunk must re-enter the persistent path");
}
}
@@ -394,7 +394,7 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
await BackendFixture.EventuallyAsync(async () =>
{
oneMinute = await SeriesAsync(agent.NodeId.Value, range + "&step=1m");
return oneMinute["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 3000;
return oneMinute["summary"]!["samples"]!["quic_ping"]?.GetValue<long>() == 3000;
});
// 5 分钟层由降采样把 5 个 1 分钟窗口精确合并而来
@@ -402,7 +402,7 @@ public sealed class IngestAndQueryTests : IClassFixture<BackendFixture>
await BackendFixture.EventuallyAsync(async () =>
{
fiveMinutes = await SeriesAsync(agent.NodeId.Value, range + "&step=5m");
return fiveMinutes["source"]!.GetValue<string>() == "5m" && fiveMinutes["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 3000;
return fiveMinutes["source"]!.GetValue<string>() == "5m" && fiveMinutes["summary"]!["samples"]!["quic_ping"]?.GetValue<long>() == 3000;
});
Assert.Equal(oneMinute["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), fiveMinutes["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 9);
Assert.Equal(oneMinute["summary"]!["metrics"]!["rtt.p99"]!.GetValue<double>(), fiveMinutes["summary"]!["metrics"]!["rtt.p99"]!.GetValue<double>(), 9);