Repository navigation
Keep the accept loop alive through shutdown, a broken client, and a wedged handle - #1
Merged
Conversation
…edged handle Three defects in AcceptLoop, all found by driving the gate hard on Linux (4 cores, 8 gates at a time, under load) while porting it into a PDF viewer. None of them is Linux-specific; that is only where the timing exposes them. 1. Shutdown killed the process. `catch (Exception ex) when (!_stopping)` is a FILTER, and a filter that declines does not swallow: once Dispose has set the flag, the exception from the in-flight connection leaves AcceptLoop and goes unhandled on that thread, taking the whole process with it. Disposing while a hand-off is in flight is common enough under load that it killed a 400-round harness outright rather than failing a round. The flag is now read inside the catch, so shutdown is a return and everything else is logged. 2. A client that gave up before writing left the holder deaf. Releasing the connection was gated on IsConnected, which tracks the LOCAL handle and is already false by then, so the instance was never disconnected and every later hand-off met a listener that could only throw. Disconnect is unconditional now; it throws when there was nothing to release, and that is what the catch is for. 3. A wedged handle never recovered. A client that connects and vanishes before the accept returns can leave the instance throwing ERROR_PIPE_CLOSING from WaitForConnection for ever, and Disconnect does not clear it -- measured at 111 consecutive failures with no way out, which is a gate any local process can kill permanently. After five consecutive failures the listener is now rebuilt. That releases and retakes the pipe NAME, which normally must never happen, but the alternative is an instance that has stopped accepting for good; if the name cannot be retaken the loop ends, because this process is then genuinely no longer the holder. A Thread.Sleep(50) bounds the retry to 20/s so a failing wait costs a delay nobody can perceive rather than a burnt core. Two tests, both cross-platform. The dispose-race one does not fail as a red test if it regresses -- it takes the run with it, which is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExF9yGxB83NgQo6zJwcQMM
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.
Three defects in
AcceptLoop, all found by driving the gate hard on Linux (4 cores, 8 gates at a time, under load) while porting it into a PDF viewer. None is Linux-specific; that is only where the timing exposes them.1. Shutdown killed the process
catch (Exception ex) when (!_stopping)is an exception filter, and a filter that declines does not swallow. OnceDisposehas set the flag, the exception from the in-flight connection leavesAcceptLoopand goes unhandled on that thread, taking the whole process with it.Disposing while a hand-off is in flight is common enough under load that it killed a 400-round harness outright rather than failing a round:
The flag is now read inside the catch, so shutdown is a
returnand everything else is logged.2. A client that gave up before writing left the holder deaf
Releasing the connection was gated on
IsConnected, which tracks the local handle and is already false by then, so the instance was never disconnected and every later hand-off met a listener that could only throw.Disconnectis unconditional now; it throws when there was nothing to release, and that is what the catch is for.3. A wedged handle never recovered
A client that connects and vanishes before the accept returns can leave the instance throwing
ERROR_PIPE_CLOSINGfromWaitForConnectionfor ever, andDisconnectdoes not clear it:That is a gate any local process can kill permanently. After five consecutive failures the listener is rebuilt, which recovers in ~344 ms:
Rebuilding releases and retakes the pipe name, which normally must never happen — but the alternative is an instance that has stopped accepting for good, so a window of microseconds beats a permanent outage. If the name cannot be retaken the loop ends, because this process is then genuinely no longer the holder. A
Thread.Sleep(50)bounds the retry to 20/s, so a failing wait costs a delay nobody can perceive rather than a burnt core.Tests
Two, both cross-platform.
Disposing_while_a_hand_off_is_in_flight_does_not_take_the_process_downdoes not fail as a red test if it regresses — it takes the run with it, which is the point, and is why it loops rather than trying once.Suite: 22/22, and it got faster (6s to 2s) because the deaf-holder case no longer burns its 5 second timeout.
🤖 Generated with Claude Code
https://claude.ai/code/session_01ExF9yGxB83NgQo6zJwcQMM