Namespaces, assemblies, projects and directories, the solution, binaries, images, systemd units, config paths, environment variables, protocol method names, the media type and the raw chunk magic all move to MoShiro/moshiro. Existing data and deployments are not compatible and must be recreated. Also fix the architecture tests after the solution layout change and make the update tests independent of their order.
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 == 2));
|
|
|
|
// 已经落盘的块在重启后仍然可以按 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);
|
|
}
|
|
}
|