Conversation
Pure rename; no content change. Prepares for splitting the module into one file per layer. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
Pure move: every test body is byte-identical (review with `--color-moved`); section-banner comments are replaced by one file per topic, and the shared fixtures (`ECHO_REQ`, `slow_router`, `pki`, ...) live in `tests/mod.rs`. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
…r` forward to them The per-connection settings (HTTP/1.1 keep-alive, header read timeout, HTTP/2 windows/keepalive/max streams, max age/idle/requests and their shared grace) become one `#[non_exhaustive]` value with a `Default`, `ConnectionConfig`; TLS and the handshake timeout become `AcceptConfig`. `Server` and `BoundServer` each hold one of each, gain `with_connection_config` / `connection_config()` and `with_accept_config` / `accept_config()` so configuration crosses between them (#250), and every existing `with_*` setter keeps its signature as a one-line forward whose canonical documentation now lives on the config type. The accept loop takes the two values instead of seven positional knobs, which retires both `too_many_arguments` allows and the "grouping them into a struct would not improve clarity" note. The two once-per-server diagnostics (inert grace, inert keepalive timeout) move to `ConnectionConfig::lint`. Setter-semantics tests (adaptive-window precedence, zero rejection, defaults) move next to `ConnectionConfig`; the per-topic "builder threads through" tests collapse into one `builders.rs` test that checks every forward on both types and the crossing of one value between them. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
`Acceptor` owns `accept(2)`, transient-error retry (with a one-second pause on `EMFILE`/`ENFILE` instead of a hot loop) and `TCP_NODELAY`; `Accepted::handshake` (run on the connection's task) owns TLS termination within its timeout and the capture of the verified client chain, and yields an opaque `ServerIo` so the TLS stream type is not part of the API. `HandshakeError` names the peer (`peer_addr()`), says whether the timeout ran out (`is_timeout()`), and carries the rustls or timeout `io::Error` as its `source()`; the loop, not the acceptor, decides how loudly to log it. The acceptor's output is the new public `ConnectionInfo` (replacing the private `PeerInfo`): `peer_addr()` (an `Option<SocketAddr>`, matching `RequestContext::peer_addr()`), `peer_certs()`, and connection-scoped `extensions()` / `extensions_mut()` that every request on the connection will carry. The driver stamps requests from it; `PeerAddr` / `PeerCerts` are always set from what the transport observed, so nothing inserted into the extensions under those types reaches a request. It lives in `peer.rs` with `PeerAddr` / `PeerCerts`, the seam both the acceptor and the connection driver import, and is re-exported at the crate root. The accept loop is now `acceptor.accept()` + `accepted.handshake()` + serve, identical with and without TLS. CI checks and lints the `server`-only feature shape. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
`serve_connection(io, info, service, config, shutdown)` serves HTTP/1.1
or HTTP/2 on one already-accepted stream with the whole RPC lifecycle:
protocol settings from `ConnectionConfig`, max-age (jitter now sampled
per connection inside the driver) / idle / request-count retirement
with the shared grace, graceful GOAWAY when `shutdown` resolves, panic
isolation (a `CatchPanic` per connection instead of per server), and
`ConnectionInfo`'s extensions plus `PeerAddr` / `PeerCerts` on every
request. It is generic over any tower HTTP service so `axum::Router` can
ride the same driver, uses `serve_connection_with_upgrades` (identical
on the wire for services that never answer 101), installs the HTTP/2
timer unconditionally, returns `impl Future + Send + 'static` explicitly
(the contract custom loops spawn on), and binds nothing to a runtime
until first poll, so an accept loop places a connection by choosing
where to spawn the future. `Server::serve_connection` is the method
form that applies the server's service and config.
The lifecycle state machine is restructured, not moved verbatim: one
`grace` instead of a copy per trigger, states `Serving` / `Draining` /
`Retiring`, `conn` polled first in every state; the private
`RetirementConfig` family dissolves into reads of `ConnectionConfig`.
Its semantics are pinned by the unchanged end-to-end suites
(`tests/{shutdown,max_connection_age,max_connection_idle,max_requests}.rs`)
plus new direct layer-2 tests (`tests/connection.rs`: shutdown drains
in-flight, each trigger without a listener, built-ins stamped over
extensions, panic survival on keep-alive, runtime placement, a plain
`tower::service_fn`) and auto-trait assertions on the public types and
futures. The accept loop is now accept → handshake → `serve_connection`.
Conformance: server default 3600/3600, Connect+TLS 2396/2396.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7
Signed-off-by: Ryan Brewster <rpb@anthropic.com>
`accept_loop::run` is the whole of layer 4: accept, spawn handshake → `serve_connection` on the ambient runtime, track the tasks, fan the shutdown signal out, drain; it also owns how loudly a failed handshake is logged. It is generic over the service so the axum adapter can share it. `Server` and `BoundServer` move to `standalone.rs` as thin fronts for it (`Server` gains `service()` next to `router()`), and `server/mod.rs` is now only the module map and re-exports. A new test pins that dropping the serving future aborts live connections. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
…ion driver `connectrpc::axum::serve(listener, router)` (new, plaintext; `axum` + `server`) and `serve_tls` both return `Serve`, which is the same `accept_loop::run` over `Acceptor` + `serve_connection` that `Server` uses, with an `axum::Router` as the service. Parity with `Server` is now structural: `Serve::with_connection_config` takes the same `ConnectionConfig` (max age / idle / requests, HTTP/2 keepalive and windows), plaintext apps get `PeerAddr`, and there is no second lifecycle to keep in sync. `ServeTls` remains as a deprecated alias of `Serve`. Observable changes on the axum path, all inherited from the driver: a panicking handler yields a Connect `internal` 500 and the connection survives (previously the connection task died — every stream on an h2 connection with it); HTTP/2 adaptive flow control is on by default as on `Server`; connection tasks are owned by the `Serve` future and aborted when it is dropped instead of running detached. The module gate relaxes from `axum + server-tls` to `axum + server`. The mTLS test PKI is shared with the server tests instead of duplicated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
…lient identity A short loop over `Acceptor` + `Server::serve_connection`: one TLS listener, the workload identity parsed once per connection from the client certificate, connections without a known identity refused before any HTTP is spoken, `batch-*` workloads served on a separate `bulk` runtime and everyone else on the accepting one. Handlers read the identity from the request extensions and report the thread they ran on; `tests/e2e.rs` asserts the placement and the refusal. The README states what the loop inherits from the connection driver and the two lifetime rules it owns. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
`server/mod.rs` opens with the layer table (protocol service → connection driver → acceptor → loop) and what each owns; the crate docs gain a "Custom accept loops" entry; the guide's Hosting section is rebuilt around the layers, with `ConnectionConfig`, `connectrpc::axum::serve`, a custom loop trimmed from `examples/custom-accept-loop` and the two runtime-lifetime rules, and raw hyper demoted to "hyper knobs only, no lifecycle". Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
The hand-rolled `ConnectionLifecycle` poll state machine (with `IdleTracker` and `ConnectionLifecycleState`) becomes two `biased` `select!` phases inside `serve_connection`: serve until the peer closes, the shutdown future resolves, or `retirement()` names the first trigger to fire (max age, then `ConnectionActivity::quiet_for`, then the request-count latch); issue graceful shutdown; then either drain indefinitely (shutdown) or for at most the shared grace unless shutdown arrives meanwhile and lifts the cap. `biased` preserves the old poll order with the connection first; `latched` is a plain `async fn`. Same semantics, pinned by the unchanged shutdown / max-age / idle / max-requests / direct-driver suites (max-age suite run 20×, full lib suite 6×, no failures). This commit is separable: drop it to keep the state-machine form. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BgcfmMV1ECyzhbqGvUV1a7 Signed-off-by: Ryan Brewster <rpb@anthropic.com>
Server as their composition|
Closing: the parts of this with adopter demand landed as #306, #307, #308 and #304 (one What remains of the layering proposal is internal and behaviour-neutral: splitting |
This retitles #305. It is not required for any of the API in A–D (#306
ConnectionInfo+Server::serve_connection, #307ConnectionConfig, #308 freeserve_connection+axum::serve, #304with_connection_extensions) — those four land the whole adopter-facing surface on today'sserver.rs/axum.rs, each under the 250-line rule by net non-test lines (B's gross churn is ~2k because setter docs move; it is mechanical), with no file moves. What is left in this branch is internal structure, offered because it addresses #250 (items 2–3 and the Notes line) and #253 (theserver.rsrow) directly. If you want it, I will rebase it onto D and send it as 3–4 mostly-move PRs; if not, nothing in A–D depends on it and it can be closed.The branch as pushed (
rpb/server-layers, 10 commits onmain) predates A–D and still carries the earlier, larger public surface (publicAcceptorfamily,Server::service,accept_configaccessors). Read it for shape, not for API: rebased, every one of those becomespub(crate)and the public items are exactly A–D's.What A–D already bank (so this RFC does not claim them)
Server,BoundServer,axum::serve,axum::serve_tlsand custom loops (C), returningConnectionClosed(A).serve_tlscan no longer silently lack a setting.ConnectionConfigvalue instead of nine fields × two builders × four default blocks × positional plumbing (B): a new knob is field + default + setter + accessor + apply = 5 hunks, versus the measured 11 (HTTP/2 knob) to 22 (positional knob; the fourth default block really does failcargo check) on 0.9.0, and it cannot skipaxum.rsas 4 of the last 5 knob PRs did.What the restructure adds, measured on the branch against
main@ 758bd3aserver.rsserver/{mod 134, config 838, peer 259, acceptor 369, connection 621, accept_loop 125, standalone 784}.rs; largest non-test file 707accept_loop::run, 72 lines;Accepted::handshake, 28) — already 1 / 1 after C; what the branch adds is the loop and the accept+handshake step as their own small units (accept_loop.rs,acceptor.rs) instead of one 130-line functionserve_with_listener174,ConnectionLifecycle::poll115serve_connection126,accept_loop::run72Future: struct +IdleTracker+ 3-state enum +pollwith 10 poll sites, 210 linesbiasedselect!phases +retirement()+quiet_for, 99 lines (last commit, −177/+90, independently droppable)#[allow(clippy::too_many_arguments)]/ cfg-dependent aritymod tests(59 tests), 0 in-memory, 41 socketsserver/tests/{acceptor,builders,connection,header_read_timeout,http2,max_connection_age,max_connection_idle,max_requests,peer,shutdown}.rsby topic (#253's own axes) + unit tests beside the code; a direct driver suite of 8 tests overtokio::io::duplex(max-age / idle / request-count retirement, drain, panic isolation, extension stamping) with no listenerCosts, also measured: 9
pub(crate)seams between files that were one;cfg(feature = "server-tls")sites 37 → 41 spread over 5 files instead of 2; the layer table has to be kept true in module docs, crate docs and the guide; +4 CI steps (already in C).Proposed shape if accepted (rebased onto D)
server.rs→server/mod.rs; test module →server/tests/*.rsby topic. Pure moves, reviewable with--color-moved; closes Test modules run several times the size CONTRIBUTING allows, with the harness copied per test #253'sserver.rsrow on its own.Acceptor/Accepted::handshake(TCP accept, nodelay, transient-error retry with a 1 s back-off onEMFILE/ENFILE, TLS handshake + timeout + cert capture in one place) andaccept_loop::runin their own files;standalone.rsbecomes the two builders +run. No public items.pollstate machine as an async body. Droppable.examples/custom-accept-loop(admission and runtime placement by client-certificate identity over the publicserve_connection, handshake inline as in C's guide section, with an e2e test) and the module-doc layer table.Keeping
Acceptor/Accepted/ServerIo/HandshakeError/AcceptConfigcrate-private until someone asks is deliberate: promoting them later is ~24 visibility tokens and re-exports; retracting them would be a breaking release, and #250'sself-taking transition may want to reshapeAcceptConfigfirst.Evidence from the branch as it stands: per-commit fmt / clippy (all feature shapes) / rustdoc / MSRV green; 697 lib tests; conformance server mode 3600/3600 (all protocols) and 2396/2396 (Connect + TLS);
cargo semver-checksvs v0.9.0 clean three ways;echo_bench --quickwithin noise ofmain.🤖 Generated with Claude Code