ci/woodpecker/push/main-amd64-build Pipeline was successful
Agents and Probes exit in order at a configured time (default 04:30 UTC+8, switch and time zone in the admin settings) and the service manager starts them again; a local restart_at or MOSHIRO_RESTART_AT wins. The connect kind (TCP connect and TLS handshake timings) is now a default continuous line template, with a section on the line page.
69 lines
4.0 KiB
C#
69 lines
4.0 KiB
C#
using System.Text.Json.Nodes;
|
|
using MoShiro.Protocol;
|
|
using MoShiro.TestSupport;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
/// <summary>重启后端:数据、身份和令牌都必须还在;热层从原始块回填;段文件尾部没有索引的数据被截断。</summary>
|
|
public sealed class PersistenceTests : IAsyncLifetime
|
|
{
|
|
private readonly BackendFixture _backend = new();
|
|
public ValueTask InitializeAsync() => _backend.InitializeAsync();
|
|
|
|
public ValueTask DisposeAsync() => _backend.DisposeAsync();
|
|
|
|
[Fact]
|
|
public async Task EverythingSurvivesARestart_AndTheHotLayerIsRebuiltFromRawChunks()
|
|
{
|
|
long minute = 60_000;
|
|
long baseMs = ((TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds() / minute) - 3) * minute;
|
|
AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
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))));
|
|
}
|
|
|
|
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<long>() == 1200);
|
|
before = await _backend.GetJsonAsync(_backend.Admin, url + "&step=1m");
|
|
|
|
await agent.DisposeAsync();
|
|
await _backend.RestartAsync();
|
|
HttpClient admin = _backend.Admin; // 重启之后用同一个令牌,证明令牌已经落盘
|
|
|
|
// 身份与令牌:Agent 记录在,管理员令牌仍然有效
|
|
JsonNode agents = await _backend.GetJsonAsync(admin, "/api/admin/agents");
|
|
JsonNode restored = agents["items"]!.AsArray().Single(a => a!["id"]!.GetValue<string>() == id)!;
|
|
Assert.Equal("active", restored["status"]!.GetValue<string>());
|
|
Assert.False(restored["online"]!.GetValue<bool>());
|
|
|
|
// 持久层:重启前算好的分钟窗口原样还在
|
|
JsonNode after = await _backend.GetJsonAsync(admin, url + "&step=1m");
|
|
Assert.Equal(before["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), after["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 9);
|
|
Assert.Equal(25.0, after["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
|
|
|
|
// 热层:重启后为空,回填之后才会被查询选用;回填完成后 1s 步长能读到同样的数据
|
|
JsonNode hot = null!;
|
|
await BackendFixture.EventuallyAsync(async () =>
|
|
{
|
|
hot = await _backend.GetJsonAsync(admin, url + "&step=1s");
|
|
return hot["source"]!.GetValue<string>() == "hot" && hot["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 1200;
|
|
});
|
|
Assert.Equal(25.0, hot["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
|
|
|
|
// 同一个身份可以重新连接,并拿回当前版本的期望状态
|
|
await using AgentSimulator again = await _backend.ReconnectAsync(agent);
|
|
Assert.NotNull(await again.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount));
|
|
|
|
// 已经落盘的块在重启后仍然可以按 ID 下载
|
|
JsonNode chunks = await _backend.GetJsonAsync(admin, $"/api/admin/raw/chunks?agent_id={id}&limit=1");
|
|
string chunkId = chunks["items"]![0]!["chunk_id"]!.GetValue<string>();
|
|
using HttpResponseMessage download = await admin.GetAsync("/api/admin/raw/chunks/" + chunkId);
|
|
Assert.True(RawChunkCodec.Decode(await download.Content.ReadAsByteArrayAsync()).IsSuccess);
|
|
}
|
|
}
|