ci/woodpecker/push/main-amd64-build Pipeline was successful
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
88 lines
2.9 KiB
C#
88 lines
2.9 KiB
C#
using System.Text.Json;
|
||
using Fleet.Core;
|
||
using Fleet.Link;
|
||
using MoShiro.Measurement;
|
||
using MoShiro.Protocol;
|
||
|
||
namespace MoShiro.Probe;
|
||
|
||
/// <summary>
|
||
/// 估计 Agent 单调时钟 − Probe 单调时钟:每次 <c>moshiro.clock</c> 往返取中点,在最近 8 次里取往返最短的一次
|
||
/// (往返越短,对称性假设的误差越小)。Probe 的列块头里带着这个偏移,Backend 据此对齐两端的时间戳。
|
||
/// </summary>
|
||
public sealed class ClockEstimator
|
||
{
|
||
private const int Window = 8;
|
||
|
||
private readonly object _gate = new();
|
||
private readonly Queue<(long RoundTripUs, long OffsetUs)> _samples = new();
|
||
|
||
public long OffsetUs { get; private set; }
|
||
|
||
public bool HasEstimate { get; private set; }
|
||
|
||
public void Add(long sendUs, long agentUs, long receiveUs)
|
||
{
|
||
lock (_gate)
|
||
{
|
||
_samples.Enqueue((receiveUs - sendUs, agentUs - ((sendUs + receiveUs) / 2)));
|
||
while (_samples.Count > Window)
|
||
{
|
||
_samples.Dequeue();
|
||
}
|
||
|
||
OffsetUs = _samples.MinBy(s => s.RoundTripUs).OffsetUs;
|
||
HasEstimate = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把 Probe 一侧的行交给这个 Agent:补全块头 → 编码 → 放进会话的发件箱(通道 17,至少一次,有界)。
|
||
/// 发件箱的内容在连接断开重连之后重发;发件箱满了丢弃最旧的。
|
||
/// </summary>
|
||
public sealed class FragmentShipper : IFragmentSink
|
||
{
|
||
private readonly Outbox _outbox;
|
||
private readonly NodeId _agent;
|
||
private readonly ProbeId _probe;
|
||
private readonly ClockEstimator _clock;
|
||
private readonly TimeProvider _time;
|
||
|
||
public FragmentShipper(Outbox outbox, NodeId agent, ProbeId probe, ClockEstimator clock, TimeProvider time)
|
||
{
|
||
_outbox = outbox;
|
||
_agent = agent;
|
||
_probe = probe;
|
||
_clock = clock;
|
||
_time = time;
|
||
}
|
||
|
||
public void Ship(string kind, FragmentScope scope, IReadOnlyList<RawStream> streams, IReadOnlyDictionary<string, JsonElement>? parameters, long startUtcMs, int durationMs, int port)
|
||
{
|
||
var header = new RawChunkHeader
|
||
{
|
||
ChunkId = ChunkId.New(_time),
|
||
Kind = kind,
|
||
TaskId = scope.Task,
|
||
TestId = scope.Test,
|
||
Phase = scope.Phase,
|
||
AgentId = _agent,
|
||
ProbeId = _probe,
|
||
Port = port,
|
||
StartUtcMs = startUtcMs,
|
||
DurationMs = Math.Max(1, durationMs),
|
||
Clock = new ChunkClock { ProbeOffsetUs = _clock.OffsetUs },
|
||
};
|
||
if (parameters is not null)
|
||
{
|
||
foreach ((string key, JsonElement value) in parameters)
|
||
{
|
||
header.Params[key] = value;
|
||
}
|
||
}
|
||
|
||
_outbox.Enqueue(MoShiroChannels.ColumnBlock, header.ChunkId.Value, RawChunkCodec.Encode(new RawChunk(header, streams)));
|
||
}
|
||
}
|