Files
MoShiro/backend/agent/src/MoShiro.Agent/Dial/TaskLink.cs
T
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
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.
2026-10-21 11:55:36 +08:00

163 lines
6.1 KiB
C#

using Fleet.Core;
using MoShiro.Measurement;
using MoShiro.Protocol;
namespace MoShiro.Agent;
/// <summary>
/// 任务层的 QUIC 数据通道:控制层在线期间在后台维持一条 QUIC 连接,断开后按 <see cref="QuicRedialBackoff"/> 重拨。
/// 重拨期间控制连接和 TCP 类积木不受影响;UDP 类积木的发送在通道不可用时按各自的规则处理。
/// </summary>
public sealed class TaskLink : IDisposable
{
/// <summary>QUIC 连接可用至少这么久才算稳定,重拨节奏随之复位。刚连上就断的通道不会把间隔重置回 1 秒。</summary>
private static readonly TimeSpan s_stableAfter = TimeSpan.FromSeconds(10);
/// <summary>一直在发测量报文、却这么久没有收到任何一个:QUIC 路径已经是黑洞,主动关闭重拨。</summary>
private static readonly TimeSpan s_silentLimit = TimeSpan.FromSeconds(30);
private static readonly TimeSpan s_watchInterval = TimeSpan.FromSeconds(5);
private static readonly TimeSpan s_summaryInterval = TimeSpan.FromSeconds(60);
private readonly QuicDatagramTransport _datagrams;
private readonly LinkStatus _status;
private readonly TimeProvider _time;
private readonly SemaphoreSlim _retrySignal = new(0, 1);
public TaskLink(QuicDatagramTransport datagrams, LinkStatus status, TimeProvider time)
{
_datagrams = datagrams;
_status = status;
_time = time;
}
public void Dispose() => _retrySignal.Dispose();
/// <summary>管理端手动重试:跳过当前的重拨等待。</summary>
public void RetryNow()
{
try
{
_retrySignal.Release();
}
catch (SemaphoreFullException)
{
// 已有一次排队
}
}
public async Task RunAsync(CancellationToken ct)
{
var backoff = new QuicRedialBackoff();
while (!ct.IsCancellationRequested)
{
_status.SetQuic(QuicState.Connecting, attempt: backoff.Attempts);
LinkFault fault;
long readyAt = 0;
try
{
try
{
await _datagrams.ConnectAsync(ct);
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
_status.Handshake(false, null, ex is TimeoutException or OperationCanceledException);
throw;
}
_status.Handshake(true, _datagrams.HandshakeDurationMs, false);
readyAt = _time.GetTimestamp();
_status.SetQuic(QuicState.Ready);
fault = await WatchAsync(ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return;
}
catch (Exception ex)
{
fault = LinkFaults.From(ex, "quic_handshake");
}
await _datagrams.CloseClientAsync();
if (readyAt != 0 && _time.GetElapsedTime(readyAt) >= s_stableAfter)
{
backoff.Reset();
}
TimeSpan delay = backoff.Next(Random.Shared);
_status.SetQuic(QuicState.Retrying, fault, backoff.Attempts, delay);
try
{
using var wait = CancellationTokenSource.CreateLinkedTokenSource(ct);
Task sleep = Task.Delay(delay, _time, wait.Token);
Task manual = _retrySignal.WaitAsync(wait.Token);
await Task.WhenAny(sleep, manual);
await wait.CancelAsync();
await Task.WhenAll(Settle(sleep), Settle(manual));
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return;
}
}
}
/// <summary>连接可用期间的看护:连接关闭(对端关闭、空闲超时)或数据面静默时返回原因。</summary>
private async Task<LinkFault> WatchAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(s_watchInterval, _time);
Task closed = _datagrams.WaitForDisconnectAsync(ct);
long seenIn = _datagrams.Counters.PacketsIn;
long seenOut = _datagrams.Counters.PacketsOut;
long inChangedAt = _time.GetTimestamp();
long lastSummaryAt = inChangedAt;
while (true)
{
Task tick = timer.WaitForNextTickAsync(ct).AsTask();
if (await Task.WhenAny(closed, tick) == closed)
{
await closed;
return LinkFaults.Closed("quic_data", "QUIC connection closed");
}
await tick;
long packetsIn = _datagrams.Counters.PacketsIn;
long packetsOut = _datagrams.Counters.PacketsOut;
long now = _time.GetTimestamp();
if (packetsIn != seenIn)
{
seenIn = packetsIn;
inChangedAt = now;
}
else if (packetsOut != seenOut && _time.GetElapsedTime(inChangedAt, now) > s_silentLimit)
{
return new LinkFault { Phase = "quic_data", Kind = "silent", Message = $"no datagram received for {(long)_time.GetElapsedTime(inChangedAt, now).TotalSeconds}s while sending" };
}
seenOut = packetsOut;
if (_time.GetElapsedTime(lastSummaryAt, now) >= s_summaryInterval)
{
lastSummaryAt = now;
QuicTransportSnapshot? transport = _datagrams.GetTransportSnapshot();
_status.Log.Info("agent measurement summary", ("layer", "task_quic"), ("packets_in", packetsIn), ("packets_out", packetsOut),
("dropped_invalid", _datagrams.Counters.DroppedInvalid), ("dropped_rate_limited", _datagrams.Counters.DroppedRateLimited),
("transport_packets_sent", transport?.PacketsSent), ("path_mtu", transport?.PathMtu));
}
}
}
private static async Task Settle(Task task)
{
try
{
await task;
}
catch (OperationCanceledException)
{
// 等待被另一条路径或停止打断
}
}
}