fix(transport): guard liveness watchdog against long-running commands + tests#154
Merged
dsarno merged 1 commit intoJun 15, 2026
Conversation
The receive loop awaits HandleExecuteAsync inline, so while a command runs it stops reading inbound frames and _lastInboundUtcTicks goes stale. A command longer than the liveness timeout (~37.5s) — test runs, asset imports, batch ops — would make the watchdog falsely abort the socket and kill the in-flight command. Track an in-flight command counter and skip the watchdog while a command is executing; refresh liveness on completion (before decrementing) so the gap until the next ReceiveAsync drains buffered server pings can't race into a false trip. A genuinely dead socket still surfaces via the result-send failure or the next faulted ReceiveAsync. Extract the timeout derivation and trip decision into pure static helpers and cover them with unit tests.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
dsarno
added a commit
that referenced
this pull request
Jun 15, 2026
…#154) The receive loop awaits HandleExecuteAsync inline, so while a command runs it stops reading inbound frames and _lastInboundUtcTicks goes stale. A command longer than the liveness timeout (~37.5s) — test runs, asset imports, batch ops — would make the watchdog falsely abort the socket and kill the in-flight command. Track an in-flight command counter and skip the watchdog while a command is executing; refresh liveness on completion (before decrementing) so the gap until the next ReceiveAsync drains buffered server pings can't race into a false trip. A genuinely dead socket still surfaces via the result-send failure or the next faulted ReceiveAsync. Extract the timeout derivation and trip decision into pure static helpers and cover them with unit tests. Co-authored-by: Claude <noreply@anthropic.com>
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.
Follow-up to #151 addressing the one correctness risk from review, plus unit tests.
Problem
ReceiveLoopAsyncawaitsHandleExecuteAsyncinline, so while a command runs the loop stops callingReceiveAsyncand_lastInboundUtcTicksgoes stale (server pings sent during execution sit unread in the buffer). A command that runs longer than_inboundLivenessTimeout(~37.5s) — play-mode test runs, large asset imports, batch/refresh ops with a server-assigned timeout above the liveness window — would make the watchdogAbort()the socket and cancel the in-flight command. Before #151 these survived (the separate keep-alive task kept sending pongs), so this was a regression introduced by the watchdog.Fix
_commandsInFlight), incremented around dispatch inHandleExecuteAsync.finallybefore decrementing, so the gap until the nextReceiveAsyncdrains the buffered server pings can't race into a false trip.ReceiveAsyncfaults → existingHandleSocketClosureAsyncpath.Refactor for testability
Extracted the two pure decisions into private static helpers so they can be unit-tested without sockets:
ComputeInboundLivenessTimeout(TimeSpan)—max(30s, keepAlive × 2.5)ShouldTripLivenessWatchdog(sinceInbound, timeout, commandsInFlight)Tests
Added to
WebSocketTransportClientTests.cs(same reflection pattern as the existing tests in that file):Could not compile-check locally (no Unity/.NET toolchain in this environment); logic and conventions verified by inspection against the existing test file.
https://claude.ai/code/session_01HKue2hxNbVaNNYkpYABqjp
Generated by Claude Code