Repository navigation
serve: close a connection that falls behind, and size its buffer in bytes - #266
Merged
Merged
Conversation
…ytes A connection dropped from a session's output fanout (`OutFanout::broadcast`, first failed `try_send`) was only unregistered. The attach loop's own `reply_tx` clone kept its channel open, so the socket stayed up: pings kept arriving and its commands still ran, but it never received another frame, never reconnected, and its session counted as attached and was never reaped. And healthy clients were being dropped. The buffer was a 1024-frame channel; the daemon's runtime is single-threaded by default, so the session's writer moves everything one model read delivered into it before the socket writer runs. A 3000-delta burst measured 252 KB queued at once and dropped a client that never stopped reading. Now each network connection has a `ConnTx`/`ConnRx` channel capped at 8 MiB of live, unwritten output (`CONN_BACKLOG_MAX_BYTES`, ~33x that burst). Backpressure is not an option: the event sink is synchronous, one writer task feeds every attached connection, and approval/elicitation/app frames are broadcast from sync code under the fanout lock, so a frame that does not fit can only be lost. A refused frame instead cancels the connection's `dropped` token: the send task stops writing at once (even mid-send to an unread socket), sends `Close 1013`, the read loop stops and unpins, and the client reconnects to be caught up. Frames seeded on attach (catch-up, views, turn replay) are queued outside the cap, so a reconnect is never dropped for its own replay. A dropped HTTP POST answers 503. The send task's cancel and close are now bounded too, so a teardown can no longer hang on a client that stopped reading. tests/serve_slow_client.rs turns the audit repro into permanent tests (both failed before); unit tests pin the cap, draining, and seed exemption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
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.
Fixes serve audit defect #2 (HANG: a connection dropped from the fanout stays open and silent forever).
Problem
OutFanout::broadcastdropped a sink on its first failedtry_send, but the attach loop'sreply_txclone kept the channel open. The socket stayed up, pings kept arriving, and the client's commands still ran, but it never got another frame. It never reconnected, its session counted as attached and was never reaped, and approvals waited on it forever.Design decision
Buffer by bytes, then drop, and a drop closes the socket.
ConnTx/ConnRxreplace the bounded mpsc. The cap is 8 MiB of live, unwritten output per connection (CONN_BACKLOG_MAX_BYTES), about 33× the measured burst. The bound exists to cap memory, so it is counted in bytes.droppedtoken:Close 1013 Try Again Later(bounded).Tests
tests/serve_slow_client.rs(the audit repro made permanent; both tests failed before this change):a_reading_client_gets_every_frame_of_a_burst: 3000 deltas, every byte arrives in order.a_client_that_stops_reading_is_disconnected_and_can_reattach: a 24 MiB turn with a paused reader; the socket closes, and the reconnect is caught up and answered.serve.rs: the cap boundary (dropped exactly past it, no holes, token fires), a draining connection is never dropped, and seeded frames are exempt.--all-targets -D warnings, the full agent suite and dprint check all pass. ARCHITECTURE.md has a new "Why a slow connection is closed" section.🤖 Generated with Claude Code
https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk