fix(transport/websocket): refuse reconnect on evicted session instead of silently spawning new mux (fixes #354)#360
Merged
Conversation
… of silently spawning new mux (fixes #354) HandleAsync's ChannelClosedException catch (added by #279 for the eviction race) fell through to the fresh-session creation branch. When a client reconnect with a known sessionId X was blocked on a full ConnectionChannel and the session was evicted before the blocked WriteAsync drained, the catch ran and the pair was bound to a brand-new mux with SessionId Y. The client's reconnect handshake then faulted terminally with SessionMismatch — the exact defect #236 fixed and #279 unintentionally re-surfaced. Mirror the unknown-session branch: send a PolicyViolation close, dispose the pair, return. The client can then decide to start fresh with sessionId=null. Regression test creates a session, fills its bounded ConnectionChannel, starts a second HandleAsync(sessionId=X) that blocks on WriteAsync, evicts the session, and asserts (a) HandleAsync returns cleanly, (b) the client observes a PolicyViolation close, (c) no orphan mux is registered in _sessions. Without the fix the test times out waiting on the spawned mux's completion; with the fix it passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #354.
WebSocketMuxListener.HandleAsync'sChannelClosedExceptioncatch (added by #279 for the eviction race) fell through to the fresh-session creation branch. This silently bound the reconnecting client's pair to a brand-new mux with a brand-newSessionId, causing the client's reconnect handshake to fault terminally withSessionMismatch— the exact defect #236 fixed and #279 unintentionally re-surfaced.Trace
_sessions[X] = entry.HandleAsync(sessionId=X)→WriteAsync(pair1)succeeds (channel cap 1).ARM64CPU architecture support #2 arrives →WriteAsync(pair2)blocks (channel full).Disconnected→evictHandler→TryComplete()on the channel.WriteAsyncthrowsChannelClosedException.SessionId = Y ≠ X, wires pair2 into Y'sStreamFactory. Client's handshake then claims session X against mux Y →MultiplexerException(SessionMismatch)→ client's mux is poisoned for life.Fix
Mirror the unknown-session branch in the
ChannelClosedExceptioncatch:The client's next reconnect attempt will observe the session is truly gone and can fall back to a fresh
sessionId = nullconnection (or surface the eviction to the application).Tests
New:
tests/NetConduit.Transport.WebSocket.IntegrationTests/Issue354ChannelClosedReconnectTests.csReconnectBlockedOnFullChannel_SessionEvicted_RefusesInsteadOfNewMux:ConnectionChannelstays full.HandleAsync(sessionId=X); blocks onWriteAsync.listener.RemoveSession(X)to evict the session, completing the channel writer.HandleAsync#2returns cleanly within 5s, the client observes aWebSocketCloseStatus.PolicyViolationclose frame, and the internal_sessionsdictionary is empty (no orphan mux Y was registered).Verification:
TimeoutExceptionat the 5sWaitAsync—HandleAsync#2is hanging inside the spawned mux Y'scompletion.Task.WaitAsync.Full
NetConduit.Transport.WebSocket.IntegrationTestssuite: 18/18 pass.Full solution build (net8/9/10): clean.
Related
mux.Disconnected/Closed, only manualRemoveSession()or full listenerDisposeAsync()cleans up; long-lived listeners leak unbounded memory, and any peer presenting a dead-session GUID is routed to the disposed mux's staleChannel<CompletionStreamPair>, the pair is never consumed, HandleAsync hangs forever #279 — introduced theChannelClosedExceptioncatch. Right intent (don't crash with the exception) but wrong recovery (fall through instead of refuse).