Skip to content

sendfile(2) binding (foundation, no caller yet) - #102

Merged
codetalcott merged 3 commits into
mainfrom
claude/sendfile-binding
Aug 25, 2026
Merged

sendfile(2) binding (foundation, no caller yet)#102
codetalcott merged 3 commits into
mainfrom
claude/sendfile-binding

Conversation

@codetalcott

Copy link
Copy Markdown
Owner

Groundwork for the zero-copy static-file step docs/ROADMAP.md records. No caller yet — the event loop can still only send a flat buffer it already holds, and giving it an fd-backed response body is the larger change this unblocks. Opening it separately because it is self-contained, independently testable, and makes the eventual feature PR smaller.

Why a module and not two external_calls at the use site

The platforms disagree about nearly everything except the name:

Linux   ssize_t sendfile(out_fd, in_fd, off_t *offset, size_t count)
Darwin  int     sendfile(fd, s, off_t offset, off_t *len, hdtr, flags)

Two traps, both silent:

  • The first two arguments are in the opposite order. Darwin takes the file then the socket; Linux takes the socket then the file. A swap compiles cleanly on both and fails only at runtime — which is why the tests actually move bytes through a socketpair and compare them, rather than checking a return code.
  • Darwin reports its count on EAGAIN too. A short write to a non-blocking socket is -1 with a positive len. A caller reading -1 as "nothing happened" would resend those bytes and corrupt the response. SendFileResult carries sent alongside again so that state cannot be expressed.

No hdtr on the Darwin side: Linux has no equivalent, so a portable caller must send headers separately anyway, and using the header vector on one platform only would mean two orderings to reason about for no gain.

Notes

The in/out parameter is a stack local addressed with Pointer(to=) rather than a one-element heap allocation — the callee writes it before returning, so it cannot outlive the frame, and it keeps this file off the warning ratchet (alloc without a Layout is deprecated with no replacement on this toolchain). Ratchet holds at 68.

test_sendfile.mojo compiles and exercises the module on every test-http run, which is deliberately the guard this repo already uses for ffi_exports.mojo — the fix for the time packages/m0-core/ffi/ rotted because nothing compiled it. 5 tests: the no-op guards, a real whole-file transfer, and an offset transfer (the Range case). The partial-send path is documented as not covered here — forcing it needs a full socket buffer and a peer that never reads, which is timing-dependent at unit scale; it belongs in the loop-level smoke that serves a file larger than every buffer.

build-all + test-all clean.

🤖 Generated with Claude Code

The foundation for serving static files without copying them through
userspace. No caller yet — the event loop can still only send a flat
buffer it already holds, and giving it an fd-backed response body is the
larger change this unblocks.

The two platforms disagree about nearly everything except the name, and
this module exists to make them agree about what a caller wants: "send up
to `count` bytes from `in_fd` at `offset` to `out_fd`; tell me how many
went and whether to come back."

  Linux   ssize_t sendfile(out_fd, in_fd, off_t *offset, size_t count)
  Darwin  int sendfile(fd, s, off_t offset, off_t *len, hdtr, flags)

Two traps are worth naming, because both are silent:

- The first two arguments are in the OPPOSITE order. Darwin takes the
  file then the socket; Linux takes the socket then the file. A swap
  compiles cleanly on both and fails only at runtime, which is why the
  tests actually move bytes through a socketpair and compare them rather
  than just checking a return code.
- Darwin reports the count through its in/out `len` parameter on EAGAIN
  as well as on success — a short write is `-1` WITH a positive count. A
  caller that reads -1 as "nothing happened" would resend those bytes and
  corrupt the response. `SendFileResult` carries `sent` alongside `again`
  so that cannot be expressed.

No `hdtr` on the Darwin side: Linux has no equivalent, so a portable
caller has to send its headers separately anyway, and using the header
vector on one platform only would mean two orderings to reason about for
no gain.

The in/out parameter is a stack local addressed with `Pointer(to=)`
rather than a one-element heap allocation — the callee writes it before
returning, so it cannot outlive the frame, and it keeps this file off the
warning ratchet (`alloc` without a `Layout` is deprecated with no
replacement on this toolchain). Ratchet holds at 68.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`smoke-asgi-fanout` opens six streams against two workers and requires
them to land on both. Nothing makes that happen: with a shared listener
the accept race can legitimately put all six on one worker, and then
there is no cross-worker delivery left to verify.

Measured on an idle laptop, interleaving the two binaries over 20+ runs
each: main fails this roughly 5% of the time and so does the branch it
was found on. It is pre-existing and unrelated to whatever PR draws the
short straw — it just reads as a mystery failure there.

The fix separates the two outcomes, because they are not the same kind of
event. A stream that misses the broadcast is the real failure this probe
exists to catch, and still exits immediately — retrying it would turn a
genuine delivery bug into an intermittent one. Streams that failed to
spread are a failed *setup*: the attempt is discarded and the race is run
again, and only a run that cannot achieve spread in `FANOUT_ATTEMPTS`
(default 4) tries fails, because at that point it is no longer luck.

Verified load-bearing: asking for three workers from a two-worker server
retries and then fails, and the retry notice names what happened rather
than passing quietly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codetalcott

Copy link
Copy Markdown
Owner Author

CI note. The first smoke run here failed — on smoke-asgi-fanout, which is worth pausing on, because this PR adds only a dead syscall binding and its unit test. There is no runtime behaviour change of any kind, so it could not have caused it:

fanout: 6/6 streams delivered across 1 worker(s)
FAIL: streams did not span 2 workers

Note 6/6 delivered — fan-out worked. Only the spread precondition lost its race: nothing makes the kernel hand six near-simultaneous connections to both workers, and with a shared listener all six can legitimately land on one.

That makes this PR fairly strong evidence the flake is pre-existing, and it is now the second PR it has cost a red build. I've extracted the fix to #103, off main, so it can land on its own and stop doing this to unrelated work — and cherry-picked the identical commit here so this branch is green in isolation. It'll drop out on rebase once #103 (or #101, which carries the same commit) lands.

Clean automerge; the cherry-picked fan-out probe fix is byte-identical to
the one that landed via #103, so it resolves to a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codetalcott
codetalcott merged commit 89c209c into main Aug 25, 2026
4 checks passed
@codetalcott
codetalcott deleted the claude/sendfile-binding branch August 25, 2026 13:21
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