fix(link): recover stale websocket sessions [CI]
This commit is contained in:
1 parent
b5e275c51f
commit
be97e24693
2 files changed
+41
-6
No files matched your search
@@ -140,7 +140,7 @@ public sealed class FleetLink
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is WebSocketException or IOException or HttpRequestException or InvalidOperationException or TimeoutException)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
reason = ex.Message;
|
reason = ex.Message;
|
||||||
}
|
}
|
||||||
@@ -214,6 +214,7 @@ public sealed class FleetLink
|
|||||||
|
|
||||||
Task cancellation = Task.Delay(Timeout.Infinite, ct);
|
Task cancellation = Task.Delay(Timeout.Infinite, ct);
|
||||||
Task finished = await Task.WhenAny(run, pump, heartbeat, cancellation);
|
Task finished = await Task.WhenAny(run, pump, heartbeat, cancellation);
|
||||||
|
string finishedReason = DescribeFinishedTask(finished, run, pump, heartbeat);
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -231,7 +232,7 @@ public sealed class FleetLink
|
|||||||
|
|
||||||
await sessionCts.CancelAsync();
|
await sessionCts.CancelAsync();
|
||||||
await Task.WhenAll(Quiet(run), Quiet(pump), Quiet(heartbeat));
|
await Task.WhenAll(Quiet(run), Quiet(pump), Quiet(heartbeat));
|
||||||
return finished == run ? "closed by server" : "link task failed";
|
return finishedReason;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -398,9 +399,24 @@ public sealed class FleetLink
|
|||||||
{
|
{
|
||||||
await task;
|
await task;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is OperationCanceledException or WebSocketException or IOException or ObjectDisposedException)
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Session cancellation is expected during reconnect and shutdown.
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is WebSocketException or IOException or ObjectDisposedException or TimeoutException or InvalidOperationException)
|
||||||
{
|
{
|
||||||
// 会话结束时的正常异常
|
// 会话结束时的正常异常
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string DescribeFinishedTask(Task finished, Task run, Task pump, Task heartbeat)
|
||||||
|
{
|
||||||
|
if (finished.IsFaulted)
|
||||||
|
{
|
||||||
|
string taskName = finished == run ? "receive" : finished == pump ? "outbox send" : "heartbeat";
|
||||||
|
return $"{taskName} failed: {finished.Exception?.GetBaseException().Message ?? "unknown error"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return finished == run ? "closed by server" : "link task stopped";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ namespace Fleet.Link;
|
|||||||
/// <summary>WebSocket 传输(节点 → 服务端):文本帧是 JSON-RPC,二进制帧是发件箱。</summary>
|
/// <summary>WebSocket 传输(节点 → 服务端):文本帧是 JSON-RPC,二进制帧是发件箱。</summary>
|
||||||
public sealed class WsTransport : IRpcTransport
|
public sealed class WsTransport : IRpcTransport
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan s_sendTimeout = TimeSpan.FromSeconds(20);
|
||||||
private readonly WebSocket _socket;
|
private readonly WebSocket _socket;
|
||||||
private readonly int _maxFrameBytes;
|
private readonly int _maxFrameBytes;
|
||||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||||
@@ -19,14 +20,32 @@ public sealed class WsTransport : IRpcTransport
|
|||||||
|
|
||||||
public async ValueTask SendAsync(RpcFrame frame, CancellationToken ct)
|
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
|
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
|
finally
|
||||||
{
|
{
|
||||||
_sendLock.Release();
|
if (lockTaken)
|
||||||
|
{
|
||||||
|
_sendLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user