flux-network: return bound endpoints and remove a port-selection race - #145
Closed
Bronek wants to merge 5 commits into
Closed
flux-network: return bound endpoints and remove a port-selection race#145Bronek wants to merge 5 commits into
Bronek wants to merge 5 commits into
Conversation
Bronek
marked this pull request as draft
August 28, 2026 14:28
A caller who already holds its body has nowhere to render it, so the buffer the service keeps for composed bodies costs it a copy it never needed: once into the scratch, once out of it. A HEAD request paid for both and then discarded the result. Framing now takes the bytes wherever the caller holds them. respond hands write_framed the caller's slice, respond_with renders into the scratch and hands it that, and the admission rule both share sits in answerable, which respond_with also asks before composing anything. On the service, one path builds the responder and reclaims what its answer consumed, and the statuses the service raises itself carry an empty body rather than an empty closure. Assisted-by: Claude Code:claude-opus-5
A caller that asks for TCP port 0 lets the kernel choose the port and then has no way to learn it, so anything that must dial the listener has to pick a port up front. Picking one means binding a probe, reading its address and releasing it, and between the release and the real bind the port belongs to whoever takes it first. listen now returns the endpoint it bound. It is the endpoint asked for in every case but a TCP port of 0, where it carries the port the kernel chose, so a caller binds first and hands out an address that is already its own. Assisted-by: Claude Code:claude-opus-5
Every test binary picked its loopback ports by binding a probe, reading its address and releasing it, and one of them held colliding probes bound while it looked for a free port -- squatting on an address it had already handed to another thread. The victim's own bind then failed with AddrInUse, once in roughly thirty-five runs of the HTTP suite. Listeners now bind port 0 and the tests use the endpoint that comes back, so an address exists only once something holds it. The transport macros hand a body a request rather than a reservation, and the helpers that build a server -- Http, Server, Peer -- report where they landed. Two places still choose an address before it is bound, both because TcpConnector::listen_at takes one and reports none back: the wire compatibility test in stream_network.rs, which says so where it probes, and the tcp_* connector tests, which never shared the squatting helper. Assisted-by: Claude Code:claude-opus-5
The in-module harness listens on port 0 and dials the address the listener reports, rather than probing for a free port and hoping it stays free until the service binds it. The unit tests no longer race anything else on the machine for a port. Assisted-by: Claude Code:claude-opus-5
Both public answer paths ask answerable before anything is composed -- a refused status or header runs no body closure -- and write_framed, which they share, frames what they hand it as admitted. The check moves out of write_framed into respond, beside the one respond_with already made, so a composed answer scans its headers once rather than twice; a debug assertion keeps the invariant visible where the framing happens. One admission rule, one framing implementation, one body copy for a slice and no closure for a refused answer, as before. Assisted-by: Claude Code:claude-fable-5
Bronek
force-pushed
the
bronek/api_server-c
branch
from
September 3, 2026 13:54
97e76ad to
f93d2ee
Compare
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.
This PR builds on #144.
listennow reports the endpoint it bound, tests bind ephemeral ports without a probe-then-bind race, and slice response bodies go directly to the framing path without first passing through the service's composition scratch.Bound endpoints
StreamNetwork::listen(group, endpoint)andHttpService::listen(&mut net, endpoint)returnio::Result<Endpoint>instead ofio::Result<()>. The result matches the requested endpoint except when TCP port 0 is used, in which case it contains the port selected by the kernel; a Unix listener returns its path. A caller binding a fixed endpoint may ignore the value, while one requesting port 0 can publish an address that the listener already holds.Response framing
respond(status, headers, body)passes the caller's slice directly to the framing path, whilerespond_withrenders into the reusable service-owned buffer. Both use one framing implementation and copy the body into the send queue once. Admission requires a pending request, a status in100..=599, and headers the service does not reserve; it is checked before composition, so a refused answer does not run the body closure. AHEADrequest still uses the composed body length forContent-Lengthwhile sending none of the body.Tests bind ephemeral ports
The integration suites and in-module harness now bind TCP port 0 and dial the endpoint returned by
listen, so an address is not published until its listener holds it. The old helper released a probe before the real bind and, while avoiding reuse, could temporarily occupy a port already handed to another test thread; the victim then failed withAddrInUse.Verification
Tests cover a composed response followed by a slice response, preventing stale scratch contents from leaking, and pin refusal before body composition on both response paths. Workspace tests, Clippy with warnings denied, the nightly formatting check, and
cargo docpass without new warnings.Scope
The only API change is the return type of the two
listenmethods. The version bump that lists this and the stack's other breaking changes follows as the last PR.Base: #144. Next: the 0.2.0 bump.
Refer: #143
Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5