fix(link): recover stale websocket sessions [CI]
ci/woodpecker/push/dev-image Pipeline is pending
ci/woodpecker/push/web-build Pipeline was canceled
ci/woodpecker/push/verify Pipeline was canceled

This commit is contained in:
justme committed 2026-10-11 19:54:14 +08:00
1 parent b5e275c51f
commit be97e24693
2 files changed
+41 -6

No files matched your search

+22 -3
View File
@@ -7,6 +7,7 @@ namespace Fleet.Link;
/// <summary>WebSocket 传输(节点 → 服务端):文本帧是 JSON-RPC,二进制帧是发件箱。</summary>
public sealed class WsTransport : IRpcTransport
{
private static readonly TimeSpan s_sendTimeout = TimeSpan.FromSeconds(20);
private readonly WebSocket _socket;
private readonly int _maxFrameBytes;
private readonly SemaphoreSlim _sendLock = new(1, 1);
@@ -19,14 +20,32 @@ public sealed class WsTransport : IRpcTransport
public async ValueTask SendAsync(RpcFrame frame, CancellationToken ct)
{
await _sendLock.WaitAsync(ct);
using var timeout = new CancellationTokenSource(s_sendTimeout);
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token);
bool lockTaken = false;
try
{
await _socket.SendAsync(frame.Data, frame.Kind == FrameKind.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, endOfMessage: true, ct);
await _sendLock.WaitAsync(linked.Token);
lockTaken = true;
if (_socket.State != WebSocketState.Open)
{
throw new WebSocketException($"WebSocket is not open (state: {_socket.State})");
}
await _socket.SendAsync(frame.Data, frame.Kind == FrameKind.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, endOfMessage: true, linked.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested && !ct.IsCancellationRequested)
{
// A wedged send otherwise holds the lock and prevents heartbeat/reconnect from making progress.
_socket.Abort();
throw new TimeoutException($"WebSocket send timed out after {s_sendTimeout.TotalSeconds:0} seconds");
}
finally
{
_sendLock.Release();
if (lockTaken)
{
_sendLock.Release();
}
}
}