Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
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.Agent;
|
|
|
|
/// <summary>
|
|
/// 估计监听方单调时钟 − 拨号方单调时钟:每次 <c>moshiro.clock</c> 往返取中点,在最近 8 次里取往返最短的一次
|
|
/// (往返越短,对称性假设的误差越小)。拨号方的列块头里带着这个偏移,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 listenerUs, long receiveUs)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
_samples.Enqueue((receiveUs - sendUs, listenerUs - ((sendUs + receiveUs) / 2)));
|
|
while (_samples.Count > Window)
|
|
{
|
|
_samples.Dequeue();
|
|
}
|
|
|
|
OffsetUs = _samples.MinBy(s => s.RoundTripUs).OffsetUs;
|
|
HasEstimate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 把拨号方一侧的行交给监听方:补全块头 → 编码 → 放进会话的发件箱(通道 17,至少一次,有界)。
|
|
/// 发件箱的内容在连接断开重连之后重发;发件箱满了丢弃最旧的。
|
|
/// </summary>
|
|
public sealed class FragmentShipper : IFragmentSink
|
|
{
|
|
private readonly Outbox _outbox;
|
|
private readonly NodeId _listener;
|
|
private readonly NodeId _dialer;
|
|
private readonly ClockEstimator _clock;
|
|
private readonly TimeProvider _time;
|
|
|
|
public FragmentShipper(Outbox outbox, NodeId listener, NodeId dialer, ClockEstimator clock, TimeProvider time)
|
|
{
|
|
_outbox = outbox;
|
|
_listener = listener;
|
|
_dialer = dialer;
|
|
_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 = _listener,
|
|
DialerId = _dialer,
|
|
Port = port,
|
|
StartUtcMs = startUtcMs,
|
|
DurationMs = Math.Max(1, durationMs),
|
|
Clock = new ChunkClock { DialerOffsetUs = _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)));
|
|
}
|
|
}
|