Skip to content

Bound a blocking send, which MSG_DONTWAIT does not (#291) - #301

Merged
scgopi merged 1 commit into
mainfrom
fix/291-bound-the-blocking-send
Sep 6, 2026
Merged

scgopi merged 1 commit into
mainfrom
fix/291-bound-the-blocking-send

Conversation

@scgopi

@scgopi scgopi commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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 bounded poll, on the stated grounds that no single send could then park, and that closeAndWait was therefore guaranteed to return.

The flag does not do that. On macOS MSG_DONTWAIT has no effect on send for a blocking AF_UNIX stream socket. Measured directly:

60 KB into a peer with a 4 KB buffer, MSG_DONTWAIT, blocking socket
  → still inside send() after 120 seconds

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. closeAndWait was not bounded, because a thread already blocked inside a send is not reliably woken by another thread's shutdown — 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_SNDTIMEO on 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_NONBLOCK was rejected in #291 and is still the wrong tool: a reader returning EAGAIN would 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_DONTWAIT is 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 boundBlockingSends carries the measurement so the reason survives.

Verification

  • theChannelBoundsHowLongOneSendMayPark asserts SO_SNDTIMEO is set and within a sane bound — a direct, non-flaky pin on the fix rather than a timing test.
  • Full gate: 1636 tests / 169 suites / 0 failures, xcodebuild test exit 0, no restarts. swiftlint 0 errors, swift-format clean.

Credit

Found by the BroadcastSlimming loop, which reproduced it twice in Python and once through OutboundChannel itself 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

#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant