fix(battle-node): dispose participants, unsubscribe events, filter catch
#5: BattleSession.RunAsync now unsubscribes FrameEmitted handlers (-= OnFrameFromA/B) before termination and calls DisposeAsync on both participants + the dispatch gate SemaphoreSlim afterward. This unpins the session state from live delegates and releases the WS. #6: Bare catch {} blocks replaced with filtered exception handlers that silently swallow OperationCanceledException and WebSocketException (expected at battle end) but log anything else at Warning. NREs and other real bugs in handler threads are now visible instead of silently eaten. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Sessions.Dispatch;
|
||||
@@ -125,20 +126,34 @@ public sealed class BattleSession
|
||||
|
||||
cts.Cancel(); // unblock the survivor's RunAsync read loop
|
||||
try { await Task.WhenAll(aTask, bTask).ConfigureAwait(false); }
|
||||
catch { /* swallow cancellation / WS exceptions */ }
|
||||
catch (Exception ex) when (ex is OperationCanceledException or WebSocketException) { }
|
||||
catch (AggregateException ex) when (ex.Flatten().InnerExceptions.All(
|
||||
e => e is OperationCanceledException or WebSocketException)) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning(ex, "BattleSession {Bid}: unexpected exception from WhenAll (PvP drain)", BattleId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bot mode: the NoOp opponent's RunAsync returns immediately; wait for the real
|
||||
// participant. The session keeps running for the real one.
|
||||
try { await Task.WhenAll(aTask, bTask).ConfigureAwait(false); }
|
||||
catch { /* swallow */ }
|
||||
catch (Exception ex) when (ex is OperationCanceledException or WebSocketException) { }
|
||||
catch (AggregateException ex) when (ex.Flatten().InnerExceptions.All(
|
||||
e => e is OperationCanceledException or WebSocketException)) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning(ex, "BattleSession {Bid}: unexpected exception from WhenAll (Bot drain)", BattleId);
|
||||
}
|
||||
}
|
||||
|
||||
// Audit Md11 — release per-participant outbound archives at battle-end
|
||||
// (only RealParticipant has one; bots don't archive). Heavy state is
|
||||
// dropped synchronously here so the participant's TerminateAsync doesn't
|
||||
// need to keep the dict alive through its disposal handshake.
|
||||
// Unsubscribe event handlers so the session + state aren't pinned by live delegates.
|
||||
A.FrameEmitted -= OnFrameFromA;
|
||||
B.FrameEmitted -= OnFrameFromB;
|
||||
|
||||
// Release per-participant outbound archives at battle-end
|
||||
// (only RealParticipant has one; bots don't archive).
|
||||
if (A is RealParticipant rpA) rpA.Outbound.Clear();
|
||||
if (B is RealParticipant rpB) rpB.Outbound.Clear();
|
||||
|
||||
@@ -146,6 +161,10 @@ public sealed class BattleSession
|
||||
A.TerminateAsync(BattleFinishReason.NormalFinish),
|
||||
B.TerminateAsync(BattleFinishReason.NormalFinish))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await A.DisposeAsync().ConfigureAwait(false);
|
||||
await B.DisposeAsync().ConfigureAwait(false);
|
||||
_dispatchGate.Dispose();
|
||||
}
|
||||
|
||||
private Task OnFrameFromA(MsgEnvelope env, CancellationToken ct) => HandleFrameAsync(A, env, ct);
|
||||
|
||||
Reference in New Issue
Block a user