using Fleet.Core;
using MoShiro.Measurement;
using MoShiro.Protocol;
namespace MoShiro.Agent;
///
/// 任务层的 QUIC 数据通道:控制层在线期间在后台维持一条 QUIC 连接,断开后按 重拨。
/// 重拨期间控制连接和 TCP 类积木不受影响;UDP 类积木的发送在通道不可用时按各自的规则处理。
///
public sealed class TaskLink : IDisposable
{
/// QUIC 连接可用至少这么久才算稳定,重拨节奏随之复位。刚连上就断的通道不会把间隔重置回 1 秒。
private static readonly TimeSpan s_stableAfter = TimeSpan.FromSeconds(10);
/// 一直在发测量报文、却这么久没有收到任何一个:QUIC 路径已经是黑洞,主动关闭重拨。
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();
/// 管理端手动重试:跳过当前的重拨等待。
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;
}
}
}
/// 连接可用期间的看护:连接关闭(对端关闭、空闲超时)或数据面静默时返回原因。
private async Task 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)
{
// 等待被另一条路径或停止打断
}
}
}