Skip to content

Bound a channel's sends: macOS ignores MSG_DONTWAIT on a blocking unix socket - #302

Closed
scgopi wants to merge 2 commits into
mainfrom
fix/291-bounded-sends
Closed

Bound a channel's sends: macOS ignores MSG_DONTWAIT on a blocking unix socket#302
scgopi wants to merge 2 commits into
mainfrom
fix/291-bounded-sends

Conversation

@scgopi

@scgopi scgopi commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Split out of the #289 diagnostics PR at PerfTriage's request: it fixes shipped behaviour (#291 is in 0.1.64-beta1), it is two lines with its own test, and #289's blocked_ms accounting depends on it.

The defect

OutboundChannel.writeFrame claimed to be non-blocking per call (MSG_DONTWAIT) and to wait for writability in bounded poll slices — which is what let closeAndWait promise to return on a peer that stopped reading. On macOS the claim was false: send(2) with MSG_DONTWAIT on a blocking AF_UNIX stream socket blocks anyway; the flag is ignored. Reproduced three ways — Python socketpair with 4 KB buffers (60 KB send: still inside the syscall after a second; PerfTriage saw 120 s), and through the channel itself (60 KB queued, 4096 readable at the peer, no line from the loop). So the writer parked inside the syscall and never reached its loop.

What that did and did not break: the actor was never exposed — the per-connection thread is the fix that mattered, and #288's stall is genuinely gone. What was wrong is teardown: closeAndWait after shutdown still returns (a shutdown does wake a parked send here), but detach — the path for a descriptor number a new connection has already taken over — cannot shut the socket down, and a writer parked in send never notices isClosing; it waits for the dead peer's lifetime. And nothing in the loop could be measured.

The change

SO_SNDTIMEO, one poll slice (50 ms), set on the channel's socket at open (boundSends). A blocking send then returns what it wrote, or EAGAIN when nothing went, after the slice — exactly the shape the loop was written for — and poll(POLLOUT) reports honestly afterwards (verified: 60 KB into a 4 KB peer returns 4096 after 0.052 s; the following poll says not-ready). It bounds sends only: the reader sharing the descriptor keeps its blocking reads, which O_NONBLOCK could not promise since it lives on the open file description. MSG_DONTWAIT stays for Linux, where it is honoured. The comments on writeFrame now describe the mechanism that works rather than the one that did not.

Verification

OutboundChannelBoundedSendTests: a detached writer wedged on a deaf 4 KB peer retires within a bounded time (fails without the line — the writer never returns from send and the test process cannot exit; passes with it), and a slow reader still receives a whole 20 KB frame. Existing OutboundChannelTests and OutboundChannelReviewTests pass unchanged.

Gate: full Xcode suite (gate on this head in flight; reported in a comment when it completes), swiftlint 0 errors, swift-format clean, graphcoded and graphcode-cli schemes build; SwiftPM swift build passes (the Linux branch keeps MSG_DONTWAIT).

Refs #288, #291, #289.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DeGL2CxuGmq16RSZpJYm2N

scgopi and others added 2 commits September 6, 2026 13:38
…x socket

OutboundChannel's writer claimed to be non-blocking per call and to wait
for writability in slices, which is what let closeAndWait promise to
return on a peer that stopped reading. On macOS the claim was false: send(2)
with MSG_DONTWAIT on a blocking AF_UNIX stream socket blocks anyway, so the
writer parked inside the syscall and never reached the loop — teardown of
a wedged client was unbounded. SO_SNDTIMEO, one slice long, set on the
socket at open, makes the call return what it wrote (or EAGAIN) after the
slice, which is the shape the loop was written for; the reader sharing the
descriptor keeps its blocking reads. The comments now describe the
mechanism that works. The actor was never exposed — the per-connection
thread is the fix that mattered — this is teardown.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DeGL2CxuGmq16RSZpJYm2N
@scgopi

scgopi commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Duplicate of #301, which landed the same SO_SNDTIMEO fix, corrected comments and a test on main while this was gating (we found it in parallel). The one thing here #301 does not have — a test that a detached writer wedged on a deaf peer retires within a bounded time, the path that cannot shutdown — moves into the #289 diagnostics PR, whose blocked_ms accounting is what needed it.

@scgopi scgopi closed this Sep 6, 2026
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