Bound a blocking send, which MSG_DONTWAIT does not (#291) - #301
Merged
Merged
Conversation
#291 replaced the channel's blocking write with `send(… MSG_DONTWAIT)` and a poll loop, on the claim that no single send could then park and that `closeAndWait` was therefore bounded. The flag does not do that. On macOS `MSG_DONTWAIT` has no effect on `send` for a blocking AF_UNIX stream socket: measured here, 60 KB into a peer with a 4 KB buffer was still inside the syscall after 120 seconds. So the poll loop never ran and the writer parked in the send exactly as before. The actor was never at risk — that is the writer thread's doing, and #288's stall is genuinely fixed — but the teardown guarantee was not real. A wedged peer could still hang whoever was disconnecting it, which is the same bug one layer down. `SO_SNDTIMEO` is the half that works. It bounds sending only, so the reader that shares this open file description is untouched, which is why `O_NONBLOCK` was rejected: a reader returning EAGAIN would tear down every connection. With it a full peer returns a short count after one slice, and #291's loop does what it always claimed to. `MSG_DONTWAIT` stays — it is honoured on Linux and costs nothing where it is not — but the comments no longer credit it with the guarantee. They described a mechanism that never ran, which is worse than no comment, because the next person to touch this would have trusted them. Found by BroadcastSlimming, who reproduced it twice in Python and once through `OutboundChannel` itself before reporting it; confirmed here independently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9
This was referenced Sep 6, 2026
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.
Two lines and a correction to comments I wrote in #291.
The claim that was wrong
#291 replaced the channel's blocking write with
send(… MSG_DONTWAIT)plus a boundedpoll, on the stated grounds that no single send could then park, and thatcloseAndWaitwas therefore guaranteed to return.The flag does not do that. On macOS
MSG_DONTWAIThas no effect onsendfor a blocking AF_UNIX stream socket. Measured directly:So the poll loop never ran, and the writer thread parked inside the syscall exactly as it had before the change.
What was and wasn't affected
Not affected: the actor. That protection comes from the writer thread, not the flag, and #288's stall is genuinely fixed — verified against the shipped 0.1.64-beta1 daemon at 0.003 s with a deaf client attached, against a 12 s timeout on 0.1.63.
Affected: the teardown guarantee.
closeAndWaitwas not bounded, because a thread already blocked inside a send is not reliably woken by another thread'sshutdown— which is exactly the reasoning #291 used to justify moving off blocking writes in the first place. The bug was one layer down from where it was fixed.The fix
SO_SNDTIMEOon the channel's descriptor at open, at the same 50 ms as the poll slice.It bounds sending only, so the reader that shares this open file description is untouched — which is why
O_NONBLOCKwas rejected in #291 and is still the wrong tool: a reader returningEAGAINwould tear down every connection. With the timeout set, a full peer returns a short count after one slice and #291's loop does what it always claimed to.MSG_DONTWAITis kept — it is honoured on Linux and costs nothing where it is not — but the comments no longer credit it with the guarantee.The comments
Three comments described a mechanism that never ran. That is worse than no comment: the next person to touch this would have trusted them and reasoned from a false premise. They now say what actually holds, and
boundBlockingSendscarries the measurement so the reason survives.Verification
theChannelBoundsHowLongOneSendMayParkassertsSO_SNDTIMEOis set and within a sane bound — a direct, non-flaky pin on the fix rather than a timing test.xcodebuild testexit 0, no restarts. swiftlint 0 errors, swift-format clean.Credit
Found by the
BroadcastSlimmingloop, which reproduced it twice in Python and once throughOutboundChannelitself before reporting it. I confirmed it independently before writing this — my own probe hung for 120 seconds, which was the proof.Related to #288, #291.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9