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) { // 等待被另一条路径或停止打断 } } }