Release the flux-network changes as 0.2.0 - #146
Closed
Bronek wants to merge 40 commits into
Closed
Conversation
…stream network
Record the design for flux-network before the implementation stack lands:
- CONTEXT.md: the workspace glossary (Tile, Spine, Signal, Waker; Stream, Group,
Tenant, Raw group, Owned/External mode, Endpoint, Peer, Deadline, Draining,
Lingering, Refused).
- ADR 0001: poll ownership is a construction-time mode of the stream network, and
protocol layers are tenants the network schedules — one driver owns deadline
folding, routing by group, maintenance order and ticking; tenants expose their
events by pull; hooks stay private behind an opaque TenantRef carrier.
- ADR 0002: transports are the closed set Endpoint { Tcp, Unix }.
- ADR 0003: Unix-domain socket files are probed (lstat, then connect) before a
stale one is replaced, and removed on close.
- docs/extension-points.md: eventual (streamed responses) and speculative (TLS,
Unix socket file mode, QUIC) directions the design keeps open without deciding.
All three ADRs are accepted.
Assisted-by: Claude Code:claude-fable-5
The networking glossary names a ConnectionGroup — the connections sharing one configuration and one owner, together with the listeners and outbound endpoints that produce them; a Service, the stateful protocol layer that owns one group inside a shared network and is scheduled by it; and an unclaimed ConnectionGroup, one no Service has claimed, whose events the caller takes inline through the closure it passes to drive. Owned poll and External poll name the two poll-ownership choices. ADR 0001 speaks of services with the same generality: a service is any protocol layer the network schedules, and HttpService is the first of them. Its decisions, options and consequences carry over untouched; only the vocabulary moves. ADR 0003 names the ConnectionGroup for the reconnect interval it configures. docs/extension-points.md leaves the tree. Its purpose was informational — directions the design keeps open, deciding nothing — and reviewers read that as scope instead. It stays in the maintainers' notes, where a direction can be written down without reading as a commitment, and ADR 0003 states the Unix file-mode setting as speculative in its own words. This answers the review on #135. Assisted-by: Claude Code:claude-opus-5
The probe connects without blocking, so a live owner whose accept queue is full is reported rather than waited for: a connection that completes or is left pending fails the bind with AddrInUse, and any other probe error is returned as it is, naming the path, instead of being read as a live owner. Assisted-by: Claude Code:claude-fable-5
An iteration captures the time once for validation, maintenance and the deadline fold, but the poll wait that follows may last the whole timeout, so the service ticks are given the time the wait ended: a timer a tick starts runs from the end of the wait, and one that expired during it is due in the same iteration rather than the next. Assisted-by: Claude Code:claude-fable-5
The network's unit is a Stream — an ordered byte channel, TCP or Unix-domain (CONTEXT.md, ADR 0002) — so the type, its event and group types, and the module take the stream name: TcpNetwork → StreamNetwork, TcpEvent → StreamEvent, TcpGroup → ConnectionGroup, TcpGroupConfig → ConnectionGroupConfig, module tcp → stream. The private module file tcp/stream.rs becomes stream/tcp_stream.rs, since stream/stream.rs would trip clippy's module_inception; its content is unchanged. Integration tests that drive StreamNetwork are renamed tcp_network*.rs → stream_network*.rs; the TcpConnector-only tests keep their names. TcpConnector, TcpStream, TcpTelemetry, ConnState and HttpNetwork keep their names, and runtime error and panic strings keep their wording. No behaviour changes. Assisted-by: Claude Code:claude-opus-5 Assisted-by: Claude Code:claude-fable-5
StreamNetwork listens and connects on `Endpoint { Tcp(SocketAddr),
Unix(PathBuf) }` and reports the remote end as `Peer { Tcp(SocketAddr),
Unix }`, so one poll holds both transports and one group mixes them.
ADR 0002 fixes the set: an internal enum over the two mio listener types
and the two mio stream types costs one match per operation, where a type
parameter would forbid mixing and a trait object would put a virtual
call on every read and write. Parsing an address out of text stays with
the caller.
TCP_NODELAY, keepalive and TCP_USER_TIMEOUT do not exist for a
Unix-domain socket and dispatch to a no-op there; SO_SNDBUF and
SO_RCVBUF reach both through the socket's raw fd. A Unix-domain outbound
endpoint is persistent like a TCP one: ENOENT for a socket file that has
not appeared yet, and ECONNREFUSED for one with no listener, each count
as a failed attempt and retry at the group's reconnect interval.
HttpNetwork follows the same signatures, and a request with no caller
Host header writes `Host: localhost` for a Unix-domain endpoint, which
has no address to name.
Runtime strings that called the network or a group TCP would now be
false, and drop the word:
unknown TCP group -> unknown connection group
couldn't set up a poll for tcp network -> ... for the stream network
couldn't poll tcp network -> couldn't poll the stream
network
tcp token space exhausted -> stream token space
exhausted
max_frame_size exceeds the TCP wire -> ... exceeds the wire
length field length field
couldn't start tcp connection -> couldn't start connection
couldn't register connecting tcp stream -> ... connecting stream
tcp connection attempt failed -> connection attempt failed
couldn't register connected tcp stream -> ... connected stream
tcp connection established -> connection established
tcp accept failed -> accept failed
couldn't register accepted tcp stream -> ... accepted stream
tcp connection accepted -> connection accepted
ignoring stale tcp readiness event -> ignoring stale readiness
event
tcp payload exceeds maximum frame size -> payload exceeds maximum
frame size
tcp send backlog growing -> send backlog growing
tcp send backlog would exceed -> send backlog would exceed
configured maximum configured maximum
tcp raw read failed -> raw read failed
tcp header read failed -> header read failed
tcp payload read failed -> payload read failed
tcp frame exceeds configured maximum -> frame exceeds configured
maximum
tcp frame write failed -> frame write failed
tcp backlog write failed -> backlog write failed
The default ConnectionGroupConfig name changes from "tcp" to "stream" for
the same reason; it labels a group that may now hold either transport.
The nodelay and keepalive warnings keep "tcp stream" because only a TCP
socket reaches them, and the tcp_latency_/tcp_alloc_ metric prefixes are
untouched.
The core accept, message and disconnect tests, and the HTTP accept,
request, respond, keep-alive and pipelining tests, run over both
transports from one body. Tests that turn on SO_SNDBUF backpressure or
talk to TcpConnector stay TCP-only.
Assisted-by: Claude Code:claude-opus-5
A listener binding an Endpoint::Unix whose path already exists resolves it the way ADR 0003 settles: `lstat` the path without following it, and leave anything that is not a socket — a regular file, a directory, a symbolic link even when it points at a socket — exactly where it is, failing the bind with AlreadyExists and an error naming the path. For a socket, connect to it: a refused connection means no process is listening, so the file is a remnant of one that did not clean up and is unlinked before the bind; a connection that succeeds means a live server owns the path, and the bind fails with AddrInUse. The `lstat` is what makes the unlink safe. Connecting to a regular file is refused too, so a probe on its own proves nothing about what the path holds and would let a bind delete an unrelated file it happened to be pointed at. The probe is nonblocking, because the owner ADR 0003 is guarding against is often one that has stopped accepting: a full accept queue answers WouldBlock, which reports AddrInUse rather than sleeping until the owner comes back. Closing the listener unlinks its socket file, so a crash never leaves a path that blocks the next start. There is no API for removing a single listener today, so the unlink lives in the listener wrapper's Drop and runs when the StreamNetwork is dropped. Assisted-by: Claude Code:claude-opus-5
Endpoint is the closed set of stream transports and the only form the network accepts, so callers name the variant. The conversion from SocketAddr had no callers and privileged TCP over Unix-domain paths. Assisted-by: Claude Code:claude-fable-5
`ConnectionGroupConfig` carries three options that only a TCP connection has: `TCP_NODELAY`, keepalive, and `TCP_USER_TIMEOUT`. A `TcpOptions` sub-struct states that scope in the type, and lets the note about Unix-domain connections ignoring them sit on a single field. `TcpOptions::default()` holds the defaults every group starts from: nodelay on, keepalive off, and `DEFAULT_TCP_USER_TIMEOUT_MS`. Assisted-by: Claude Code:claude-opus-5
A protocol layer such as an HTTP server owns one group inside a shared network, and the network schedules it: `drive` captures the time, runs maintenance, folds every deadline into one poll, routes each event to the group's service or to the unclaimed handler, and ticks each service in slice order with the time the wait ended, reporting whether anything happened. The scheduling hooks live on a flux-internal trait, so a service hands the network an opaque `ServiceRef` and nothing outside flux can implement or call a hook. Groups no service claimed stay raw: their events reach the handler as they arrive, lending each payload for the call, and `poll_with` is the nonblocking driver call for a network of those alone. A service's claim is validated before anything else happens on every driver call, so a service that was dropped instead of closed, or one supplied twice, is a configuration error reported at the next call rather than a timing-dependent one. Assisted-by: Claude Code:claude-opus-5
The HTTP layer owns one unclaimed group of a network the caller drives, not a network of its own: `HttpService::new` claims a caller-created group, every network-touching call takes the network, and `close` hands the group back, empty and raw. Transport and queue policy stays on the group; `HttpConfig` carries what parsing and connection state need. Events are pulled rather than pushed. `next_event` parses the next ready connection on demand and hands out a request borrowed from its buffer beside a `Responder` that writes the answer, so a request is answered exactly once and never copied to make a response possible. Dropping the responder answers later by token. Readiness is connection state, so a caller may stop pulling and resume in a later iteration, and `tick` applies the bookkeeping the last pull left behind so that work is reported while any pullable event remains. A connection reclaims what it has answered by cursor: parsing runs from `start`, a response moves `consumed`, and the buffer is compacted only once the answered prefix is half of it, which a benchmark measures at a quarter of the cost of dropping the prefix after every response. Request metadata is kept as byte ranges the service owns, so an event outlives the parse that produced it. Assisted-by: Claude Code:claude-opus-5
The test pushes 8 MiB through a 1 KiB socket buffer to a receiver that sleeps first, and how long the queue takes to drain is the receiver's and the scheduler's business, not a number the test can pick. A fixed five-second pump was on the edge of it and failed a run in several under load. Each collector now announces the frame the test waits for, and the pump runs until both announcements arrive; the remaining deadline only exists to fail loudly rather than hang. Every assertion and the payload size stay as they were: real backpressure is the point of the test. Assisted-by: Claude Code:claude-opus-5
An inline answer runs inside the event it was handed, which borrows the connection until it is dropped, so the service records the token and the next driver call reclaims the answered bytes and queues whatever is behind them. An answer by token borrows nothing and reclaims before it returns. Both advance `consumed` at once, so the two paths judge a connection's limits alike and write the same bytes in the same order. Assisted-by: Claude Code:claude-opus-5
Integration tests observe HTTP from outside the service: the bytes on the wire, the order requests are served in, what a limit accepts, and what a driver call reports as work. Unit tests beside the code read the representation directly — `start`, `consumed`, `req_end`, the compaction threshold and the ready queue — so the service exposes no accessor for what it keeps to itself, and `cursors` and `ready_len` go with it. Assisted-by: Claude Code:claude-opus-5
Add an External poll mode built from the caller's mio Registry and a token base. The caller folds next_deadline into its poll timeout, passes each readiness event to handle_event, and calls tick once per iteration. Keep poll ownership fixed at construction. Owned networks use drive and may hand out one reserved-token waker; External networks use the three-phase interface and leave foreign events untouched. Each mode rejects the other mode's driving operations. Allocate network tokens upward without reuse so handle_event can distinguish network events from the caller's sources. Stale network tokens remain classified as ours. Readiness handling drains any disconnect it produces before returning, while maintenance and service ticks reuse the same internal phases as Owned driving. Re-export mio so callers can use the exact Poll, Registry, Event and Waker types exposed by the External interface without coordinating a separate dependency version. Exercise the External loop with HTTP over both transports, deadline and reconnect handling, foreign and stale events, mode misuse, wakers, token-space bounds and per-event disconnect delivery. Assisted-by: Claude Code:claude-fable-5 Assisted-by: Claude Code:claude-opus-5 Assisted-by: Codex:gpt-5
shutdown_write_when_drained ends a connection's outbound stream without giving up what the peer still has to say: the write side shuts as soon as the queued bytes are written, and the connection stays registered and readable. The peer reads the end of the stream, the bytes it sends afterwards still arrive as Message, and its own close still arrives as Disconnected. Both transports half-close, which is what ADR 0002 asks of them. Sends to such a token are rejected and queue nothing, so the queue only shrinks from the request onward. A hard close still ends the connection at any point, and a token that is draining as well closes when its queue empties rather than half-closing. Assisted-by: Claude Code:claude-opus-5
A group can bound how many connections it holds: max_connections is enforced in the accept loop, which accepts the connection arriving at a full group and drops it where it stands — no registration, no bytes, no event — and drains the rest of the backlog, as an edge-triggered listener requires. refused_connections reports how often that happened, and a warning names the group at the ten-second cadence the backlog warning already uses, never once per refusal. The cap counts what the group accepted and still holds, a connection it is closing included: draining and half-closed connections hold their places until they are gone. Outbound endpoints and listeners are not accepted connections and count for nothing. Nothing above the transport takes part: an HttpService on a capped group never hears of the connection its group refused. Assisted-by: Claude Code:claude-opus-5
Three rules the half-close tests left free to be negated: that the hard close wins over the half-close whichever was asked for first, that a broadcast passes over a half-closed member rather than writing to it, and that a reconnecting outbound endpoint gets its write side back. Each new test fails if its rule is reversed — the drain gate's branches swapped, the per-member broadcast filter dropped, the reset in disconnect_index deleted — and each runs over both transports, none of the three being a matter of which one carries the bytes. The recorder the file already had now keeps every lifecycle event with its token, which is what lets a test say which connection was closed and which one a payload arrived on. The write side of a shut connection is one the peer can read the end of the stream from, whether or not it has; the rustdoc of WriteSide::Shut said it already had. Assisted-by: Claude Code:claude-opus-5
Admission compares a count the group keeps, incremented when an accepted connection is inserted and decremented at each of the three places one is removed, so refusing a connection at the cap costs the same however many connections the network holds. A refusal flood is the case the cap exists for, and a scan of every connection per arrival made that cost grow with the network, unrelated groups and outbound endpoints included. The count is a private invariant of the group that owns the policy. The tests pin every path that moves it — peer disconnect, local disconnect, removal and group close — and that two listeners of one group share one cap. Assisted-by: Claude Code:claude-opus-5
A client that meets a reset before it has read the answer to its upload reports a node that is down rather than the status it was sent. A response that closes a connection now shuts the write side alone whenever the request stream behind it was cut short — every status the service raises itself, and a connection driven over its buffer limit — and reads its peer out under an idle and a total cap until the peer stops, the caps run out, or it closes. The bytes over the limit are no verdict on a request already delivered: a connection holding one keeps it, and the answer the caller is producing carries the close. Lingering connections answer to the linger's caps rather than the idle sweep, and hold their place in the group meanwhile. Assisted-by: Claude Code:claude-opus-5
A request to an endpoint that answers nothing used to wait as long as the connection stood, and a peer that broke the protocol dropped the connection with nothing to say for it. An outbound request now carries a deadline from the moment its bytes are queued — a request waiting behind a backlog is already waiting — and every request that will not be answered names its reason: the deadline, an answer the service cannot frame, an answer over the caps the operator sets, or an endpoint that closed with the request in flight. The failure reaches the caller before the disconnect it causes, so a puller learns why the connection went before learning that it did. Assisted-by: Claude Code:claude-opus-5
An endpoint that echoes a status chosen elsewhere needs the whole range and the phrase-free status line RFC 9112 §4.1 permits, so a status the service has no phrase for is framed as `HTTP/1.1 250 ` rather than carrying a phrase nobody chose. A caller-chosen 1xx is final: it completes the request, framed with no length and no body. `reason_phrase` is public, and the phrase it returns for a status it does not name is now the empty string rather than `Unknown`. Assisted-by: Claude Code:claude-opus-5
The caps were timing the wrong thing. They ran from the moment the answer was queued, so a large answer over a small socket was cut off mid-body — the idle cap ending a connection that had delivered a fraction of the length it announced, which is the reset lingering exists to avoid. They now time what they name: the reading and discarding that begins where the answer ends. A connection still writing is bounded by its transport, exactly as a draining one is. This is the one network-side addition PR B needs: `write_side_shut` reports that the peer has the whole of what was queued and the end of the stream after it, which is where the clock starts. A linger whose clock has yet to start folds no deadline of its own, since the write it is waiting on is what wakes the poll. A connection over its buffer limit with nothing pending is now answered with the 431 every other head too large for the service gets, rather than dropped where it stands: an over-limit that lands exactly on a request boundary closes the way one that lands mid-request already did, however the request before it was answered. Assisted-by: Claude Code:claude-opus-5
The reason an outbound request carries is what tells an operator to raise a cap apart from what tells them an endpoint is broken, so each path that picks one is pinned: a head over its cap, a chunked body over its cap and one the close delimits over its cap all report `TooLarge`, while a chunk size the service cannot read and an answer framed twice over report `Malformed`. Each asserts the failure reaches the caller ahead of the disconnect that follows it. A request deadline also has to be the deadline a blocking drive waits for, which is what the last test holds it to. Assisted-by: Claude Code:claude-opus-5
The caps time the reading and discarding, so a connection whose answer has yet to reach its peer runs under none of them. A peer that stops reading held such a connection open — and its place in the group — against nothing but the transport, which bounds a TCP peer late and a Unix-domain one never. The sweep now lets go of a lingering connection only once its own clock has taken over. Until then it is held to the same idle bound as any other, a draining connection included, and folds that bound into the deadline rather than none. Assisted-by: Claude Code:claude-opus-5
Every parse borrowed a fresh header vector, every outbound request owned a copy of its method and of the host it addressed, and every response carried its body through the caller's slice. A warm connection needs none of it: the scratch a parse borrows is bounded, all that is asked of a request in flight is whether it was HEAD, and a body composed into a buffer the service keeps reaches the wire in one copy. Parsing borrows a stack array of MAX_HEADERS entries sliced to what the configuration asks for, and with_max_headers refuses a larger cap. An outbound connection records the request in flight rather than its method. request() writes the Host it defaults to straight into the send buffer. respond_with lends a closure the service's body scratch and frames the Content-Length it wrote, HEAD included; respond is that call with the body it was handed. Framing stays in one place, now the Responder the request carries, and the wire bytes are unchanged. Assisted-by: Claude Code:claude-opus-5
A warm keep-alive connection in each direction runs a thousand round trips under a global allocator that counts allocation events on the thread driving them, and each direction is asserted to have made none. The peers are plain sockets driven from the same thread between iterations of the network, so nothing else runs while a count is open and nothing the peers do grows a buffer of their own. The bytes each side reads are checked against the message it expects, so a direction cannot pass by falling silent. flux-profiler's CountingAllocator tallies bytes rather than events and keeps its counters to the profiler, so the test brings its own. Assisted-by: Claude Code:claude-opus-5
A lingering connection arms its caps once the transport has taken the whole answer, and finding that out walks the network's connections. The linger loop asked for every lingering connection on every tick; it now asks only for one whose clock has yet to start, so a connection whose caps are already running costs nothing further. `write_side_shut` says what it means: the transport has taken everything queued for the connection and the end of the stream after it, with up to a socket buffer of that still on its way, not that the peer has read it. The test pins that the caps run from the moment the answer has left, which is the time the ticks are given, rather than from the start of the poll wait that preceded it. Assisted-by: Claude Code:claude-opus-5
A head that never ends is caught by the cap while the parse is still incomplete, which is a path of its own: an answer whose open head runs past the cap now pins `TooLarge` there, where before only the head that parsed whole was held to it. A request ending exactly at the buffer limit has two correct closes, and which one the peer reads is settled by whether a tick falls between the request and its answer. The pair that answer first are named for that, and the third holds the other outcome: an answer that carries the close itself, with no rejection behind it. Assisted-by: Claude Code:claude-opus-5
A response the service refuses never reaches its body: the status range, the header names and the pending request are all settled before the closure that would compose one runs. Nothing said so, and nothing would have noticed the check moving below the render. The header count had the same gap on the other side. Its three tests covered the builder alone, so what the count decides on the wire --- a request at the count served, one over it answered 400, and a count written past the scratch held to what the scratch holds --- went unpinned, as did the answer an endpoint sending too many headers gets. Both new server-side tests run over a socket file: what a header count decides is the parse, and the loopback ports the rest of the suite shares are contended enough already. The zero-alloc harness takes the connection its service dialled straight from the listener rather than opening a placeholder first, and records what one round trip already settles. Assisted-by: Claude Code:claude-opus-5
`HttpConfig`'s fields are public, so a configuration is as likely written whole as built call by call. The builder refuses a header count outside `1..=MAX_HEADERS`; a field written straight into the struct was quietly held to `MAX_HEADERS` at every parse instead, so the same configuration meant two different things. `HttpService::new` validates the configuration it is handed, sharing the check with the builder so the rule and its message cannot drift, and the parse borrows the scratch the count names. Assisted-by: Claude Code:claude-opus-5
`write_side_shut` answers a question the lingering close asks of its own transport, and nothing outside the crate has cause to ask it. It is `pub(crate)`, and the linger test that reached for it measures the same instant from where a peer can see it: the client reads the whole answer and then the end of the stream, which is the answer having left. The test keeps its point by timing that instant against both ends. The peer takes nothing for half a second, so the answer leaves only after the pause -- and the caps end the connection an idle cap after that, rather than an idle cap after it was queued. Assisted-by: Claude Code:claude-opus-5
`buffered` handed the tests a connection's byte count, which is the service's own representation and nothing a caller has business reading. It goes, and what it stood in for is measured where it belongs. The lingering close's discard is a state transition, so it is pinned by a unit test that drives a connection through it: bytes read while lingering leave the buffer empty, leave the total cap where the answer put it, and move the idle cap to the read. The wire-level test keeps what a peer can see -- the answer, then the end of the stream, and a client that goes on uploading uncut. The gates that make the over-limit-while-pending path deterministic ask the network instead. An accepted connection is polled for reads alone until the service writes, and a raw read empties the socket, so between a request and its answer the one iteration reporting work is the one that took the overrun whole. Assisted-by: Claude Code:claude-opus-5
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
flux-network's public API is incompatible with v0.1.3: the tcp module and TcpNetwork family become stream and StreamNetwork; listen and connect now take an Endpoint; events report a Peer; listen returns the endpoint it bound; and the default group name changes from "tcp" to "stream". Under Cargo's pre-1.0 compatibility rules, this requires a minor version bump. Flux uses one version for every workspace crate, so the workspace moves to 0.2.0 together. Assisted-by: Claude Code:claude-fable-5 Assisted-by: Codex:gpt-5
13 tasks
Bronek
marked this pull request as draft
August 28, 2026 11:15
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.
DRAFT: FURTHER REFACTORING COMING, BELOW WILL BE SOON OBSOLETE
This PR builds on #145 and changes only the workspace version in
Cargo.tomland the derived workspace-package versions inCargo.lock.The networking changes since
v0.1.3break public API compatibility, so Cargo's pre-1.0 compatibility rules require a minor version bump. Flux uses one version for every workspace crate; the complete workspace therefore moves from0.1.3to0.2.0.Closes #143.
Release highlights
StreamNetworksupports TCP and Unix-domain endpoints through one connection lifecycle, with network-owned or caller-owned polling.HttpServiceinstances can share one network and poll.listenreports the endpoint it bound, including a TCP port selected by the kernel.HTTP server example
StreamNetwork::default()owns its poll anddrive(None, ...)blocks until I/O or the next deadline. Applications that already own amio::Pollcan instead construct the network withwith_registryand callnext_deadline,handle_event, andtickfrom their loop.Migration from 0.1.3
The released
v0.1.3networking API was TCP-only. Its users need the following source changes:flux_network::tcpflux_network::streamTcpNetwork,TcpEvent,TcpGroup,TcpGroupConfigStreamNetwork,StreamEvent,ConnectionGroup,ConnectionGroupConfiglisten(group, SocketAddr),connect(group, SocketAddr)listen(group, Endpoint),connect(group, Endpoint)usingEndpoint::Tcp(addr)orEndpoint::Unix(path)listen(...) -> io::Result<()>listen(...) -> io::Result<Endpoint>; TCP port 0 returns the selected portpeer_addr: SocketAddrpeer: Peer, matched asPeer::Tcp(addr)orPeer::UnixTcpGroupConfigConnectionGroupConfig::tcp: TcpOptionswith the same defaults"tcp""stream"; callers relying on default log labels or telemetry suffixes will observe the changeTcpConnector,TcpStream, andTcpTelemetrykeep their names but move from thetcpmodule tostream.There is no HTTP migration from
v0.1.3: that release did not contain the HTTP module.HttpServiceand its resilience features are new in0.2.0.Verification
The release checks pass with the updated lockfile:
Base: #145.
Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5