feat(server): add with_max_connection_idle (idle connection reaping)#190
Draft
iainmcgin wants to merge 2 commits into
Draft
feat(server): add with_max_connection_idle (idle connection reaping)#190iainmcgin wants to merge 2 commits into
iainmcgin wants to merge 2 commits into
Conversation
Add `with_max_connection_idle(Duration)` to `Server` and `BoundServer`. When set, a connection that has had no in-flight requests for the configured duration is retired: the server sends an HTTP/2 GOAWAY (or disables HTTP/1.1 keep-alive) and then drains over the existing max-connection-age grace period before force-closing. This complements `with_max_connection_age` (#165): age caps a connection's total lifetime regardless of use, while idle reclaims connections that have gone quiet (clients behind NAT, bursty workloads, pooled clients holding connections they no longer need). Both knobs reuse the per-connection lifecycle state machine; when both are configured, whichever fires first retires the connection. Idle reaping is disabled by default. Implementation notes: - In-flight requests are counted at the hyper service_fn dispatch boundary via an RAII guard, so the count stays correct across handler panics and cancellation. An epoch counter (bumped on request start and completion) lets the lifecycle detect activity that began and ended within an idle window, so the timer resets on any activity. - The idle window is evaluated lazily (re-checked at timer expiry), so a connection is retired between one and two times the configured duration after its last activity. No jitter is applied. - Whole-server graceful shutdown is unchanged and is never capped by the idle grace period. In-flight accounting is only allocated when idle reaping is enabled, so there is no per-request overhead otherwise.
|
All contributors have signed the CLA ✍️ ✅ |
# Conflicts: # connectrpc/src/server.rs
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.
Problem
BoundServer/Servercan cap connection lifetime by age (#165) but cannot reclaim connections that have simply gone quiet. Idle connections (clients behind NAT, bursty workloads, pooled clients holding connections they no longer use) sit open indefinitely. This is the standard companion to max-connection-age found in grpc-go (MaxConnectionIdle), grpc-java (maxConnectionIdle), and connect-go (http2.Server.IdleTimeout). There is noaxum::serveescape hatch for it. Closes #177.Change
Adds
with_max_connection_idle(Duration)to bothServerandBoundServer. When set, a connection that has had zero in-flight requests for the configured duration is retired the same way max-age retires one: HTTP/2 connections get a GOAWAY, HTTP/1.1 connections have keep-alive disabled, then the connection drains over the existingwith_max_connection_age_graceperiod before force-closing.service_fndispatch boundary via an RAII guard, so the count survives handler panics and cancellation. An epoch counter (bumped on request start and completion) lets the lifecycle detect activity that began and ended inside an idle window, so the timer resets on any activity.AgeDraininglifecycle state was renamedDrainingsince it now serves both triggers.In-flight accounting is allocated only when idle reaping is enabled, so there is no per-request overhead otherwise.
Overlap with #135
#135 (read/idle timeouts) also touches idle handling in
connectrpc/src/server.rs. This PR is scoped narrowly to the dedicatedwith_max_connection_idlereaping knob from #177; the two will need reconciliation in whichever lands second.Validation
cargo test --workspace --all-features— 654 passed.cargo test -p connectrpc --features server max_connection_idle— 6 focused tests, run repeatedly for stability.cargo clippy --workspace --all-features --all-targets -- -D warnings— clean.cargo +nightly-2026-02-27 fmt --check,cargo docwith-Dwarnings, and minimal-features check all pass.Tests cover: a quiet connection reaped (GOAWAY asserted) after the idle window plus reset-on-activity, an in-flight request keeping the connection alive across multiple idle windows then reaped after completion, idle firing before a longer max age, the activity counter/guard accounting, builder defaults/threading, and the zero-duration panic.
Reviewer note
A deterministic
start_pausedintegration test for "many short requests keep the connection alive" is not feasible: the paused clock auto-advances through idle windows whenever the test parks between requests, andGracefulConnectionis sealed so the lifecycle cannot be driven with a mock. The in-flight-request test covers "activity prevents reaping" without timing fragility, andreaps_quiet_connectioncovers the reset-on-activity branch.