Skip to main content

sozu_lib/protocol/mux/
mod.rs

1//! HTTP/1.1 and HTTP/2 multiplexing layer.
2//!
3//! This module unifies HTTP/1.1 and HTTP/2 behind a single [`Mux`] session
4//! state machine that integrates with sozu's mio event loop. The key types:
5//!
6//! - [`Mux`]: The top-level session state, generic over socket (`TcpStream` or
7//!   `FrontRustls`) and listener. Implements `SessionState`.
8//! - [`Connection`]: Enum dispatching to [`ConnectionH1`] or [`ConnectionH2`]
9//!   for protocol-specific readable/writable logic.
10//! - [`Stream`]: Per-request state with front/back kawa buffers, metrics, and
11//!   lifecycle tracking. Shared between H1 and H2 paths.
12//! - [`Context`]: Per-session context (cluster, backends, routing, timeouts).
13//!
14//! The H2 implementation handles RFC 9113 framing, HPACK (RFC 7541), flow
15//! control, flood detection (CVE-2023-44487, CVE-2019-9512/9514/9515/9518,
16//! CVE-2024-27316), and graceful shutdown (double-GOAWAY per RFC 9113 §6.8).
17
18use std::{
19    cell::RefCell,
20    collections::{BTreeMap, VecDeque},
21    fmt::Debug,
22    io::ErrorKind,
23    net::{Shutdown, SocketAddr},
24    rc::{Rc, Weak},
25    sync::Arc,
26    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
27};
28
29use mio::{Interest, Token, net::TcpStream};
30use rand::{SeedableRng, rngs::StdRng};
31use rusty_ulid::Ulid;
32use sozu_command::{
33    logging::ansi_palette,
34    proto::command::{Event, EventKind},
35    ready::Ready,
36};
37
38/// Protocol label + session descriptor used as a prefix on every [`Mux`] log
39/// line. Matches the RUSTLS log-context convention:
40/// `[<ulid> - - -]\tMUX\tSession(...)\t >>>`. When colored output is enabled
41/// (via [`ansi_palette`]) the label is wrapped in bold bright-white ANSI
42/// (uniform across every protocol) and the session detail block is rendered
43/// in light grey.
44///
45/// Fields included in the session block:
46/// - `frontend` — mio token of the frontend socket
47/// - `peer` — [`Connection::peer_address`]: a snapshot, not a live lookup
48/// - `streams` — number of streams currently held by the [`Context`]
49/// - `backends` — number of backend connections in the [`Router`]
50/// - `pending_links` — streams waiting to be linked to a backend
51/// - `readiness` — frontend mio readiness snapshot
52macro_rules! log_context {
53    ($self:expr) => {{
54        let (open, reset, grey, gray, white) = ansi_palette();
55        format!(
56            "[{ulid} - - -]\t{open}MUX{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend}{reset}, {gray}peer{reset}={white}{peer:?}{reset}, {gray}streams{reset}={white}{streams}{reset}, {gray}backends{reset}={white}{backends}{reset}, {gray}pending_links{reset}={white}{pending_links}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
57            open = open,
58            reset = reset,
59            grey = grey,
60            gray = gray,
61            white = white,
62            ulid = $self.session_ulid,
63            frontend = $self.frontend_token.0,
64            peer = $self.frontend.peer_address(),
65            streams = $self.context.streams.len(),
66            backends = $self.router.backends.len(),
67            pending_links = $self.context.pending_links.len(),
68            readiness = $self.frontend.readiness(),
69        )
70    }};
71}
72
73/// Lighter variant of [`log_context!`] that omits the
74/// `streams`/`backends`/`pending_links` counts. Used at sites where the
75/// borrow checker forbids reading `self.router.backends` or
76/// `self.context.streams` (e.g. inside a method that already holds a mutable
77/// borrow on one of them). The ULID and frontend snapshot still carry enough
78/// context to correlate the line back to the rest of the session.
79macro_rules! log_context_lite {
80    ($self:expr) => {{
81        let (open, reset, grey, gray, white) = ansi_palette();
82        format!(
83            "[{ulid} - - -]\t{open}MUX{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend}{reset}, {gray}peer{reset}={white}{peer:?}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
84            open = open,
85            reset = reset,
86            grey = grey,
87            gray = gray,
88            white = white,
89            ulid = $self.session_ulid,
90            frontend = $self.frontend_token.0,
91            peer = $self.frontend.peer_address(),
92            readiness = $self.frontend.readiness(),
93        )
94    }};
95}
96
97/// Module-level prefix for logs emitted from free functions or routing
98/// blocks where no [`Mux`] is in scope. Honours the colored flag.
99///
100/// Two arms:
101/// * `log_module_context!()` — zero-arg, legacy `MUX\t >>>` output. Kept
102///   for sites without an `HttpContext` in scope (e.g. the generic
103///   `trace!` that fires before the variant-specific match).
104/// * `log_module_context!($http_context)` — rich form. `$http_context`
105///   must be `&HttpContext`. Produces the same
106///   `[session req cluster backend]` bracket as RUSTLS/PIPE/TCP followed
107///   by a `Session(...)` block, so MUX lines emitted from variant match
108///   arms stay filterable by session ULID or request ULID. Mirrors
109///   `router.rs:log_module_context!($http_context)` (see there). Custom
110///   methods render only their byte length, and authority renders only its
111///   byte length.
112macro_rules! log_module_context {
113    () => {{
114        let (open, reset, _, _, _) = ansi_palette();
115        format!("{open}MUX{reset}\t >>>", open = open, reset = reset)
116    }};
117    ($http_context:expr) => {{
118        let (open, reset, grey, gray, white) = ansi_palette();
119        let http_ctx: &HttpContext = &$http_context;
120        let ctx = http_ctx.log_context();
121        format!(
122            "{gray}{ctx}{reset}\t{open}MUX{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend:?}{reset}, {gray}method{reset}={white}{method:?}{reset}, {gray}authority_bytes{reset}={white}{authority_bytes:?}{reset})\t >>>",
123            open = open,
124            reset = reset,
125            grey = grey,
126            gray = gray,
127            white = white,
128            ctx = ctx,
129            frontend = http_ctx.session_address,
130            method = http_ctx.method,
131            authority_bytes = http_ctx.authority.as_ref().map(String::len),
132        )
133    }};
134}
135
136use self::h2::MetricEvent;
137
138pub mod answers;
139pub mod auth;
140pub mod buffer_source;
141pub mod connection;
142mod converter;
143pub mod debug;
144mod h1;
145mod h2;
146mod h2_close;
147mod h2_control_tx;
148mod h2_drain;
149mod h2_flood_detector;
150mod h2_flow_control;
151mod h2_header_reassembly;
152mod h2_output;
153mod h2_scheduler;
154mod h2_stream_table;
155pub mod h2_transmit;
156mod h2_write_pass;
157pub mod hpack;
158mod hpack_state;
159pub mod parser;
160mod pkawa;
161pub mod router;
162pub(crate) mod serializer;
163mod shared;
164pub mod stream;
165
166use crate::metrics::names;
167use crate::{
168    BackendConnectionError, FrontendFromRequestError, L7ListenerHandler, L7Proxy, ListenerHandler,
169    Protocol, ProxySession, Readiness, RetrieveClusterError, SessionIsToBeClosed, SessionMetrics,
170    SessionResult, StateResult,
171    backends::{Backend, BackendError, BackendMap},
172    http::HttpListener,
173    https::HttpsListener,
174    pool::{Checkout, Pool},
175    protocol::{SessionState, http::editor::HttpContext},
176    retry::RetryPolicy,
177    server::push_event,
178    socket::{FrontRustls, SessionTcpStream, SocketHandler, SocketResult, stats::socket_rtt},
179    timer::TimeoutContainer,
180};
181
182pub(crate) use crate::protocol::mux::answers::{
183    forcefully_terminate_answer, set_default_answer, set_default_answer_with_retry_after,
184};
185use crate::protocol::mux::connection::{EndpointClient, EndpointServer};
186pub use crate::protocol::mux::{
187    answers::terminate_default_answer,
188    buffer_source::{BufferSource, PoolBufferSource},
189    connection::Connection,
190    debug::{DebugEvent, DebugHistory},
191    h1::ConnectionH1,
192    h2::CLIENT_PREFACE_SIZE,
193    h2::ConnectionH2,
194    h2::H2ByteAccounting,
195    h2::H2ConnectionConfig,
196    h2::{
197        H2ControlFlushTarget, H2FinalizeTarget, H2ForceDisconnectTarget, H2ReadOutcome,
198        H2ReadTarget, H2Shell, H2StreamId, H2WritableStateTarget, H2WriteTarget, H2Written,
199    },
200    h2_flood_detector::H2FloodConfig,
201    h2_write_pass::H2WritePass,
202    parser::H2Error,
203    router::Router,
204    stream::{Stream, StreamParts, StreamState},
205};
206
207// ── Tuning Constants ─────────────────────────────────────────────────────────
208
209/// Maximum inner-loop iterations of one `Mux::ready_inner` call, a safety
210/// bound that keeps a spinning session from holding the single-threaded
211/// worker. Reaching it is a bug: it increments `http.infinite_loop.error` and
212/// logs a warning. A session with output still queued for the client then
213/// waits for its next writable event; any other session is closed.
214const MAX_LOOP_ITERATIONS: i32 = 10_000;
215// ─────────────────────────────────────────────────────────────────────────────
216
217/// Debug tripwire for the one-active-stream invariant that
218/// `ConnectionH1::end_stream`'s early-return guard rests on.
219///
220/// `Mux::timeout` retires streams through `Connection::end_stream`, and its
221/// proof that no linked stream survives (LIFECYCLE invariant 22) depends on
222/// that call actually unlinking. `ConnectionH2::end_stream` always does.
223/// `ConnectionH1::end_stream` does NOT: it guards `self.stream != Some(stream)`
224/// and returns early, skipping the `Context::unlink_stream`. The guard is safe
225/// only because an H1 connection carries at most one linked stream and it
226/// equals `self.stream` — `self.stream = Some(..)` occurs only in
227/// `ConnectionH1::start_stream`, paired with `Context::link_stream`, and
228/// `self.stream = None` only inside `end_stream` itself, after the unlink.
229///
230/// This is the tripwire for that invariant: a drift between
231/// `context.backend_streams` (or a stream's `StreamState::Linked`) and
232/// `ConnectionH1::stream` fires here, in every debug / test / e2e build,
233/// instead of silently leaving a stream in the reverse index with no backend
234/// timer.
235///
236/// To SEE THIS FIRE: in
237/// `a_backend_timeout_leaves_no_linked_stream_behind_on_the_close_path`, set
238/// `h1.stream = Some(1)` instead of `Some(0)` while still linking stream 0 to
239/// the backend.
240///
241/// Deliberately at the CALL SITE rather than inside `ConnectionH1::end_stream`,
242/// and the reason is scope, not risk. That function has around a dozen callers
243/// reaching it through `Connection::end_stream` and the two `Endpoint`
244/// adaptors, and the H2-driven ones are precisely the ones this proof never
245/// traced: `H2Shell::close` and `ConnectionH2::end_stream` iterate their
246/// OWN wire map (`for global_stream_id in self.streams.values()`, `h2.rs`) and
247/// hand each `StreamState::Linked` gid to the peer, which on an H2-frontend /
248/// H1-backend session is a `ConnectionH1`. Add the reset and HUP paths and the
249/// set is wider than the claim.
250///
251/// Asserting inside the function would therefore assert something broader than
252/// what was traced. Note what does NOT justify the placement: running the full
253/// e2e suite with a `debug_assert_eq!` in that mismatch arm passes with zero
254/// hits, and green e2e shows absence of COVERAGE, not absence of the path. The
255/// claim is about the two `Mux::timeout` loops, so the assertion is too.
256#[cfg(debug_assertions)]
257fn debug_assert_h1_owns_stream<Front: SocketHandler>(
258    connection: &Connection<Front>,
259    stream_id: GlobalStreamId,
260) {
261    if let Connection::H1(h1) = connection {
262        debug_assert_eq!(
263            h1.stream,
264            Some(stream_id),
265            "ConnectionH1::end_stream would skip its unlink: the connection's \
266             active stream is {:?}, not the {stream_id} Mux::timeout is ending \
267             — the backend index and ConnectionH1::stream have drifted",
268            h1.stream,
269        );
270    }
271}
272
273/// Generic Http representation using the Kawa crate using the Checkout of Sozu as buffer
274type GenericHttpStream = kawa::Kawa<Checkout>;
275type StreamId = u32;
276type GlobalStreamId = usize;
277pub type MuxClear = Mux<SessionTcpStream, HttpListener>;
278pub type MuxTls = Mux<FrontRustls, HttpsListener>;
279
280/// An opaque, session-scoped name for one entry of the worker's backend
281/// registry.
282///
283/// It is an index into `Mux::backend_registry` and nothing else: the core
284/// can carry it, compare it and hand it back, and cannot turn it into a
285/// `Backend`. Only the embedder resolves it, which is what keeps
286/// `Rc<RefCell<Backend>>` out of the core (#1340, Question 12).
287///
288/// Session-scoped rather than derived from `(cluster_id, backend_id)` on
289/// purpose. A reload that replaces a registry entry mid-session builds a NEW
290/// `Rc`, which takes a NEW slot, so connections opened before it keep charging
291/// the handle they were dialled against — exactly what holding the `Rc`
292/// inside `Position::Client` used to do. Keying on the backend's name instead
293/// would split a balanced start/end pair across two `Backend` values: the
294/// superseded one would keep a charge forever and the fresh one would
295/// saturate at zero.
296#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
297pub struct BackendSlot(usize);
298
299/// A backend as the core sees it: the opaque slot, plus the two identity
300/// fields the datapath renders.
301///
302/// `backend_id` and `address` are copied once, when the session first interns
303/// the backend in its `BackendRegistry`, and every later dial of it clones
304/// them from there. Both are immutable for the life of a registry entry, so
305/// carrying them is not a cached view of mutable state — no load state
306/// crosses this boundary.
307/// The mutable half (`active_connections`, `active_requests`, `failures`,
308/// `connection_time`, `health`, `status`) stays behind the slot, and the core
309/// reaches it only by emitting a [`BackendDelta`].
310///
311/// `backend_id` is an `Rc<str>` so that no step past interning copies it:
312/// rebuilding `Position::Client` on `Connecting` -> `Connected` once per dial,
313/// minting the id for a redial (the registry keeps the `Rc<str>` it built at
314/// interning, #1564), and stamping a request with the backend it runs on,
315/// dialled or reused, into `HttpContext::backend_id` and
316/// `SessionMetrics::backend_id` (#1579) are each a reference-count increment.
317/// A `String` would make every one of them a heap copy.
318#[derive(Clone, Debug, PartialEq, Eq)]
319pub struct BackendId {
320    /// Private to this module: [`Mux::backend`] is the only way out.
321    slot: BackendSlot,
322    /// `Backend::backend_id`, for log lines and per-backend metric labels.
323    pub backend_id: Rc<str>,
324    /// `Backend::address`, for log lines and `push_event` payloads.
325    pub address: SocketAddr,
326    /// Metrics lifetime captured by the request that opened this connection.
327    /// It stays with the connection through draining so its close cannot
328    /// mutate a replacement cluster with the same labels.
329    metric_incarnation: crate::metrics::ClusterMetricsCapture,
330}
331
332impl BackendId {
333    /// An id for the backend at `address`, named `backend_id`, in `slot` of
334    /// whichever registry mints it (#1684).
335    ///
336    /// The worker's mux never calls this: its ids come from the session's
337    /// `BackendRegistry`, which interns the backend it names and is the only
338    /// thing that can resolve a slot back to it. This is for an embedder that
339    /// owns its own selector — a simulator implementing
340    /// [`router::BackendSelector`] — and numbers its backends itself. A slot
341    /// means something only to the registry that issued it.
342    pub fn new(slot: usize, backend_id: Rc<str>, address: SocketAddr) -> Self {
343        BackendId {
344            slot: BackendSlot(slot),
345            backend_id,
346            address,
347            metric_incarnation: Default::default(),
348        }
349    }
350
351    pub(crate) fn metric_incarnation(&self) -> crate::metrics::ClusterMetricsCapture {
352        self.metric_incarnation
353    }
354
355    fn set_metric_incarnation(&mut self, incarnation: crate::metrics::ClusterMetricsCapture) {
356        self.metric_incarnation = incarnation;
357    }
358
359    /// The slot this id names. Module-private on purpose — see [`Mux::backend`]
360    /// for the crate-visible resolution path.
361    fn slot(&self) -> BackendSlot {
362        self.slot
363    }
364}
365
366/// One accounting change the core decided and the embedder performs.
367///
368/// LIFECYCLE §9 invariant 14 is a balance, and this is the ledger it balances:
369/// every [`BackendChange::StreamsStarted`] unit the mux emits is covered by
370/// exactly one [`BackendChange::StreamsEnded`] unit against the same slot.
371/// Because every write to `Backend::active_requests` and
372/// `Backend::active_connections` on the mux path now goes through
373/// `Mux::apply_backend_deltas`, reconciling the emitted stream to zero is a
374/// direct test of the invariant rather than a proxy for it.
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub struct BackendDelta {
377    /// The registry entry to charge.
378    pub slot: BackendSlot,
379    /// What to charge it.
380    pub change: BackendChange,
381}
382
383/// The accounting operations [`BackendDelta`] can carry.
384///
385/// The two counters invariant 14 is about, plus the release of a
386/// reservation whose dial never started. The other health and latency writes
387/// the mux makes (`failures` and `retry_policy` on an asynchronous connect
388/// failure, `connection_time`) are NOT here: they stay immediate at their
389/// existing embedder-side call sites rather than being deferred to a drain.
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub enum BackendChange {
392    /// `Backend::active_requests += n` — `n` streams started on this backend.
393    StreamsStarted(usize),
394    /// `Backend::active_requests -= n`, saturating — `n` streams left it.
395    StreamsEnded(usize),
396    /// `Backend::dec_connections()` — the connection to this backend closed,
397    /// or a dial that connected was abandoned before its connection was
398    /// registered (`Mux::dial_backend`, #1713): either way the reservation
399    /// selection took is released, and no failure is recorded.
400    ConnectionClosed,
401    /// The `connect(2)` of a dial failed at this instant (#1684): release the
402    /// reservation selection took (`Backend::dec_connections()`), and record
403    /// the failure (`failures += 1`, `retry_policy.fail`), as
404    /// `Backend::release_failed_dial` does. The drain before the next
405    /// selection applies it, so that selection sees the backend exactly as a
406    /// failed `Backend::try_connect` left it.
407    DialFailed(Instant),
408}
409
410/// The embedder's table of registry handles, indexed by [`BackendSlot`].
411///
412/// A newtype rather than a bare `Vec` so the two operations that may cross
413/// Question 12's perimeter — minting a slot for a freshly dialled backend,
414/// and performing a [`BackendDelta`] against one — are the only ones there
415/// are. Nothing hands a `&Rc<RefCell<Backend>>` out of here except
416/// [`Mux::backend`], whose one caller is the WebSocket upgrade.
417#[derive(Debug, Default)]
418pub(crate) struct BackendRegistry(Vec<RegistryEntry>);
419
420/// One slot of the [`BackendRegistry`]: the handle, plus the [`BackendId`]
421/// identity fields read from it once, when it was interned.
422///
423/// The cached `backend_id` is what keeps `BackendRegistry::id_for`
424/// allocation-free on every dial after a backend's first (#1564). It cannot go
425/// stale: `Backend::backend_id` and `Backend::address` are never written after
426/// construction, and a reload that replaces the backend builds a new `Rc`,
427/// which takes a new slot with its own entry.
428#[derive(Debug)]
429struct RegistryEntry {
430    handle: Rc<RefCell<Backend>>,
431    backend_id: Rc<str>,
432    address: SocketAddr,
433}
434
435impl BackendRegistry {
436    /// Name `backend` with a slot, reusing the one it already has.
437    ///
438    /// Identity is `Rc::ptr_eq`, not the backend's id or address: two dials
439    /// separated by a reload that replaced the registry entry are two
440    /// different `Backend` values, and each must keep its own accounting.
441    /// The scan is linear over the backends ONE session has dialled, which
442    /// is the cluster's backend count at worst — the same order as the
443    /// `Router::backends` walk the caller has just done.
444    ///
445    /// A new slot copies the backend's id into an `Rc<str>` here, once per
446    /// session and backend; that copy and the table's own growth are the only
447    /// allocations this path makes.
448    fn intern(&mut self, backend: &Rc<RefCell<Backend>>) -> BackendSlot {
449        match self
450            .0
451            .iter()
452            .position(|known| Rc::ptr_eq(&known.handle, backend))
453        {
454            Some(slot) => BackendSlot(slot),
455            None => {
456                let (backend_id, address) = {
457                    let borrow = backend.borrow();
458                    (Rc::from(borrow.backend_id.as_str()), borrow.address)
459                };
460                // A session almost always dials one backend: its first entry
461                // sizes the table for exactly one instead of four (#1610),
462                // and a second distinct backend grows it as usual.
463                if self.0.capacity() == 0 {
464                    self.0.reserve_exact(1);
465                }
466                self.0.push(RegistryEntry {
467                    handle: backend.clone(),
468                    backend_id,
469                    address,
470                });
471                BackendSlot(self.0.len() - 1)
472            }
473        }
474    }
475
476    /// Build the core's view of a backend it is about to be handed.
477    ///
478    /// Runs once per backend dial. Past a backend's first dial in this
479    /// session it clones the id cached in its slot and allocates nothing.
480    fn id_for(&mut self, backend: &Rc<RefCell<Backend>>) -> BackendId {
481        let slot = self.intern(backend);
482        let entry = &self.0[slot.0];
483        BackendId {
484            slot,
485            backend_id: entry.backend_id.clone(),
486            address: entry.address,
487            metric_incarnation: Default::default(),
488        }
489    }
490
491    fn get(&self, slot: BackendSlot) -> Option<&Rc<RefCell<Backend>>> {
492        self.0.get(slot.0).map(|entry| &entry.handle)
493    }
494
495    /// Resolve one of the core's opaque [`BackendId`]s to the registry handle
496    /// it names.
497    ///
498    /// The single crate-visible way back from an id to an
499    /// `Rc<RefCell<Backend>>`, so the perimeter Question 12 draws has one
500    /// door rather than a `pub` slot every caller can walk through. Its only
501    /// callers are `upgrade_mux` in `lib/src/http.rs` and `lib/src/https.rs`,
502    /// which hand the live handle to the WebSocket `Pipe` that takes the
503    /// backend over.
504    ///
505    /// It sits here rather than on [`Mux`] because those callers have already
506    /// moved `Mux::frontend` out by the time they need it; a field access
507    /// borrows disjointly where a `&self` method on `Mux` could not.
508    ///
509    /// `None` cannot happen for an id this session minted — the table never
510    /// shrinks — so a caller treats it as the desync it would be.
511    pub(crate) fn handle(&self, backend: &BackendId) -> Option<&Rc<RefCell<Backend>>> {
512        self.get(backend.slot())
513    }
514
515    /// Whether the backend `backend` names has left the configuration.
516    ///
517    /// `BackendList::remove_backend` and `BackendMap::remove_cluster`
518    /// (`lib/src/backends.rs`) mark a backend `Closing` as they drop it, and
519    /// the registry still holds that very `Rc`. A backend added again later,
520    /// even at the same address, is a new `Rc` in a new slot, so a
521    /// connection dialled to the old one stays retired. Reads one `RefCell`
522    /// borrow and allocates nothing.
523    pub(crate) fn is_retired(&self, backend: &BackendId) -> bool {
524        self.get(backend.slot())
525            .is_some_and(|handle| handle.borrow().status != crate::backends::BackendStatus::Normal)
526    }
527
528    /// Perform every accounting change in `deltas`, in the order they were
529    /// pushed, and leave the ledger empty.
530    ///
531    /// The one drain both `Mux::apply_backend_deltas` and the pre-plan settle
532    /// in `Mux::ready_inner` go through.
533    ///
534    /// Drained in place rather than taken, so the ledger keeps its capacity
535    /// for the next change: the charge `Connection::start_stream` records on
536    /// every request would otherwise reallocate it each time (#1583). The
537    /// capacity is bounded by the most deltas a single pass has pushed.
538    ///
539    /// `backends` is the worker's backend map, borrowed only to draw the
540    /// backoff jitter of a [`BackendChange::DialFailed`] from its generator;
541    /// every other change leaves it untouched.
542    pub(crate) fn apply_all(&self, deltas: &mut Vec<BackendDelta>, backends: &RefCell<BackendMap>) {
543        for delta in deltas.drain(..) {
544            self.apply(delta, backends);
545        }
546    }
547
548    /// Perform one accounting change the core decided.
549    ///
550    /// The before/after pair-assertions that used to sit at each emitting
551    /// site live here now: this is where both halves are observable, and one
552    /// copy covers every emitter instead of each carrying its own.
553    fn apply(&self, delta: BackendDelta, backends: &RefCell<BackendMap>) {
554        let Some(backend) = self.get(delta.slot) else {
555            // Unreachable while the vector only grows, which it does. Report
556            // rather than panic: losing one charge is a drifting gauge, and
557            // dropping a session over it would be worse.
558            error!(
559                "{} backend accounting delta {:?} names a slot this session never minted",
560                log_module_context!(),
561                delta
562            );
563            return;
564        };
565        let mut backend = backend.borrow_mut();
566        match delta.change {
567            BackendChange::StreamsStarted(count) => {
568                let before = backend.active_requests;
569                backend.active_requests += count;
570                debug_assert_eq!(
571                    backend.active_requests,
572                    before + count,
573                    "a StreamsStarted delta must raise active_requests by exactly its count"
574                );
575            }
576            BackendChange::StreamsEnded(count) => {
577                // `saturating_sub` is the network-safe floor a desynced peer
578                // needs, so the post-relation is all that can be asserted —
579                // not that `before >= count`.
580                let before = backend.active_requests;
581                backend.active_requests = backend.active_requests.saturating_sub(count);
582                debug_assert_eq!(
583                    backend.active_requests,
584                    before.saturating_sub(count),
585                    "a StreamsEnded delta must lower active_requests by its count (saturating)"
586                );
587                debug_assert!(
588                    backend.active_requests <= before,
589                    "active_requests must not grow on a StreamsEnded delta"
590                );
591            }
592            BackendChange::ConnectionClosed => {
593                // `dec_connections` floors at 0 (a double close from a
594                // desynced peer must not panic), so the post-relation is
595                // "decreased by one, unless already at zero".
596                let before = backend.active_connections;
597                backend.dec_connections();
598                debug_assert_eq!(
599                    backend.active_connections,
600                    before.saturating_sub(1),
601                    "a ConnectionClosed delta must release exactly one backend connection \
602                     (saturating at 0)"
603                );
604            }
605            BackendChange::DialFailed(at) => {
606                // The one change that draws randomness, so the only one that
607                // borrows the map. `release_failed_dial` carries the
608                // before/after assertions for both halves.
609                backend.release_failed_dial(at, backends.borrow_mut().rng());
610            }
611        }
612        trace!(
613            "{} backend accounting {:?} applied: {:#?}",
614            log_module_context!(),
615            delta.change,
616            backend
617        );
618    }
619}
620
621pub enum Position {
622    Client(sozu_command::state::ClusterId, BackendId, BackendStatus),
623    Server,
624}
625
626impl Debug for Position {
627    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
628        match self {
629            Self::Client(cluster_id, _, status) => f
630                .debug_tuple("Client")
631                .field(cluster_id)
632                .field(status)
633                .finish(),
634            Self::Server => write!(f, "Server"),
635        }
636    }
637}
638
639impl Position {
640    fn is_server(&self) -> bool {
641        match self {
642            Position::Client(..) => false,
643            Position::Server => true,
644        }
645    }
646    fn is_client(&self) -> bool {
647        !self.is_server()
648    }
649
650    /// The bytes-read event for this side.
651    ///
652    /// Returns exactly one event — never zero, never two — because both arms
653    /// of the match below emit one, so the caller needs no container to carry
654    /// it. A caller that is its own shell records it at once; a caller inside
655    /// the H2 core queues it. `#[must_use]` is what makes that a compiler
656    /// obligation rather than a convention: this leaf is reached from a core
657    /// AND from shells, and an event silently dropped here is an upward
658    /// drift that never underflows, so nothing would log and nothing would
659    /// saturate.
660    #[must_use]
661    fn bytes_in_event(&self, size: usize) -> MetricEvent {
662        match self {
663            Position::Client(..) => MetricEvent::BackendBytesIn(size as i64),
664            Position::Server => MetricEvent::FrontendBytesIn(size as i64),
665        }
666    }
667
668    /// The bytes-written event for this side. Same one-event guarantee and
669    /// same reason for `#[must_use]` as [`Self::bytes_in_event`].
670    #[must_use]
671    fn bytes_out_event(&self, size: usize) -> MetricEvent {
672        match self {
673            Position::Client(..) => MetricEvent::BackendBytesOut(size as i64),
674            Position::Server => MetricEvent::FrontendBytesOut(size as i64),
675        }
676    }
677
678    /// Attribute `size` bytes read to the appropriate `SessionMetrics` field.
679    pub fn count_bytes_in(&self, metrics: &mut SessionMetrics, size: usize) {
680        match self {
681            Position::Client(..) => metrics.backend_bin += size,
682            Position::Server => metrics.bin += size,
683        }
684    }
685
686    /// Attribute `size` bytes written to the appropriate `SessionMetrics` field.
687    pub fn count_bytes_out(&self, metrics: &mut SessionMetrics, size: usize) {
688        match self {
689            Position::Client(..) => metrics.backend_bout += size,
690            Position::Server => metrics.bout += size,
691        }
692    }
693}
694
695#[derive(Debug)]
696pub enum BackendStatus {
697    Connecting(Instant),
698    Connected,
699    KeepAlive,
700    Disconnecting,
701}
702
703#[derive(Debug, Clone, Copy)]
704pub enum MuxResult {
705    Continue,
706    Upgrade,
707    CloseSession,
708}
709
710pub trait Endpoint: Debug {
711    fn readiness(&self, token: Token) -> &Readiness;
712    fn readiness_mut(&mut self, token: Token) -> &mut Readiness;
713    /// Smoothed round-trip time of the peer side of a stream, already
714    /// sampled.
715    ///
716    /// Used by access-log emission to report TCP_INFO RTT for the side the
717    /// caller does NOT own directly: a frontend connection (Position::Server)
718    /// reports the backend's RTT through this method, and a backend connection
719    /// (Position::Client) reports the frontend's the same way. `token` is
720    /// ignored by `connection::EndpointServer` (which has a single
721    /// frontend connection) and used as a key by
722    /// `connection::EndpointClient` (which keys backends by token).
723    /// Returns `None` when the token doesn't resolve, mirroring the existing
724    /// fallback paths in `readiness`/`readiness_mut`, and also when the
725    /// platform declines to answer. The value is the peer connection's one
726    /// sample, taken by `memoized_rtt` the first time it is asked for.
727    ///
728    /// This returns the VALUE, not the socket it came from. The predecessor,
729    /// `fn socket(&self, token) -> Option<&TcpStream>`, handed out a
730    /// `mio::net::TcpStream` — a concrete OS type — so any connection could
731    /// reach any other connection's socket for any purpose, and no in-memory
732    /// transport could ever satisfy the trait. RTT is intrinsically a live
733    /// socket property and stays on the embedder's side of the boundary; the
734    /// cores receive an `Option<Duration>` that was captured for them.
735    fn peer_rtt(&self, token: Token) -> Option<Duration>;
736    /// Smoothed round-trip time of the connection this endpoint was handed
737    /// TO — the local side, where [`Self::peer_rtt`] reports the other one.
738    ///
739    /// Asked by `ConnectionH2::snapshot_rtts` for the access log's
740    /// `client_rtt` cell, at the moment a stream is logged and at no other
741    /// time, so a pass that logs nothing costs no `getsockopt(TCP_INFO)`.
742    /// Neither `EndpointClient` nor `EndpointServer` owns the local socket,
743    /// so both answer `None`: the value comes from `h2::ShellEndpoint`, the
744    /// wrapper `H2Shell` puts around them, which samples its own socket once
745    /// per connection through `memoized_rtt`. The core still never reaches
746    /// a socket.
747    fn local_rtt(&self) -> Option<Duration>;
748    /// If end_stream is called on a client it means the stream has PROPERLY finished,
749    /// the server has completed serving the response and informs the endpoint that this stream won't be used anymore.
750    /// If end_stream is called on a server it means the stream was BROKEN, the client was most likely disconnected or encountered an error
751    /// it is for the server to decide if the stream can be retried or an error should be sent. It should be GUARANTEED that all bytes from
752    /// the backend were read. However it is almost certain that all bytes were not already sent to the client.
753    fn end_stream<L: ListenerHandler + L7ListenerHandler>(
754        &mut self,
755        token: Token,
756        stream: GlobalStreamId,
757        context: &mut Context<L>,
758    );
759    /// If start_stream is called on a client it means the stream should be attached to this endpoint,
760    /// the stream might be recovering from a disconnection, in any case at this point its response MUST be empty.
761    /// If the start_stream is called on a H2 server it means the stream is a server push and its request MUST be empty.
762    /// Returns false if the stream could not be started (e.g. max concurrent streams reached).
763    fn start_stream<L: ListenerHandler + L7ListenerHandler>(
764        &mut self,
765        token: Token,
766        stream: GlobalStreamId,
767        context: &mut Context<L>,
768    ) -> bool;
769}
770
771/// Every `getsockopt(TCP_INFO)` the mux issues for an access log goes through
772/// here, and only through [`memoized_rtt`]: once per connection, frontend and
773/// backend alike.
774///
775/// One function so the tests can count the reads: under `cfg(test)` each call
776/// bumps a thread-local counter, read back by [`tcp_info_reads`]. The
777/// production body is the bare `socket_rtt` call.
778pub(super) fn sample_rtt<A: std::os::fd::AsRawFd>(socket: &A) -> Option<Duration> {
779    #[cfg(test)]
780    TCP_INFO_READS.with(|reads| reads.set(reads.get() + 1));
781    socket_rtt(socket)
782}
783
784/// The smoothed RTT of `socket`, read at most once for the connection that
785/// owns `memo` and reused by every later access log of that connection.
786///
787/// `memo` is `None` until the connection's first access log asks for its RTT
788/// — the first request completed on a frontend, the first response logged
789/// through a backend — and holds that one sample, `Some(None)` included when
790/// the platform declined, for the rest of the connection's life. Nothing
791/// resets it: a keep-alive frontend serving twenty requests, or a backend
792/// connection taken back out of the keep-alive pool, costs one
793/// `getsockopt(TCP_INFO)`, not one per request.
794///
795/// The first access log and not `accept`/`connect`: Linux already has a
796/// handshake RTT at `accept` (on loopback, 27–34 µs straight out of
797/// `accept`), so an earlier sample would be valid, but a lazy one is free on
798/// a connection that never logs, and a backend dial is still in flight when
799/// its `connect(2)` returns. The price is a frozen value: the kernel's SRTT
800/// keeps moving over the connection's life and the access log reports the
801/// sample taken at its first request. A `Cell` because `Endpoint::peer_rtt`
802/// and `Endpoint::local_rtt` take `&self`.
803pub(super) fn memoized_rtt<A: std::os::fd::AsRawFd>(
804    memo: &std::cell::Cell<Option<Option<Duration>>>,
805    socket: &A,
806) -> Option<Duration> {
807    if let Some(sample) = memo.get() {
808        return sample;
809    }
810    let sample = sample_rtt(socket);
811    memo.set(Some(sample));
812    debug_assert_eq!(memo.get(), Some(sample), "the first sample must be kept");
813    sample
814}
815
816#[cfg(test)]
817thread_local! {
818    static TCP_INFO_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
819}
820
821/// `getsockopt(TCP_INFO)` reads the calling thread has made through
822/// [`sample_rtt`] so far.
823#[cfg(test)]
824pub(super) fn tcp_info_reads() -> usize {
825    TCP_INFO_READS.with(std::cell::Cell::get)
826}
827
828/// `shutdown(SHUT_WR)` on a socket whose descriptor is about to be closed,
829/// unless the peer has already closed its side.
830///
831/// Every write-side shutdown of the HTTP and HTTPS close paths goes through
832/// here: the frontend in `HttpSession::close` (`lib/src/http.rs`) and
833/// `HttpsSession::close` (`lib/src/https.rs`), the backends in `Mux::close`
834/// and in the dead-backend sweep of `Mux::ready_inner`. A frontend's
835/// lingering close (`ConnectionH1::start_linger`, `h1.rs`, and
836/// `H2Shell::linger_instead_of_closing`, `h2.rs`) calls it earlier, before
837/// it drains what the client still sends; the session close then repeats
838/// it: a no-op once the FIN is queued, or `ENOTCONN` once the connection is
839/// gone, which both callers already ignore. `peer_closed` comes
840/// from [`Connection::peer_closed`], which `HttpsSession::close` widens with
841/// `FrontRustls::peer_disconnected`.
842///
843/// Skipping it changes nothing on the wire. None of these descriptors is
844/// shared (no `dup`, `try_clone` or fork; see `doc/lifetime_of_a_session.md`
845/// §11), so the `close(2)` that follows is the last one, and Linux's
846/// `tcp_close` then sends the FIN after the queued bytes itself when the
847/// receive queue is empty, or a RST when it is not — whether or not a
848/// `shutdown(SHUT_WR)` came first. Once the peer has closed, the shutdown is
849/// therefore a syscall that only reports what the close would do, and on a
850/// peer that has also reset it fails with `ENOTCONN`. HAProxy draws the same
851/// line: `conn_sock_shutw` (`include/haproxy/connection.h`) skips the socket
852/// shutdown once `CO_FL_SOCK_RD_SH` records the read0. The TLS
853/// `close_notify` is not affected: it is written before this call, and RFC
854/// 8446 §6.1 requires it before the write side closes whether or not the
855/// peer sent its own.
856///
857/// Under `cfg(test)` each shutdown actually issued bumps a thread-local
858/// counter, read back by [`shutdown_writes`].
859pub(crate) fn shutdown_write(socket: &TcpStream, peer_closed: bool) -> std::io::Result<()> {
860    if peer_closed {
861        return Ok(());
862    }
863    #[cfg(test)]
864    SHUTDOWN_WRITES.with(|writes| writes.set(writes.get() + 1));
865    socket.shutdown(Shutdown::Write)
866}
867
868#[cfg(test)]
869thread_local! {
870    static SHUTDOWN_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
871}
872
873/// `shutdown(SHUT_WR)` calls the calling thread has issued through
874/// [`shutdown_write`] so far.
875#[cfg(test)]
876pub(crate) fn shutdown_writes() -> usize {
877    SHUTDOWN_WRITES.with(std::cell::Cell::get)
878}
879
880/// Shared logic for half-close accounting: clear `bit` from `readiness.event` on
881/// socket errors/would-block, and return `true` (yield) when no bytes were
882/// transferred so the caller can park the half. Rationale for clearing only
883/// `bit` (not both halves) on `Closed`: the opposite half may still need one
884/// last pass to flush queued frames or the TLS close_notify.
885fn update_readiness(
886    size: usize,
887    status: SocketResult,
888    readiness: &mut Readiness,
889    bit: Ready,
890) -> bool {
891    trace!(
892        "{}   size={}, status={:?}",
893        log_module_context!(),
894        size,
895        status
896    );
897    match status {
898        SocketResult::Continue => {}
899        SocketResult::Closed | SocketResult::Error | SocketResult::WouldBlock => {
900            readiness.event.remove(bit);
901        }
902    }
903    if size > 0 {
904        false
905    } else {
906        readiness.event.remove(bit);
907        true
908    }
909}
910
911/// [`update_readiness`] for a read, with one exception: a short read after HUP.
912///
913/// The socket layer stops on a short read and answers `(n, WouldBlock)`
914/// without reading on to EOF (`plain_socket_read`, `rustls_socket_read` in
915/// `lib/src/socket.rs`). A FIN that arrived with the data raised its edge
916/// already, and HUP is that edge (`Ready::from(&Event)` in
917/// `command/src/ready.rs`); no later event will announce the EOF, so dropping
918/// READABLE here would leave it unread. Keeping READABLE makes the next pass
919/// read it as `(0, Closed)`, which the H1 close-delimited response path
920/// needs. HAProxy reads `read0` on the same condition (`src/raw_sock.c`,
921/// `FD_POLL_HUP` after `ret < try`), and tokio keeps closed readiness through
922/// `clear_readiness` (`tokio/src/runtime/io/scheduled_io.rs`). The next read
923/// returns bytes or answers with `size == 0`, which drops READABLE, so the
924/// exception cannot spin.
925fn update_readiness_after_read(
926    size: usize,
927    status: SocketResult,
928    readiness: &mut Readiness,
929) -> bool {
930    if size > 0 && status == SocketResult::WouldBlock && readiness.event.is_hup() {
931        return false;
932    }
933    update_readiness(size, status, readiness, Ready::READABLE)
934}
935
936fn update_readiness_after_write(
937    size: usize,
938    status: SocketResult,
939    readiness: &mut Readiness,
940) -> bool {
941    update_readiness(size, status, readiness, Ready::WRITABLE)
942}
943pub struct Context<L: ListenerHandler + L7ListenerHandler> {
944    pub streams: Vec<Stream>,
945    /// Streams whose state is `StreamState::Link` and need backend connection.
946    /// Replaces the O(n) scan of `streams` in the ready loop.
947    pub pending_links: VecDeque<GlobalStreamId>,
948    /// Reverse index: backend token -> global stream IDs currently in
949    /// `StreamState::Linked(token)`. Eliminates O(n) scans of `streams`
950    /// when handling backend connect/disconnect/timeout/close events.
951    ///
952    /// An entry may be empty: `remove_backend_stream` keeps an emptied `Vec`
953    /// for the next link on the same live connection (#1583), so "has linked
954    /// streams" is the entry's emptiness, not the key's presence.
955    pub backend_streams: InlineTokenMap<LinkedStreams>,
956    /// Where every buffer this session's streams and connections need comes
957    /// from, and the only thing allowed to refuse one.
958    ///
959    /// Boxed rather than concrete so the core names the contract rather than
960    /// the worker's [`Pool`]: a refusal is the same event whatever stands
961    /// behind it, and the `REFUSED_STREAM` answer to one is the same answer.
962    /// [`Context::new`] installs the [`PoolBufferSource`] the proxy runs on.
963    pub buffers: Box<dyn BufferSource>,
964    pub listener: Rc<RefCell<L>>,
965    /// Connection/session ULID — mirrors `Mux.session_ulid`. Stored here so
966    /// per-stream `HttpContext` construction in [`Self::create_stream`] can
967    /// stamp the session slot of the log-context bracket without reaching
968    /// back into the parent [`Mux`].
969    pub session_ulid: Ulid,
970    pub session_address: Option<SocketAddr>,
971    pub public_address: SocketAddr,
972    pub debug: DebugHistory,
973    /// Shrink threshold ratio for recycled stream slots.
974    /// Vec is shrunk when total_slots > active_streams * ratio.
975    pub h2_stream_shrink_ratio: usize,
976    /// TLS SNI value negotiated at handshake, propagated to every
977    /// per-stream [`HttpContext`] so the routing layer can enforce
978    /// the SNI ↔ `:authority` binding on every H2 stream (and the
979    /// single H1 request). `None` for plaintext listeners or when
980    /// the client omitted the SNI extension. Stored pre-lowercased
981    /// and without a port for cheap exact-match comparison.
982    pub tls_server_name: Option<String>,
983    /// Snapshot of the SAN set of the certificate Sōzu actually served at
984    /// the TLS handshake. Captured once in `https.rs::upgrade_handshake`
985    /// from the resolver and frozen for the connection lifetime so H2
986    /// stream coalescing (RFC 7540 §9.1.1 / RFC 9113 §9.1.1) accepts any
987    /// `:authority` covered by the certificate, with RFC 6125 §6.4.3
988    /// wildcard handling. `None` for plaintext listeners or when SNI was
989    /// absent. `Some(empty)` when the default cert was served — every
990    /// `:authority` is rejected. `Arc` so the snapshot is shared across
991    /// every per-stream `HttpContext` without re-allocation.
992    pub tls_cert_names: Option<Arc<Vec<String>>>,
993    /// Whether this session's listener speaks HTTPS or plaintext HTTP.
994    ///
995    /// The one listener value that is genuinely per-connection rather than
996    /// per-request: a listener's protocol is fixed by its type and no
997    /// `update_config` patch can change it, so capturing it once in
998    /// [`Self::new`] frees both readers — [`Self::create_stream`] and the H2
999    /// write pass's `:scheme` decision — from borrowing the listener at all.
1000    /// Contrast the per-request knobs, which [`Self::create_stream`] re-reads
1001    /// on every stream precisely because a reload *can* change them
1002    /// (`LIFECYCLE.md` §2.5).
1003    pub protocol: Protocol,
1004    /// Negotiated TLS protocol version short-form (e.g. `"TLSv1.3"`).
1005    /// Captured once at handshake completion in `https.rs` and propagated
1006    /// to every per-stream [`HttpContext`] so the access log can record it
1007    /// without reaching back into the rustls session per request. `None`
1008    /// for plaintext listeners.
1009    pub tls_version: Option<&'static str>,
1010    /// Negotiated TLS cipher suite short-form (e.g.
1011    /// `"TLS_AES_128_GCM_SHA256"`). Captured once at handshake completion
1012    /// and propagated to every per-stream [`HttpContext`]. `None` for
1013    /// plaintext listeners.
1014    pub tls_cipher: Option<&'static str>,
1015    /// Negotiated ALPN protocol short-form (e.g. `"h2"`, `"http/1.1"`).
1016    /// Captured once at handshake completion and propagated to every
1017    /// per-stream [`HttpContext`]. `None` for plaintext listeners or when
1018    /// no ALPN was negotiated.
1019    pub tls_alpn: Option<&'static str>,
1020    /// Clock snapshot for the pass currently executing.
1021    ///
1022    /// [`Mux`] is the only clock sampler in the mux: it refreshes this field
1023    /// once per outer [`Mux::ready`] pass and at the top of [`Mux::timeout`]
1024    /// and [`Mux::shutting_down`]. Every time-based decision in the H2 core
1025    /// reads this snapshot — mirrored into
1026    /// `h2::ConnectionH2::now` at each public entry point — instead of
1027    /// calling [`Instant::now`] itself, so one pass sees one consistent
1028    /// "now". Every deadline armed or evaluated inside a pass is therefore
1029    /// accurate to within that pass — **in either direction**. The error is
1030    /// not one-sided: an arm site runs at an arbitrary depth into its pass
1031    /// (a DATA or HEADERS refresh, an outbound-byte refresh) and stamps the
1032    /// snapshot taken at the START of that pass, so the stored instant is
1033    /// older than the event it records; an eval site runs near the top of a
1034    /// pass (`cancel_timed_out_streams` is the first thing
1035    /// `h2::ConnectionH2::poll_read_target` does). The measured age is therefore
1036    /// inflated by the arm site's depth, and a deadline can fire up to one
1037    /// pass EARLY as well as one pass late. At base both ends read the real
1038    /// clock and the comparison was exact; this is the cost of the snapshot.
1039    ///
1040    /// The flood and back-pressure windows are the one asymmetric case, and
1041    /// they fail closed: `now` is constant for the whole pass, so a window
1042    /// cannot decay part-way through one. A burst arriving during a pass is
1043    /// weighed in full against the window that was open when the pass
1044    /// started, where before the change a long pass could decay the counters
1045    /// under the burst. The window BOUNDARY still carries the same
1046    /// one-pass error in either direction as every other deadline.
1047    ///
1048    /// Note that [`crate::SessionMetrics`] and [`crate::timer::TimeoutContainer`]
1049    /// deliberately stay on the real clock: metrics want true elapsed time,
1050    /// and the timeout containers are the embedder's timer, held by the [`Mux`]
1051    /// adapter in [`Mux::timeouts`] and never touched by a core — which
1052    /// publishes a deadline stamped from this snapshot instead.
1053    pub now: Instant,
1054    /// Wall-clock sibling of [`Self::now`]: Unix milliseconds for the pass
1055    /// currently executing.
1056    ///
1057    /// [`Self::now`] is an [`Instant`] — an opaque monotonic point with no
1058    /// epoch and no defined conversion to civil time — so it cannot supply
1059    /// the 48-bit Unix-millisecond prefix a ULID carries in its high bits
1060    /// (see [`Self::next_request_id`]). This field is that value, and it is
1061    /// refreshed by [`Mux`] at exactly the three sites that refresh
1062    /// [`Self::now`], from the same pass. The two are siblings: a site that
1063    /// sets one and not the other lets a request id carry the timestamp of
1064    /// an older pass.
1065    ///
1066    /// It is a snapshot rather than a provider closure for the same reason
1067    /// [`Self::now`] is: the mux's discipline is one clock sample per pass,
1068    /// read by the core from a field, and a boxed provider would let the
1069    /// core reach the host at an arbitrary depth into a pass instead.
1070    pub now_wall_ms: u64,
1071    /// Entropy source for the 80-bit random suffix of a request ULID.
1072    ///
1073    /// `Ulid::generate()` is exactly
1074    /// `Ulid::from_timestamp_with_rng(unix_epoch_ms(), &mut rand::rng())` —
1075    /// two ambient reaches (the wall clock and a thread-local RNG) hidden
1076    /// inside a dependency, invisible to any grep over this module for
1077    /// `Instant::now` / `SystemTime::now` / `rand`. Holding the RNG here
1078    /// replaces the second with a field the embedder owns, exactly as
1079    /// [`Self::now_wall_ms`] replaces the first.
1080    ///
1081    /// Seeded once per session in [`Self::new`] from `rand::rng()` — the
1082    /// same OS-backed reseeding CSPRNG `Ulid::generate()` draws from, so the
1083    /// unpredictability of a request id is unchanged. A request id that
1084    /// repeats across sessions would be a regression, so nothing in the mux
1085    /// re-seeds it; a deterministic simulator assigns this field directly
1086    /// after construction instead.
1087    pub request_id_rng: StdRng,
1088    /// Backend accounting the core decided and the embedder has not performed
1089    /// yet (#1340, Question 12).
1090    ///
1091    /// The core's outbound channel for registry writes, in the same place
1092    /// `udp`'s `Output` queue sits: a site that used to reach a
1093    /// `Rc<RefCell<Backend>>` through `Position::Client` and mutate it pushes
1094    /// a [`BackendDelta`] here instead. `Mux::apply_backend_deltas` is the
1095    /// only drain, and the only writer of `Backend::active_requests` /
1096    /// `Backend::active_connections` on the mux path.
1097    ///
1098    /// Entries are applied in the order they were pushed, and the drain runs
1099    /// before anything reads backend load state — in practice, before each
1100    /// `Router::plan_connect`, whose load balancer is that sole reader. FIFO plus
1101    /// drain-before-read leaves the counters at every read point exactly
1102    /// where mutating in place left them, saturation included.
1103    pub backend_deltas: Vec<BackendDelta>,
1104    /// Response bytes this session's frontend has handed to its socket, over
1105    /// the whole session. Only ever grows (wrapping), unlike the per-stream
1106    /// `SessionMetrics::bout`, which `SessionMetrics::reset` zeroes when a
1107    /// stream ends and its slot is reused. `ConnectionH1::writable` and
1108    /// `H2Shell::write_streams` add every stream byte a server-position
1109    /// write accepted.
1110    pub(super) client_bytes_out: usize,
1111    /// [`Self::client_bytes_out`] when the inner loop of `Mux::ready_inner`
1112    /// last reached `MAX_LOOP_ITERATIONS` and waited for the client instead
1113    /// of closing. A second time with no byte written to the client since
1114    /// closes the session: waiting is only worth it while the client drains
1115    /// (`Mux::wait_for_client_at_loop_limit`).
1116    pub(super) loop_limit_bytes_out: Option<usize>,
1117}
1118
1119/// Unix milliseconds for the wall clock, saturating to 0 before the epoch.
1120///
1121/// The single host wall-clock reach behind [`Context::now_wall_ms`]. Kept as
1122/// one function so the three [`Mux`] refresh sites and [`Context::new`] cannot
1123/// drift apart on the unit or on the pre-epoch case.
1124fn unix_epoch_ms() -> u64 {
1125    SystemTime::now()
1126        .duration_since(UNIX_EPOCH)
1127        .map(|d| d.as_millis() as u64)
1128        .unwrap_or(0)
1129}
1130
1131impl<L: ListenerHandler + L7ListenerHandler> Context<L> {
1132    pub fn new(
1133        session_ulid: Ulid,
1134        pool: Weak<RefCell<Pool>>,
1135        listener: Rc<RefCell<L>>,
1136        session_address: Option<SocketAddr>,
1137        public_address: SocketAddr,
1138    ) -> Self {
1139        let h2_stream_shrink_ratio = listener
1140            .borrow()
1141            .get_h2_connection_config()
1142            .stream_shrink_ratio as usize;
1143        let protocol = listener.borrow().protocol();
1144        Self {
1145            streams: Vec::new(),
1146            pending_links: VecDeque::new(),
1147            backend_streams: InlineTokenMap::new(),
1148            buffers: Box::new(PoolBufferSource::new(pool)),
1149            listener,
1150            session_ulid,
1151            session_address,
1152            public_address,
1153            debug: DebugHistory::new(),
1154            h2_stream_shrink_ratio,
1155            tls_server_name: None,
1156            tls_cert_names: None,
1157            protocol,
1158            tls_version: None,
1159            tls_cipher: None,
1160            tls_alpn: None,
1161            now: Instant::now(),
1162            now_wall_ms: unix_epoch_ms(),
1163            request_id_rng: StdRng::from_rng(&mut rand::rng()),
1164            backend_deltas: Vec::new(),
1165            client_bytes_out: 0,
1166            loop_limit_bytes_out: None,
1167        }
1168    }
1169
1170    /// Record one backend accounting change for the embedder to perform.
1171    ///
1172    /// The single push site, so every emitter reads the same way and a
1173    /// `grep` for `record_backend_delta` enumerates the ledger's inputs.
1174    pub(super) fn record_backend_delta(&mut self, backend: &BackendId, change: BackendChange) {
1175        self.backend_deltas.push(BackendDelta {
1176            slot: backend.slot(),
1177            change,
1178        });
1179    }
1180
1181    /// Mint the request ULID for a stream this session is about to open, or
1182    /// for the next request of an H1 keep-alive connection.
1183    ///
1184    /// The mux's two production ULID sites call it: `ConnectionH2::create_stream`
1185    /// (`lib/src/protocol/mux/h2.rs`) for each H2 stream, and the keep-alive
1186    /// branch of `ConnectionH1::writable` (`lib/src/protocol/mux/h1.rs`), which
1187    /// hands it to `HttpContext::reset`. It replaces `Ulid::generate()` there.
1188    /// `generate()` composes two ambient reaches — the wall clock and a
1189    /// thread-local RNG — inside `rusty_ulid`; this composes the same ULID
1190    /// from [`Self::now_wall_ms`] and [`Self::request_id_rng`], the two
1191    /// sources the embedder owns. Same bytes, same layout, no host reach
1192    /// from inside the core.
1193    ///
1194    /// `Ulid::from_timestamp_with_rng` panics above `0xFFFF_FFFF_FFFF` ms
1195    /// (year 10889); [`Self::now_wall_ms`] is a Unix-millisecond count, so
1196    /// reaching it requires a host clock set eight millennia ahead.
1197    pub fn next_request_id(&mut self) -> Ulid {
1198        Ulid::from_timestamp_with_rng(self.now_wall_ms, &mut self.request_id_rng)
1199    }
1200
1201    pub fn active_len(&self) -> usize {
1202        self.streams
1203            .iter()
1204            .filter(|s| !matches!(s.state, StreamState::Recycle))
1205            .count()
1206    }
1207
1208    /// Shared accessor for the [`HttpContext`] owned by a stream.
1209    ///
1210    /// Prefer this over `&self.streams[stream_id].context` at call sites
1211    /// that only need read access — it keeps the `Stream`/`HttpContext`
1212    /// relationship encapsulated and reads the same regardless of whether
1213    /// the caller is inside `Router::plan_connect`, the H2 mux, or a free
1214    /// helper. Panics on an out-of-bounds `stream_id`, which is the same
1215    /// behaviour as the raw `streams[sid]` indexing it replaces.
1216    pub fn http_context(&self, stream_id: GlobalStreamId) -> &HttpContext {
1217        &self.streams[stream_id].context
1218    }
1219
1220    /// Mutable sibling of [`Self::http_context`]. Use when routing
1221    /// decisions need to stamp `cluster_id` / `backend_id` on the stream's
1222    /// [`HttpContext`] (e.g. `Router::plan_connect` at the fill-cluster /
1223    /// fill-backend points).
1224    pub fn http_context_mut(&mut self, stream_id: GlobalStreamId) -> &mut HttpContext {
1225        &mut self.streams[stream_id].context
1226    }
1227
1228    /// Register a stream as linked to a backend token in the reverse index.
1229    pub fn link_stream(&mut self, stream_id: GlobalStreamId, token: Token) {
1230        self.streams[stream_id].state = StreamState::Linked(token);
1231        // An H1 backend carries one stream at a time, which a new entry
1232        // holds inline (#1613); an H2 backend spills into a `Vec`.
1233        self.backend_streams
1234            .get_or_insert_with(token, LinkedStreams::default)
1235            .push(stream_id);
1236    }
1237
1238    /// Remove a stream from the backend reverse index if it is currently
1239    /// `Linked`. Returns the backend token if one was removed.
1240    pub fn unlink_stream(&mut self, stream_id: GlobalStreamId) -> Option<Token> {
1241        if let StreamState::Linked(token) = self.streams[stream_id].state {
1242            remove_backend_stream(&mut self.backend_streams, token, stream_id);
1243            Some(token)
1244        } else {
1245            None
1246        }
1247    }
1248
1249    /// Open (or recycle) the stream slot for a request that has just arrived,
1250    /// capturing the listener configuration it will run on.
1251    ///
1252    /// This is the mux's single listener-snapshot point. `ConnectionH2` calls
1253    /// it from its HEADERS branch, so an H2 stream captures the listener as
1254    /// the operator had it configured when that request's HEADERS landed, and
1255    /// finishes on that capture whatever a concurrent reload does; the next
1256    /// HEADERS on the same connection takes a fresh one. The H1 sessions in
1257    /// `http.rs` / `https.rs` call it once per connection instead, because one
1258    /// H1 connection owns one slot that `HttpContext::reset` deliberately
1259    /// carries across keep-alive requests. `LIFECYCLE.md` §2.5 states the
1260    /// resulting staleness window for both.
1261    ///
1262    /// Every value read under the borrow below is re-read on each call for
1263    /// that reason. The two that are NOT read here — [`Self::protocol`] and
1264    /// the TLS fields — are connection-scoped by nature and captured once.
1265    ///
1266    /// `window` seeds [`Stream::front_window`], the stream's send window on
1267    /// the frontend connection. The backend leg's window is the backend's to
1268    /// size: `ConnectionH2::start_stream` sets [`Stream::back_window`].
1269    pub fn create_stream(&mut self, request_id: Ulid, window: u32) -> Option<GlobalStreamId> {
1270        let (http_context, answers) = {
1271            let listener = self.listener.borrow();
1272            let mut http_context = HttpContext::new(
1273                self.session_ulid,
1274                request_id,
1275                self.protocol,
1276                self.public_address,
1277                self.session_address,
1278                listener.get_sticky_name().to_string(),
1279                listener.get_sozu_id_header().to_string(),
1280                listener.get_elide_x_real_ip(),
1281                listener.get_send_x_real_ip(),
1282            );
1283            // Propagate the connection-scoped TLS SNI onto every per-stream
1284            // HttpContext so `route_from_request` can enforce the SNI ↔
1285            // `:authority` binding for each H2 stream independently.
1286            http_context.tls_server_name = self.tls_server_name.clone();
1287            // Mirror the frozen-at-handshake SAN snapshot. `Arc` clone is a
1288            // refcount bump, not a deep copy — every per-stream
1289            // `HttpContext` shares the same `Vec<String>`.
1290            http_context.tls_cert_names = self.tls_cert_names.clone();
1291            // Mirror the listener's strict_sni_binding flag onto each
1292            // HttpContext so the routing layer can honor operator opt-outs
1293            // without reaching back into the listener on every request.
1294            http_context.strict_sni_binding = listener.get_strict_sni_binding();
1295            // Mirror the listener's forwarding header family the same way:
1296            // a hot update reaches the next stream (H2) or connection (H1).
1297            http_context.forwarded_headers = listener.get_forwarded_headers();
1298            // The H1 trailer field bound is the listener's H2 one, so both
1299            // frontends admit the same number of trailer fields (#1701).
1300            http_context.max_trailer_fields = listener.get_h2_flood_config().max_header_fields();
1301            // Propagate the connection-scoped TLS metadata onto every
1302            // per-stream HttpContext so the access log can record it without
1303            // touching the rustls session on every request. These are
1304            // `&'static str` borrows from the rustls label tables — copy is
1305            // a pointer move.
1306            http_context.tls_version = self.tls_version;
1307            http_context.tls_cipher = self.tls_cipher;
1308            http_context.tls_alpn = self.tls_alpn;
1309            // The answer registry this request will render any default answer
1310            // from. Cloning the handle here — rather than borrowing the
1311            // listener again at each `set_default_answer` site — is what binds
1312            // a stream to the templates that were installed when it started.
1313            (http_context, listener.get_answers().clone())
1314        };
1315        let recycle_slot = self
1316            .streams
1317            .iter()
1318            .position(|s| s.state == StreamState::Recycle);
1319        if let Some(stream_id) = recycle_slot {
1320            let stream = &mut self.streams[stream_id];
1321            trace!("{} Reuse stream: {}", log_module_context!(), stream_id);
1322            stream.state = StreamState::Idle;
1323            stream.attempts = 0;
1324            stream.tried_backends.clear();
1325            stream.front_received_end_of_stream = false;
1326            stream.back_received_end_of_stream = false;
1327            stream.front_data_received = 0;
1328            stream.back_data_received = 0;
1329            // The ONE place `request_counted` is cleared without emitting the
1330            // paired `http.active_requests` decrement, and it must stay the
1331            // only one. A slot reaches `StreamState::Recycle` only through
1332            // `Stream::generate_access_log`, which already emitted the `-1`
1333            // and cleared the flag, so this is re-initialisation of an
1334            // already-false field rather than a decrement that went missing.
1335            // `Stream::new` asserts the same invariant for a fresh slot
1336            // ("new stream must not have a counted request"); this is its
1337            // reuse-path counterpart. A future site that clears this flag
1338            // without accounting for the gauge is a silent upward leak: the
1339            // `+1` stays in the aggregate with nothing left to pair it, and
1340            // an aggregate that only drifts up never underflows, so nothing
1341            // logs and nothing saturates.
1342            stream.request_counted = false;
1343            stream.front_window = i32::try_from(window).unwrap_or(i32::MAX);
1344            stream.back_window = i32::try_from(h2::DEFAULT_INITIAL_WINDOW_SIZE).unwrap_or(i32::MAX);
1345            stream.context = http_context;
1346            // A recycled slot takes the fresh capture too: the request that
1347            // released it ran on whatever the listener held then, and the one
1348            // taking it over must not inherit that.
1349            stream.answers = answers;
1350            stream.back.clear();
1351            stream.back.storage.clear();
1352            stream.front.clear();
1353            stream.front.storage.clear();
1354            // The request that released the slot may have been encoded for
1355            // an H2 backend; the one taking it over was not.
1356            stream.front_bound_to_backend = false;
1357            stream.forget_upstream_replay();
1358            stream.metrics.reset();
1359            stream.metrics.mark_request_start();
1360            // After recycling a slot, check if the Vec has excessive trailing
1361            // Recycle entries (more than 2x active streams of total capacity).
1362            let active = self.active_len();
1363            let total = self.streams.len();
1364            if total > 1 && active > 0 && total > active * self.h2_stream_shrink_ratio {
1365                self.shrink_trailing_recycle();
1366            }
1367            return Some(stream_id);
1368        }
1369        let stream = Stream::new(&mut *self.buffers, http_context, answers, window)?;
1370        self.streams.push(stream);
1371        Some(self.streams.len() - 1)
1372    }
1373
1374    /// Remove consecutive `Recycle` entries from the end of the streams Vec.
1375    ///
1376    /// This prevents unbounded growth when H2 streams are created and recycled
1377    /// over time, reclaiming memory from slots that are no longer needed.
1378    pub fn shrink_trailing_recycle(&mut self) {
1379        while self
1380            .streams
1381            .last()
1382            .is_some_and(|s| s.state == StreamState::Recycle)
1383        {
1384            self.streams.pop();
1385        }
1386    }
1387}
1388
1389/// A map keyed by [`Token`] that holds its lowest-token entry inline.
1390///
1391/// A session almost always has exactly one backend connection, so the two
1392/// per-backend indexes a mux session keeps — [`Context::backend_streams`] and
1393/// the backend half of [`MuxTimeouts`] — almost always hold one entry. A
1394/// `HashMap` allocated its table on that first insert, once per session and
1395/// so once per request on a one-request connection (#1613). Here the first
1396/// entry costs nothing and only a second backend reaches the overflow map.
1397///
1398/// The shape is [`BackendConnections`](router::BackendConnections)'s: `first`
1399/// is `None` only when `rest` is empty, and its token is below every token in
1400/// `rest`, so iteration runs in ascending `Token` order.
1401pub struct InlineTokenMap<V> {
1402    first: Option<(Token, V)>,
1403    rest: BTreeMap<Token, V>,
1404}
1405
1406impl<V> InlineTokenMap<V> {
1407    pub const fn new() -> Self {
1408        Self {
1409            first: None,
1410            rest: BTreeMap::new(),
1411        }
1412    }
1413
1414    pub fn is_empty(&self) -> bool {
1415        self.first.is_none()
1416    }
1417
1418    pub fn contains_key(&self, token: &Token) -> bool {
1419        self.get(token).is_some()
1420    }
1421
1422    pub fn get(&self, token: &Token) -> Option<&V> {
1423        match &self.first {
1424            Some((first, value)) if first == token => Some(value),
1425            _ => self.rest.get(token),
1426        }
1427    }
1428
1429    pub fn get_mut(&mut self, token: &Token) -> Option<&mut V> {
1430        match &mut self.first {
1431            Some((first, value)) if first == token => Some(value),
1432            _ => self.rest.get_mut(token),
1433        }
1434    }
1435
1436    /// The value under `token`, inserting `default()` first when there is none.
1437    pub fn get_or_insert_with(&mut self, token: Token, default: impl FnOnce() -> V) -> &mut V {
1438        let first_token = self.first.as_ref().map(|(first, _)| *first);
1439        if first_token.is_some_and(|first| token > first) {
1440            return self.rest.entry(token).or_insert_with(default);
1441        }
1442        if first_token.is_some_and(|first| token < first)
1443            && let Some((displaced_token, displaced)) = self.first.take()
1444        {
1445            self.rest.insert(displaced_token, displaced);
1446        }
1447        &mut self.first.get_or_insert_with(|| (token, default())).1
1448    }
1449
1450    /// Remove and return the value under `token`.
1451    pub fn remove(&mut self, token: &Token) -> Option<V> {
1452        match &self.first {
1453            Some((first, _)) if first == token => {
1454                let removed = std::mem::replace(&mut self.first, self.rest.pop_first());
1455                removed.map(|(_, value)| value)
1456            }
1457            _ => self.rest.remove(token),
1458        }
1459    }
1460
1461    /// Keep only the entries `keep` answers `true` for.
1462    pub fn retain(&mut self, mut keep: impl FnMut(&Token, &mut V) -> bool) {
1463        let keep_first = match &mut self.first {
1464            Some((token, value)) => keep(token, value),
1465            None => true,
1466        };
1467        self.rest.retain(|token, value| keep(token, value));
1468        if !keep_first {
1469            self.first = self.rest.pop_first();
1470        }
1471    }
1472
1473    pub fn clear(&mut self) {
1474        self.first = None;
1475        self.rest.clear();
1476    }
1477
1478    /// The entries in ascending `Token` order.
1479    pub fn iter(&self) -> impl Iterator<Item = (&Token, &V)> {
1480        self.first
1481            .iter()
1482            .map(|(token, value)| (token, value))
1483            .chain(self.rest.iter())
1484    }
1485
1486    pub fn values(&self) -> impl Iterator<Item = &V> {
1487        self.iter().map(|(_, value)| value)
1488    }
1489}
1490
1491impl<V> Default for InlineTokenMap<V> {
1492    fn default() -> Self {
1493        Self::new()
1494    }
1495}
1496
1497impl<V: Debug> Debug for InlineTokenMap<V> {
1498    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1499        f.debug_map().entries(self.iter()).finish()
1500    }
1501}
1502
1503/// The streams linked to one backend connection, in link order.
1504///
1505/// An H1 backend carries one stream at a time, so the first link is held
1506/// inline and allocates nothing; a second concurrent link (an H2 backend)
1507/// moves both into a `Vec` sized for four — the allocation the `Vec` this
1508/// replaces made at the same point — and the entry keeps that capacity for
1509/// the rest of the connection's life (#1583).
1510pub enum LinkedStreams {
1511    Inline(Option<GlobalStreamId>),
1512    Spilled(Vec<GlobalStreamId>),
1513}
1514
1515impl LinkedStreams {
1516    pub fn push(&mut self, stream_id: GlobalStreamId) {
1517        match self {
1518            LinkedStreams::Inline(None) => *self = LinkedStreams::Inline(Some(stream_id)),
1519            LinkedStreams::Inline(Some(held)) => {
1520                let mut ids = Vec::with_capacity(4);
1521                ids.push(*held);
1522                ids.push(stream_id);
1523                *self = LinkedStreams::Spilled(ids);
1524            }
1525            LinkedStreams::Spilled(ids) => ids.push(stream_id),
1526        }
1527    }
1528
1529    /// Drop `stream_id` from the list, keeping the order of the others.
1530    pub fn remove(&mut self, stream_id: GlobalStreamId) {
1531        match self {
1532            LinkedStreams::Inline(held) => {
1533                if *held == Some(stream_id) {
1534                    *held = None;
1535                }
1536            }
1537            LinkedStreams::Spilled(ids) => ids.retain(|&id| id != stream_id),
1538        }
1539    }
1540}
1541
1542impl Default for LinkedStreams {
1543    fn default() -> Self {
1544        LinkedStreams::Inline(None)
1545    }
1546}
1547
1548impl std::ops::Deref for LinkedStreams {
1549    type Target = [GlobalStreamId];
1550
1551    fn deref(&self) -> &[GlobalStreamId] {
1552        match self {
1553            LinkedStreams::Inline(held) => held.as_slice(),
1554            LinkedStreams::Spilled(ids) => ids,
1555        }
1556    }
1557}
1558
1559impl Debug for LinkedStreams {
1560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1561        f.debug_list().entries(self.iter()).finish()
1562    }
1563}
1564
1565/// The `Mux` adapter's timer-wheel handles: the frontend's, and one per live
1566/// backend connection.
1567///
1568/// The frontend's key never changes for the life of a session, so its handle
1569/// is a named field; the backends' are an open set, held in an
1570/// [`InlineTokenMap`] so the usual single backend costs no allocation. The
1571/// `HashMap<Token, TimeoutContainer>` this replaces allocated its table when
1572/// the session was built (#1613).
1573///
1574/// HAProxy goes further: a stream's deadlines are fixed fields (`conn_exp` in
1575/// `include/haproxy/stream-t.h`, each channel's `analyse_exp` in
1576/// `include/haproxy/channel-t.h`) that `process_stream` folds into its one
1577/// task's `expire` with `tick_first` (`src/stream.c`), so there is no table at
1578/// all. A sozu session keeps one wheel entry per connection instead, because
1579/// each connection's core publishes its own deadline.
1580#[derive(Debug)]
1581pub struct MuxTimeouts {
1582    pub frontend: TimeoutContainer,
1583    pub backends: InlineTokenMap<TimeoutContainer>,
1584}
1585
1586impl MuxTimeouts {
1587    pub const fn new(frontend: TimeoutContainer) -> Self {
1588        Self {
1589            frontend,
1590            backends: InlineTokenMap::new(),
1591        }
1592    }
1593}
1594
1595/// Remove `stream_id` from the backend-token reverse index for `token`.
1596/// Free function to allow split borrows when `context.streams` is already
1597/// mutably borrowed (preventing a `Context::unlink_stream` call).
1598///
1599/// An emptied entry is kept, not removed: the next `Context::link_stream` on
1600/// the same backend connection — every request on a keep-alive socket or an
1601/// H2 multiplex slot — pushes into its retained capacity instead of
1602/// allocating a fresh `Vec` (#1583). The key leaves with the connection
1603/// itself, in the dead-backend sweep of `Mux::ready_inner`, and `Mux::close`
1604/// clears the whole index, so the index stays bounded by the session's live
1605/// backend connections.
1606/// [`connect_outcome`] against the three states a dialled socket can be in
1607/// when sozu sees WRITABLE on it (sozu-proxy/sozu#1800).
1608#[cfg(test)]
1609mod connect_outcome_tests {
1610    use std::{
1611        net::{SocketAddr, TcpListener},
1612        os::fd::AsRawFd,
1613        time::{Duration, Instant},
1614    };
1615
1616    use super::{ConnectOutcome, connect_outcome};
1617
1618    /// Poll `connect_outcome` until it leaves `Pending` or `deadline` passes.
1619    fn settle(socket: &mio::net::TcpStream, deadline: Duration) -> ConnectOutcome {
1620        let started = Instant::now();
1621        loop {
1622            let outcome = connect_outcome(socket);
1623            if outcome != ConnectOutcome::Pending || started.elapsed() >= deadline {
1624                return outcome;
1625            }
1626            std::thread::sleep(Duration::from_millis(10));
1627        }
1628    }
1629
1630    #[test]
1631    fn an_accepted_dial_is_connected() {
1632        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1633        let socket = mio::net::TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1634        assert_eq!(
1635            settle(&socket, Duration::from_secs(2)),
1636            ConnectOutcome::Connected
1637        );
1638    }
1639
1640    #[test]
1641    fn a_refused_dial_failed() {
1642        let address: SocketAddr = {
1643            let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1644            listener.local_addr().unwrap()
1645        };
1646        // A loopback refusal may also be reported by `connect(2)` itself, in
1647        // which case there is no socket to read the outcome from.
1648        let Ok(socket) = mio::net::TcpStream::connect(address) else {
1649            return;
1650        };
1651        assert_eq!(
1652            settle(&socket, Duration::from_secs(2)),
1653            ConnectOutcome::Failed
1654        );
1655    }
1656
1657    #[test]
1658    fn a_dial_nobody_answers_is_pending() {
1659        // A full accept queue drops the SYN, so the dial stays in flight.
1660        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1661        // SAFETY: shrinks the backlog of a socket this test owns.
1662        assert_eq!(unsafe { libc::listen(listener.as_raw_fd(), 0) }, 0);
1663        let address = listener.local_addr().unwrap();
1664        let mut queued = Vec::new();
1665        while let Ok(stream) =
1666            std::net::TcpStream::connect_timeout(&address, Duration::from_millis(300))
1667        {
1668            queued.push(stream);
1669            assert!(queued.len() < 16, "the accept queue never filled");
1670        }
1671        let socket = mio::net::TcpStream::connect(address).unwrap();
1672        std::thread::sleep(Duration::from_millis(100));
1673        assert_eq!(connect_outcome(&socket), ConnectOutcome::Pending);
1674    }
1675}
1676
1677/// Where the non-blocking `connect(2)` behind a connecting backend socket
1678/// stands when its readiness carries WRITABLE.
1679#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1680enum ConnectOutcome {
1681    /// The connection is established.
1682    Connected,
1683    /// The connect is still in progress: the WRITABLE was not the kernel's
1684    /// completion edge but one sozu raised itself (`Readiness::
1685    /// signal_pending_write` to flush queued output). The completion edge is
1686    /// still to come.
1687    Pending,
1688    /// The connect failed.
1689    Failed,
1690}
1691
1692/// Read the outcome of the non-blocking `connect(2)` behind `socket`.
1693///
1694/// The kernel reports it through the socket's pending error (`SO_ERROR`,
1695/// POSIX `connect(2)`): a WRITABLE edge means the attempt finished, not that
1696/// it succeeded, and reading the error clears it. An empty error proves
1697/// nothing alone, because the WRITABLE may be one sozu signalled itself while
1698/// the dial is still in flight, so `getpeername(2)` then tells an
1699/// established connection from a pending one (`ENOTCONN`). An error reading
1700/// either is a failed connect: nothing past it could use the socket
1701/// (sozu-proxy/sozu#1800).
1702fn connect_outcome(socket: &TcpStream) -> ConnectOutcome {
1703    match socket.take_error() {
1704        Ok(None) => {}
1705        Ok(Some(error)) | Err(error) => {
1706            // The level of the synchronous refusal the pending-links loop
1707            // of `Mux::ready_inner` logs before it retries.
1708            debug!(
1709                "{} backend connect failed, retrying: {}",
1710                log_module_context!(),
1711                error
1712            );
1713            return ConnectOutcome::Failed;
1714        }
1715    }
1716    match socket.peer_addr() {
1717        Ok(_) => ConnectOutcome::Connected,
1718        Err(error) if error.kind() == ErrorKind::NotConnected => ConnectOutcome::Pending,
1719        Err(_) => ConnectOutcome::Failed,
1720    }
1721}
1722
1723pub(super) fn remove_backend_stream(
1724    index: &mut InlineTokenMap<LinkedStreams>,
1725    token: Token,
1726    stream_id: GlobalStreamId,
1727) {
1728    if let Some(ids) = index.get_mut(&token) {
1729        ids.remove(stream_id);
1730    }
1731}
1732
1733pub struct Mux<Front: SocketHandler, L: ListenerHandler + L7ListenerHandler> {
1734    pub configured_frontend_timeout: Duration,
1735    pub frontend_token: Token,
1736    pub frontend: Connection<Front>,
1737    pub router: Router,
1738    pub context: Context<L>,
1739    /// The adapter's wheel handles, one per live token (the frontend plus
1740    /// every backend in `router.backends`).
1741    ///
1742    /// The H1/H2 cores no longer own a `TimeoutContainer`: they publish the
1743    /// instant they want to be called back at through `Connection::poll_timeout`
1744    /// and `Mux::reschedule` reflects that onto `crate::timer`, arming only
1745    /// when what the wheel holds differs from what the core wants. This map is
1746    /// the ONLY thing in the mux that talks to the timer wheel.
1747    ///
1748    /// A handle for a token that has left `router.backends` is dropped by
1749    /// `reschedule`'s `retain`, and `TimeoutContainer::drop` cancels its entry
1750    /// — which is why no backend-removal site has to remember to cancel.
1751    pub timeouts: MuxTimeouts,
1752    /// The registry handles the core's [`BackendSlot`]s stand for.
1753    ///
1754    /// The embedder's half of Question 12's perimeter: the core carries an
1755    /// opaque slot, this resolves it, and `Mux::apply_backend_deltas` is
1756    /// what turns the core's [`BackendDelta`]s into writes on the worker's
1757    /// `Backend` values. Indexed by slot.
1758    ///
1759    /// A dial reuses an existing slot when `Rc::ptr_eq` matches, so the
1760    /// vector is bounded by the number of DISTINCT backends a session dials
1761    /// rather than by its request count. Entries are never removed while the
1762    /// session lives: a delta pushed before a backend connection closed must
1763    /// still resolve when the drain reaches it, and an index that moved would
1764    /// silently mis-charge it.
1765    pub(crate) backend_registry: BackendRegistry,
1766    /// The worker's backend set, which `Mux::dial_backend` lends to
1767    /// `Router::backend_from_request` through a `RegistrySelector`.
1768    ///
1769    /// Handed over when the session is built rather than read through
1770    /// `L7Proxy` on every dial: `HttpProxy` and `HttpsProxy` never reassign
1771    /// their `backends` field, so this is the same map, and a dial no longer
1772    /// borrows the proxy or clones the handle to reach it.
1773    pub(crate) backends: Rc<RefCell<BackendMap>>,
1774    /// Per-session correlation ID generated at construction time. Included in
1775    /// every log line emitted from this module so all events for a single
1776    /// frontend connection can be reassembled (independent of the ephemeral
1777    /// per-stream request id used by access logs).
1778    pub session_ulid: Ulid,
1779}
1780
1781/// Answer the per-`(cluster, source-IP)` limit step the core paused at.
1782///
1783/// The embedder half of [`router::ConnectStep::CheckIpLimit`]. The core
1784/// cannot do this itself: it is a consult of
1785/// `SessionManager::cluster_ip_at_limit` **and**, on the admitting path, a
1786/// call to `SessionManager::track_cluster_ip`, which mutates
1787/// `connections_per_cluster_ip` — worker-global state keyed on
1788/// `(cluster, ip)` across every session, and read again by
1789/// `lib/src/tcp.rs`'s own gate for the TCP proxy.
1790///
1791/// **The track happens here, before the decision resumes, and that ordering
1792/// is the point.** An `Admitted` verdict asserts to the core that the slot
1793/// has been taken. Resuming as `Admitted` without having tracked would let
1794/// the stream through uncounted, and because the counter is worker-global
1795/// the next session from the same IP would find a slot that should already
1796/// be gone. `an_admitted_stream_is_counted_before_the_decision_resumes`
1797/// pins it.
1798///
1799/// A free function rather than a `Mux` method so it can be driven directly:
1800/// the ordering above is the thing worth testing, and testing it through a
1801/// copy of this logic would prove nothing about this logic.
1802fn consult_ip_gate(
1803    sessions: &Rc<RefCell<crate::server::SessionManager>>,
1804    frontend_token: Token,
1805    // The cluster routing resolved, borrowed from the stream's `HttpContext`
1806    // where `Router::plan_connect` stored it (#1583).
1807    cluster_id: &sozu_command::state::ClusterId,
1808    resume: &router::ConnectResume,
1809) -> router::IpGateVerdict {
1810    // BOTH caps are consulted here, through the one combined gate:
1811    // the pre-existing per-(cluster, source-IP) cap and the
1812    // per-(cluster, source-SUBNET) cap that stands beside it. A
1813    // connection is admitted only when both allow it, which is what
1814    // makes "10 per IP AND 100 per /64" expressible. The subnet cap is
1815    // `0` (disabled) by default, in which case this is byte-for-byte
1816    // the previous per-IP consult.
1817    let at_limit = sessions.borrow().cluster_connection_at_limit(
1818        frontend_token,
1819        cluster_id,
1820        &resume.ip(),
1821        resume.max_connections_per_ip(),
1822        resume.max_connections_per_subnet(),
1823    );
1824    if at_limit {
1825        let retry_after = sessions
1826            .borrow()
1827            .effective_retry_after(resume.cluster_retry_after());
1828        return router::IpGateVerdict::AtLimit { retry_after };
1829    }
1830    // Idempotent track — H2 streams to the same `(cluster, ip)` share a
1831    // single slot in the per-token set. The decrement happens wholesale on
1832    // session close, via `untrack_all_cluster_ip`.
1833    sessions.borrow_mut().track_cluster_connection(
1834        frontend_token,
1835        cluster_id.clone(),
1836        resume.ip(),
1837        resume.max_connections_per_subnet(),
1838    );
1839    router::IpGateVerdict::Admitted
1840}
1841
1842impl<Front: SocketHandler, L: ListenerHandler + L7ListenerHandler> Mux<Front, L> {
1843    pub fn front_socket(&self) -> &TcpStream {
1844        self.frontend.socket()
1845    }
1846
1847    /// Whether a backend connection carries a HUP or ERROR event that no
1848    /// socket readiness will deliver again: the flag `Mux::timeout_inner`
1849    /// sets on a dial whose connect timed out. The session answers
1850    /// `ProxySession::needs_ready_pass` with this, so `Server::timeout` runs
1851    /// the `ready` pass that counts the failure and fails the request over
1852    /// (sozu-proxy/sozu#1800).
1853    pub fn has_pending_backend_failure(&self) -> bool {
1854        self.router.backends.iter().any(|(_, backend)| {
1855            let event = backend.readiness().event;
1856            event.is_error() || event.is_hup()
1857        })
1858    }
1859
1860    /// Whether the client has closed its side of the frontend connection, the
1861    /// `peer_closed` argument the session's close path hands to
1862    /// `shutdown_write`. See [`Connection::peer_closed`].
1863    pub fn frontend_peer_closed(&self) -> bool {
1864        self.frontend.peer_closed()
1865    }
1866
1867    /// Perform every backend accounting change the core has decided since the
1868    /// last drain, in the order it decided them.
1869    ///
1870    /// The ONLY writer of `Backend::active_requests` and
1871    /// `Backend::active_connections` on the mux path. The drain leaves the
1872    /// queue empty, so running this twice in a row is idempotent — the second
1873    /// call finds nothing and performs nothing.
1874    ///
1875    /// Called from the four `Mux` wrappers that already owe the timer wheel a
1876    /// `reschedule` on every exit, and once more immediately before each
1877    /// `Router::plan_connect`, because that call's load balancer is the only
1878    /// reader of the counters this drains.
1879    pub(crate) fn apply_backend_deltas(&mut self) {
1880        self.backend_registry
1881            .apply_all(&mut self.context.backend_deltas, &self.backends);
1882    }
1883}
1884
1885impl<Front: SocketHandler + std::fmt::Debug, L: ListenerHandler + L7ListenerHandler> Mux<Front, L> {
1886    fn sync_upgrade_buffers(&mut self) {
1887        for stream in &mut self.context.streams {
1888            stream
1889                .front
1890                .storage
1891                .buffer
1892                .sync(stream.front.storage.end, stream.front.storage.head);
1893            stream
1894                .back
1895                .storage
1896                .buffer
1897                .sync(stream.back.storage.end, stream.back.storage.head);
1898        }
1899    }
1900
1901    /// Consume the wheel entry that delivered a timeout and say whether it was
1902    /// really due.
1903    ///
1904    /// Three things happen here, in this order, and all three are load-bearing.
1905    ///
1906    /// 1. **Consume.** The wheel has already handed the entry over, so
1907    ///    [`TimeoutContainer::triggered`] records that the handle holds
1908    ///    nothing. This is the consume-then-reschedule step, the direct
1909    ///    counterpart of `UdpManager::handle_timeout` clearing `armed_deadline`
1910    ///    on entry (`lib/src/protocol/udp/manager.rs`). It matters because
1911    ///    [`Mux::sync_timeout`] memoizes on "the handle already holds this
1912    ///    deadline": without the clearing, a delivery that changes nothing is
1913    ///    memoized away and the session is left with NO wheel entry at all — a
1914    ///    lost wakeup.
1915    ///
1916    /// 2. **Re-validate.** `crate::timer` rounds a delay to the NEAREST tick
1917    ///    (`timer.rs` `duration_to_tick`) and `Timer::poll` fires everything
1918    ///    whose tick has come, so an entry armed for deadline `D` is delivered
1919    ///    from `tick * round(D / tick) - tick/2` onwards. The earliness is
1920    ///    `(D_ms + 50) mod 100` at the default 100 ms tick, i.e. up to **99 ms**
1921    ///    — a full tick minus a millisecond, not half a tick; see
1922    ///    `timer.rs::duration_to_tick` for the two halves of that bound. An
1923    ///    early delivery is not an expiry: returning
1924    ///    `false` here leaves the core's deadline untouched, and the
1925    ///    `reschedule` on the way out of [`SessionState::timeout`] therefore
1926    ///    puts the entry straight back at the SAME instant.
1927    ///
1928    /// 3. **Clear the core's deadline on a real expiry.** The core asked to be
1929    ///    called back at `D` and has been; leaving `D` in place would have
1930    ///    `reschedule` re-arm an already-elapsed instant, which the wheel
1931    ///    re-delivers immediately — a busy loop. The timeout branches re-arm
1932    ///    with `arm_timeout` on every path that keeps the session alive, which
1933    ///    is what the old `triggered()` + `set(token)` pair did.
1934    ///
1935    /// A token with no core (an unknown token, or a backend already evicted)
1936    /// is reported due and handled by the caller's unknown-token branch.
1937    fn consume_timer_entry(&mut self, token: Token) -> bool {
1938        if token == self.frontend_token {
1939            self.timeouts.frontend.triggered();
1940        } else if let Some(container) = self.timeouts.backends.get_mut(&token) {
1941            container.triggered();
1942        }
1943        let core_deadline = if token == self.frontend_token {
1944            self.frontend.poll_timeout()
1945        } else {
1946            match self.router.backends.get(&token) {
1947                Some(backend) => backend.poll_timeout(),
1948                None => return true,
1949            }
1950        };
1951        if core_deadline.is_some_and(|deadline| self.context.now < deadline) {
1952            return false;
1953        }
1954        if token == self.frontend_token {
1955            self.frontend.clear_timeout();
1956        } else if let Some(backend) = self.router.backends.get_mut(&token) {
1957            backend.clear_timeout();
1958        }
1959        true
1960    }
1961
1962    /// Reflect every core's next deadline onto the timer wheel.
1963    ///
1964    /// This is the adapter half of the `poll_timeout()` split, and the direct
1965    /// analogue of `UdpManager::reschedule` (`lib/src/protocol/udp/manager.rs`).
1966    /// It runs at the end of every `SessionState` entry point that can change a
1967    /// deadline — `ready`, `timeout`, `shutting_down`, `cancel_timeouts` —
1968    /// through wrappers, so no early `return` inside those bodies can skip it.
1969    /// That structural placement is the point: a hand-placed re-arm at each
1970    /// exit is exactly what used to be forgotten.
1971    ///
1972    /// Departed tokens need no explicit cancel: `retain` drops their handle and
1973    /// `TimeoutContainer::drop` cancels the entry.
1974    fn reschedule(&mut self) {
1975        Self::sync_timeout(
1976            &mut self.timeouts.frontend,
1977            self.frontend_token,
1978            self.frontend.poll_timeout(),
1979            self.frontend.timeout_duration(),
1980        );
1981        for (token, backend) in &self.router.backends {
1982            let duration = backend.timeout_duration();
1983            Self::sync_timeout(
1984                self.timeouts
1985                    .backends
1986                    .get_or_insert_with(*token, || TimeoutContainer::new_empty(duration)),
1987                *token,
1988                backend.poll_timeout(),
1989                duration,
1990            );
1991        }
1992        let backends = &self.router.backends;
1993        self.timeouts
1994            .backends
1995            .retain(|token, _| backends.contains_key(token));
1996
1997        self.debug_assert_timer_coherence();
1998    }
1999
2000    /// Arm, re-arm or cancel one token's wheel entry so it matches `next`.
2001    ///
2002    /// The memoization — skip the wheel when nothing changed — is what makes
2003    /// this cheap enough to run on every pass, and it is also where the lost
2004    /// wakeup lives if it is written carelessly. It is guarded on BOTH halves:
2005    ///
2006    /// * `deadline() == next` — the instant has not moved; and
2007    /// * `is_armed() == next.is_some()` — the wheel actually still HOLDS it.
2008    ///
2009    /// The second is not redundant. A delivered entry is gone from the wheel,
2010    /// and `TimeoutContainer::triggered` clears the deadline alongside it, so
2011    /// the pair stays honest — that clearing is this design's
2012    /// consume-then-reschedule step, the same one `UdpManager::handle_timeout`
2013    /// performs by setting `armed_deadline = None` on entry. Memoize on the
2014    /// deadline alone, against a mirror that a firing does not clear, and every
2015    /// wheel delivery that changes nothing becomes a session with no timer.
2016    fn sync_timeout(
2017        container: &mut TimeoutContainer,
2018        token: Token,
2019        next: Option<Instant>,
2020        duration: Duration,
2021    ) {
2022        if container.duration() != duration {
2023            // Keep the handle's configured duration current without moving the
2024            // armed entry: the WebSocket upgrade hands these containers to
2025            // `Pipe`, which re-arms from `duration()`.
2026            container.retune(duration);
2027        }
2028        if container.deadline() == next && container.is_armed() == next.is_some() {
2029            return;
2030        }
2031        match next {
2032            Some(deadline) => container.set_at(token, deadline),
2033            None => {
2034                container.cancel();
2035            }
2036        }
2037    }
2038
2039    /// Timer coherence, carried over from `UdpManager::check_invariants` (6):
2040    /// for every live token the wheel handle must hold exactly what the core
2041    /// asked for — armed iff the core wants a timer, at the core's instant when
2042    /// it does — and no handle may outlive its connection.
2043    ///
2044    /// This is the invariant a missed `reschedule` breaks, and it is the reason
2045    /// the entry points are wrapped rather than trusted.
2046    fn debug_assert_timer_coherence(&self) {
2047        #[cfg(debug_assertions)]
2048        {
2049            let frontend_token = self.frontend_token;
2050            let frontend = std::iter::once((&frontend_token, &self.timeouts.frontend));
2051            for (token, container) in frontend.chain(self.timeouts.backends.iter()) {
2052                let core = if *token == frontend_token {
2053                    self.frontend.poll_timeout()
2054                } else {
2055                    match self.router.backends.get(token) {
2056                        Some(backend) => backend.poll_timeout(),
2057                        None => {
2058                            panic!("timer handle for {token:?} outlived its connection")
2059                        }
2060                    }
2061                };
2062                debug_assert_eq!(
2063                    container.is_armed(),
2064                    core.is_some(),
2065                    "timer coherence for {token:?}: armed={} but the core wants {core:?}",
2066                    container.is_armed(),
2067                );
2068                debug_assert_eq!(
2069                    container.deadline(),
2070                    core,
2071                    "timer coherence for {token:?}: the wheel holds {:?}, the core wants {core:?}",
2072                    container.deadline(),
2073                );
2074            }
2075        }
2076    }
2077
2078    /// Whether a frontend HUP still leaves an exchange to serve.
2079    ///
2080    /// The frontend HUP is not always a hang-up: `Ready::from(&Event)`
2081    /// (`command/src/ready.rs`) also raises it for `is_read_closed()`, the
2082    /// `EPOLLRDHUP` a client's half-close (`shutdown(SHUT_WR)`, RFC 9293
2083    /// §3.6) produces. Such a client has stopped sending, not receiving, and
2084    /// still expects the response to what it sent:
2085    ///
2086    /// - input not read yet may hold a request, or the rest of one: the FIN
2087    ///   can arrive with the request itself (`Connection::has_unread_input`);
2088    /// - a stream whose request was received whole is answered.
2089    ///
2090    /// Nothing else is: once the client's EOF is read, a request it left
2091    /// incomplete can never complete, and the session closes as on a hang-up
2092    /// instead of waiting for its backend.
2093    ///
2094    /// A full hang-up is never in flight: the client will read nothing more.
2095    /// It is ERROR (`EPOLLERR`, a reset) or WRITE_CLOSED (mio's
2096    /// `is_write_closed`: `EPOLLHUP` or `EPOLLERR`). ERROR alone is not
2097    /// enough: a reset whose error sozu's own `read` or `write` consumed
2098    /// first is reported as `EPOLLHUP` without `EPOLLERR`, which only
2099    /// WRITE_CLOSED tells from a half-close. A lingering frontend is the one
2100    /// exception: sozu shut its write side itself, so `EPOLLHUP` follows the
2101    /// client's FIN, and its drain still reads to that EOF.
2102    fn frontend_exchange_in_flight(&self) -> bool {
2103        let event = self.frontend.readiness().event;
2104        if event.is_error() || (event.is_write_closed() && !self.frontend.is_lingering()) {
2105            return false;
2106        }
2107        self.frontend.has_unread_input()
2108            || self
2109                .context
2110                .streams
2111                .iter()
2112                .any(|stream| stream.state.is_open() && stream.front.is_terminated())
2113    }
2114
2115    fn delay_close_for_frontend_flush(&mut self, reason: &'static str) -> bool {
2116        let _ = self.frontend.initiate_close_notify();
2117        // LIFECYCLE §9 invariant 16: consult per-stream back-buffers in
2118        // addition to the connection-level pending-write predicate so
2119        // shutdown does not close while any open H2 stream still has
2120        // kawa bytes queued after a voluntary scheduler yield.
2121        if self
2122            .frontend
2123            .has_pending_write_including_streams(&self.context)
2124        {
2125            let readiness = self.frontend.readiness_mut();
2126            readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
2127            readiness.signal_pending_write();
2128            debug!(
2129                "{} Mux delaying close on {}: {:?}",
2130                log_context!(self),
2131                reason,
2132                self.frontend
2133            );
2134            true
2135        } else {
2136            false
2137        }
2138    }
2139
2140    /// The inner loop of `Mux::ready_inner` reached `MAX_LOOP_ITERATIONS`.
2141    /// Count and log it, then tell the loop whether to wait for the client
2142    /// (`true`) or close the session (`false`).
2143    ///
2144    /// Output still queued for the client, in the connection or in an open
2145    /// stream's response buffer, is not dropped while the client drains it.
2146    /// An H1 frontend's `has_pending_write_including_streams` does not see
2147    /// its stream's buffer, so the open streams are probed here
2148    /// (sozu-proxy/sozu#1819). The session then waits for the frontend's next
2149    /// writable event, with the frontend timeout as the bound if none comes.
2150    ///
2151    /// Waiting stays bounded: reaching the limit again with no byte written
2152    /// to the client since the last time (`Context::client_bytes_out`)
2153    /// closes the session, so a loop that spins on every wake-up without
2154    /// progress costs at most two budgets.
2155    fn wait_for_client_at_loop_limit(&mut self) -> bool {
2156        incr!(names::http::INFINITE_LOOP_ERROR);
2157        #[cfg(test)]
2158        LOOP_LIMIT_HITS.with(|hits| hits.set(hits.get() + 1));
2159        let bytes_out = self.context.client_bytes_out;
2160        let progressed = self.context.loop_limit_bytes_out != Some(bytes_out);
2161        let output_queued = progressed
2162            && (self
2163                .frontend
2164                .has_pending_write_including_streams(&self.context)
2165                || self.context.streams.iter().any(|stream| {
2166                    stream.state.is_open()
2167                        && (!stream.back.out.is_empty() || !stream.back.blocks.is_empty())
2168                }));
2169        warn!(
2170            "{} Mux loop reached {} iterations, {}: frontend={:?} backends={:?}",
2171            log_context!(self),
2172            MAX_LOOP_ITERATIONS,
2173            if output_queued {
2174                "waiting for the client to drain the queued output"
2175            } else if progressed {
2176                "closing"
2177            } else {
2178                "closing: no byte reached the client since the last time"
2179            },
2180            self.frontend.readiness(),
2181            self.router
2182                .backends
2183                .iter()
2184                .map(|(token, backend)| (*token, backend.readiness().clone()))
2185                .collect::<Vec<_>>()
2186        );
2187        if output_queued {
2188            self.context.loop_limit_bytes_out = Some(bytes_out);
2189            let readiness = self.frontend.readiness_mut();
2190            readiness.interest.insert(Ready::WRITABLE);
2191            readiness.event.remove(Ready::WRITABLE);
2192            self.frontend.arm_timeout(self.context.now);
2193        }
2194        output_queued
2195    }
2196
2197    /// Drive the frontend I/O path during shutdown, when the server is polling
2198    /// `shutting_down()` outside the normal epoll readiness loop.
2199    ///
2200    /// This is required for H2 graceful shutdown because a stream may still
2201    /// need one last readable pass to observe the peer's END_STREAM or one last
2202    /// writable pass to retire the stream, emit GOAWAY, or flush TLS records.
2203    ///
2204    /// An H2 frontend always gets that one writable pass. Further passes run
2205    /// only while the connection still has output queued AND the socket took
2206    /// the last write: the loop stops as soon as either is false. It must not
2207    /// wait for the WRITABLE *event* to clear, because the forced pass sets
2208    /// that bit itself (`signal_pending_write`) and `writable()` clears it only
2209    /// on `WouldBlock`. An idle draining session therefore never cleared it,
2210    /// and every `shut_down_sessions()` tick spun `MAX_LOOP_ITERATIONS` empty
2211    /// `writable()` calls per draining H2 session.
2212    fn drive_frontend_shutdown_io(&mut self) -> SessionIsToBeClosed {
2213        let force_h2_read = matches!(self.frontend, Connection::H2(_));
2214        let force_h2_write = matches!(self.frontend, Connection::H2(_));
2215        let readiness = self.frontend.readiness().clone();
2216        if !force_h2_read
2217            && !force_h2_write
2218            && readiness.event.is_empty()
2219            && !self.frontend.has_pending_write()
2220        {
2221            return false;
2222        }
2223
2224        if force_h2_read || self.frontend.readiness().event.is_readable() {
2225            self.frontend
2226                .readiness_mut()
2227                .interest
2228                .insert(Ready::READABLE);
2229            match self
2230                .frontend
2231                .readable(&mut self.context, EndpointClient(&mut self.router))
2232            {
2233                MuxResult::Continue => {}
2234                MuxResult::CloseSession | MuxResult::Upgrade => return true,
2235            }
2236        }
2237
2238        if !force_h2_write
2239            && !self.frontend.has_pending_write()
2240            && !self.frontend.readiness().event.is_writable()
2241        {
2242            return false;
2243        }
2244
2245        let mut iterations = 0;
2246        loop {
2247            self.frontend
2248                .readiness_mut()
2249                .interest
2250                .insert(Ready::WRITABLE);
2251            if force_h2_write {
2252                self.frontend.readiness_mut().signal_pending_write();
2253            }
2254            #[cfg(test)]
2255            SHUTDOWN_WRITABLE_PASSES.with(|passes| passes.set(passes.get() + 1));
2256            match self
2257                .frontend
2258                .writable(&mut self.context, EndpointClient(&mut self.router))
2259            {
2260                MuxResult::Continue => {}
2261                MuxResult::CloseSession | MuxResult::Upgrade => return true,
2262            }
2263
2264            iterations += 1;
2265            if iterations >= MAX_LOOP_ITERATIONS
2266                || !self.frontend.has_pending_write()
2267                || !self.frontend.readiness().event.is_writable()
2268            {
2269                break;
2270            }
2271        }
2272        false
2273    }
2274}
2275
2276impl<Front: SocketHandler + std::fmt::Debug, L: ListenerHandler + L7ListenerHandler> SessionState
2277    for Mux<Front, L>
2278{
2279    /// Thin wrapper over `Mux::ready_inner` whose only job is to run
2280    /// `Mux::reschedule` on the way out. `ready_inner` has a dozen `return`
2281    /// sites; arming the wheel at each of them by hand is precisely the
2282    /// discipline this refactor exists to remove.
2283    fn ready(
2284        &mut self,
2285        session: Rc<RefCell<dyn ProxySession>>,
2286        proxy: Rc<RefCell<dyn L7Proxy>>,
2287        metrics: &mut SessionMetrics,
2288    ) -> SessionResult {
2289        let result = self.ready_inner(session, proxy, metrics);
2290        self.apply_backend_deltas();
2291        self.reschedule();
2292        result
2293    }
2294
2295    fn update_readiness(&mut self, token: Token, events: Ready) {
2296        trace!("{} EVENTS: {:?} on {:?}", log_context!(self), events, token);
2297        self.context.debug.push(DebugEvent::EV(token, events));
2298        if token == self.frontend_token {
2299            self.frontend.readiness_mut().event |= events;
2300        } else if let Some(c) = self.router.backends.get_mut(&token) {
2301            c.readiness_mut().event |= events;
2302        }
2303    }
2304
2305    /// Thin wrapper over `Mux::timeout_inner`: reschedule on the way out,
2306    /// then check the two properties carried over from `UdpManager`.
2307    fn timeout(&mut self, token: Token, metrics: &mut SessionMetrics) -> StateResult {
2308        let result = self.timeout_inner(token, metrics);
2309        self.apply_backend_deltas();
2310        self.reschedule();
2311
2312        // Strict-advance guard, carried over from `UdpManager::handle_timeout`
2313        // (`lib/src/protocol/udp/manager.rs`). After handling a firing at
2314        // `now`, the entry re-armed for the SAME token must be strictly in the
2315        // future. A deadline `<= now` makes the wheel re-deliver immediately
2316        // and the session spins — the canonical sans-io busy loop. The one
2317        // legitimate way to reach it is a configured timeout of zero, which is
2318        // an operator asking for exactly that.
2319        #[cfg(debug_assertions)]
2320        {
2321            let now = self.context.now;
2322            let (duration, container) = if token == self.frontend_token {
2323                (
2324                    Some(self.frontend.timeout_duration()),
2325                    Some(&self.timeouts.frontend),
2326                )
2327            } else {
2328                (
2329                    self.router
2330                        .backends
2331                        .get(&token)
2332                        .map(|b| b.timeout_duration()),
2333                    self.timeouts.backends.get(&token),
2334                )
2335            };
2336            if let Some(container) = container
2337                && let Some(next) = container.deadline()
2338                && duration.is_some_and(|d| !d.is_zero())
2339            {
2340                debug_assert!(
2341                    next > now,
2342                    "Mux::timeout must strictly advance past a firing on {token:?}: \
2343                     armed {next:?} <= fired_at {now:?} (busy-loop)"
2344                );
2345            }
2346        }
2347
2348        result
2349    }
2350
2351    fn cancel_timeouts(&mut self) {
2352        trace!("{} MuxState::cancel_timeouts", log_context!(self));
2353        self.frontend.clear_timeout();
2354        for backend in self.router.backends.values_mut() {
2355            backend.clear_timeout();
2356        }
2357        // Push the cleared deadlines onto the wheel in the same call: the
2358        // adapter's handles are the only thing holding real entries.
2359        self.reschedule();
2360    }
2361
2362    fn print_state(&self, _context: &str) {
2363        // The trait-required `context: &str` parameter (protocol tag like
2364        // "HTTPS"/"HTTP" passed by callers) predates the unified
2365        // `log_context!(self)` envelope. The canonical `MUX` tag lives
2366        // inside `log_context!`, so we ignore the parameter here and emit
2367        // the bracketed Session(...) block instead, mirroring the second
2368        // `error!` in this function.
2369        error!(
2370            "\
2371{} Session(Mux)
2372\tFrontend:
2373\t\ttoken: {:?}\treadiness: {:?}
2374\tBackend(s):",
2375            log_context!(self),
2376            self.frontend_token,
2377            self.frontend.readiness()
2378        );
2379        for (backend_token, backend) in &self.router.backends {
2380            error!(
2381                "{} \t\ttoken: {:?}\treadiness: {:?}",
2382                log_context!(self),
2383                backend_token,
2384                backend.readiness()
2385            )
2386        }
2387    }
2388
2389    fn close(&mut self, proxy: Rc<RefCell<dyn L7Proxy>>, _metrics: &mut SessionMetrics) {
2390        if self.context.debug.is_interesting() {
2391            warn!("{} {:?}", log_context!(self), self.context.debug.events);
2392        }
2393        debug!("{} MUX CLOSE", log_context!(self));
2394        trace!("{} FRONTEND: {:#?}", log_context!(self), self.frontend);
2395        trace!(
2396            "{} BACKENDS: {:#?}",
2397            log_context!(self),
2398            self.router.backends
2399        );
2400
2401        self.log_and_recycle_open_streams();
2402
2403        self.frontend
2404            .close(&mut self.context, EndpointClient(&mut self.router));
2405
2406        // Release every wheel handle: the session is going away and
2407        // `TimeoutContainer::drop` cancels each entry. This replaces the
2408        // per-backend `timeout_container().cancel()` the loop below used to do
2409        // and additionally covers the frontend, which it never did. The
2410        // frontend's handle is a field rather than a map entry, so it is
2411        // replaced by an unarmed one, which is what a later `reschedule`
2412        // would have inserted in its place.
2413        self.timeouts.frontend = TimeoutContainer::new_empty(self.timeouts.frontend.duration());
2414        self.timeouts.backends.clear();
2415
2416        self.close_backend_connections(&proxy);
2417
2418        // The session is going away, so this is the last chance to settle the
2419        // ledger against the worker's registry. Everything the frontend close
2420        // and the loop above decided is performed here.
2421        self.apply_backend_deltas();
2422    }
2423
2424    /// Thin wrapper over `Mux::shutting_down_inner`: it drives frontend I/O
2425    /// and can change a core deadline, so it owes the wheel a reschedule on
2426    /// every exit.
2427    fn shutting_down(&mut self) -> SessionIsToBeClosed {
2428        let result = self.shutting_down_inner();
2429        self.apply_backend_deltas();
2430        self.reschedule();
2431        result
2432    }
2433}
2434
2435/// The bodies the `SessionState` wrappers above drive. They are inherent
2436/// methods rather than trait methods so the wrappers can own the one thing
2437/// every exit path needs: the [`Mux::reschedule`] that pushes the cores'
2438/// deadlines onto the timer wheel.
2439impl<Front: SocketHandler + std::fmt::Debug, L: ListenerHandler + L7ListenerHandler> Mux<Front, L> {
2440    /// Emit the access log of every stream still open with a request start,
2441    /// with the connection overhead shared among them, and recycle it.
2442    /// Called by `Mux::close` at teardown and by
2443    /// [`Self::release_streams_and_backends_for_error_linger`].
2444    fn log_and_recycle_open_streams(&mut self) {
2445        // Log active streams at session teardown for timeout diagnosis
2446        let active_count = self
2447            .context
2448            .streams
2449            .iter()
2450            .filter(|s| s.state.is_open() && s.metrics.start.is_some())
2451            .count();
2452        if active_count > 0 {
2453            debug!(
2454                "{} Session close with {} active stream(s)",
2455                log_context!(self),
2456                active_count
2457            );
2458            for (idx, stream) in self
2459                .context
2460                .streams
2461                .iter()
2462                .enumerate()
2463                .filter(|(_, s)| s.state.is_open() && s.metrics.start.is_some())
2464            {
2465                let elapsed = stream.metrics.service_time();
2466                debug!(
2467                    "{}   active stream[{}]: state={:?} service_time={:?} method={:?} path={:?} status={:?}",
2468                    log_context!(self),
2469                    idx,
2470                    stream.state,
2471                    elapsed,
2472                    stream.context.method,
2473                    stream.context.path,
2474                    stream.context.status,
2475                );
2476            }
2477            incr!(names::h2::CLOSE_WITH_ACTIVE_STREAMS);
2478        }
2479
2480        // Distribute H2 connection-level overhead (control frames) across in-flight
2481        // streams so that access log bytes_in/bytes_out reflect actual wire cost.
2482        // Integer division may lose up to (active_count - 1) bytes, which is acceptable.
2483        let active_count = active_count.max(1);
2484        let (total_overhead_in, total_overhead_out) = self.frontend.overhead_bytes();
2485        let share_in = total_overhead_in / active_count;
2486        let share_out = total_overhead_out / active_count;
2487
2488        // Generate access logs for in-flight streams on session teardown.
2489        // Skip streams that already had their access log emitted (metrics.start is
2490        // set to None by metrics.reset() after generate_access_log in the happy path).
2491        // Each connection's RTT is its one memoized sample (`memoized_rtt`):
2492        // asked for only for a stream that still owes a log, so the ordinary
2493        // close, where every stream has already been logged, reads nothing
2494        // (issue #1590), and a connection that logged before reads nothing
2495        // either.
2496        for stream in &mut self.context.streams {
2497            if stream.state.is_open() && stream.metrics.start.is_some() {
2498                let client_rtt = self.frontend.rtt();
2499                stream.metrics.bin += share_in;
2500                stream.metrics.bout += share_out;
2501                stream.metrics.service_stop();
2502                if stream.metrics.backend_stop.is_none() {
2503                    stream.metrics.backend_stop();
2504                }
2505                // Only mark as error if the stream had an actual protocol/processing failure
2506                // (kawa parse error, backend error). Normal timeouts, client disconnects,
2507                // and graceful connection closures are not errors.
2508                let is_error = stream.front.is_error() || stream.back.is_error();
2509                let server_rtt = stream
2510                    .linked_token()
2511                    .and_then(|token| self.router.backends.get(&token).and_then(Connection::rtt));
2512                for event in stream
2513                    .generate_access_log(
2514                        is_error,
2515                        Some("session close"),
2516                        self.context.listener.clone(),
2517                        client_rtt,
2518                        server_rtt,
2519                    )
2520                    .into_iter()
2521                    .flatten()
2522                {
2523                    crate::protocol::mux::h2::record_metric(event);
2524                }
2525                stream.state = StreamState::Recycle;
2526            }
2527        }
2528    }
2529
2530    /// Shut down, deregister from the session slab and account for every
2531    /// backend connection (`StreamsEnded`, `ConnectionClosed`, the backend
2532    /// gauges), then clear the backend-stream reverse index. The connections
2533    /// stay in `router.backends`: at teardown they drop with the `Mux`.
2534    fn close_backend_connections(&mut self, proxy: &Rc<RefCell<dyn L7Proxy>>) {
2535        for (token, client) in &mut self.router.backends {
2536            let proxy_borrow = proxy.borrow();
2537            let peer_closed = client.peer_closed();
2538            let socket = client.socket_mut();
2539            // No `EPOLL_CTL_DEL`: the socket stays in `router.backends` until
2540            // this `Mux` drops with its session, before the event loop's next
2541            // `epoll_wait`, and its last close removes it from the epoll set —
2542            // the same invariant as the front socket in `HttpsSession::close`
2543            // (`lib/src/https.rs`).
2544            // invariant: write-only shutdown — Shutdown::Both on a TLS frontend
2545            // discards the receive buffer and elicits TCP RST, truncating the
2546            // already-queued response. Canonical write-up: `HttpsSession::close`
2547            // (`lib/src/https.rs`). Backend sockets follow the same discipline for symmetry.
2548            // Skipped once the backend has closed: see `shutdown_write`.
2549            if let Err(e) = shutdown_write(socket, peer_closed)
2550                && e.kind() != ErrorKind::NotConnected
2551            {
2552                error!(
2553                    "{} error shutting down back socket({:?}): {:?}",
2554                    log_context_lite!(self),
2555                    socket,
2556                    e
2557                );
2558            }
2559            if !proxy_borrow.remove_session(*token) {
2560                error!(
2561                    "{} session {:?} was already removed!",
2562                    log_context_lite!(self),
2563                    token
2564                );
2565            }
2566
2567            match client.position() {
2568                Position::Client(cluster_id, backend, _) => {
2569                    // Session teardown iterates the backends map directly
2570                    // instead of routing through `Connection::close`, so it
2571                    // emits the same two accounting changes that path emits.
2572                    // Going through the ledger rather than mutating here is
2573                    // what lets the invariant-14 balance be read off one
2574                    // stream of deltas — see `Mux::apply_backend_deltas`.
2575                    let count = self
2576                        .context
2577                        .backend_streams
2578                        .get(token)
2579                        .map_or(0, |ids| ids.len());
2580                    let (slot, backend_id, metric_incarnation) = (
2581                        backend.slot(),
2582                        backend.backend_id.clone(),
2583                        backend.metric_incarnation(),
2584                    );
2585                    self.context.backend_deltas.push(BackendDelta {
2586                        slot,
2587                        change: BackendChange::StreamsEnded(count),
2588                    });
2589                    self.context.backend_deltas.push(BackendDelta {
2590                        slot,
2591                        change: BackendChange::ConnectionClosed,
2592                    });
2593                    gauge_add!(names::backend::CONNECTIONS, -1);
2594                    // Second `-1` site for `backend.pool.size` (the first is
2595                    // in `connection.rs::pre_close_client_bookkeeping`). This
2596                    // path runs during session teardown when the frontend
2597                    // session iterates the backends map directly without
2598                    // routing through `Connection::close`. Both `-1` sites
2599                    // mirror the single `+1` in router.rs::connect and the
2600                    // matching `backend.connections, -1` calls already
2601                    // present here, so symmetry follows from
2602                    // `backend.connections` correctness.
2603                    gauge_add!(names::backend::POOL_SIZE, -1);
2604                    gauge_add!(
2605                        names::backend::CONNECTIONS_PER_BACKEND,
2606                        -1,
2607                        Some(cluster_id),
2608                        Some(&backend_id),
2609                        metric_incarnation
2610                    );
2611                    trace!(
2612                        "{} connection (session) closed: {:?}",
2613                        log_context_lite!(self),
2614                        backend
2615                    );
2616                }
2617                Position::Server => {
2618                    error!(
2619                        "{} close_backend called on Server position",
2620                        log_context_lite!(self)
2621                    );
2622                }
2623            }
2624        }
2625        // Clear the reverse index after all backends have charged their
2626        // `StreamsEnded` deltas (whose counts come from that index).
2627        self.context.backend_streams.clear();
2628    }
2629
2630    /// Release the streams and backends of an H2 frontend that lingers after
2631    /// a final GOAWAY carrying an error code, in the pass that decides the
2632    /// linger rather than at its end, the order
2633    /// `ngx_http_v2_finalize_connection` follows. A linger still pending on
2634    /// its TLS `close_notify` counts as decided
2635    /// (`Connection::lingers_after_error_goaway`): the pass that queues the
2636    /// alert can end with the kernel refusing it and nothing re-armed, and
2637    /// then reach the `pending_links` pass with its streams still open.
2638    ///
2639    /// Such a GOAWAY can leave streams open (a flood, a protocol or flow
2640    /// control error, a SETTINGS timeout), unlike a final GOAWAY(NO_ERROR),
2641    /// which waits for the stream table to empty. None of them will be
2642    /// answered, so their access logs are emitted now with the same outcome
2643    /// the immediate close gave them before (`session close`), the slots
2644    /// recycled, and the backend connections shut down, accounted for and
2645    /// dropped, closing their sockets. With every stream recycled, nothing
2646    /// can be re-linked: the `pending_links` pass of `Mux::ready_inner`
2647    /// skips any stream not in `StreamState::Link`, and no backend is left
2648    /// to report a failure that would queue one. The recycled slots keep
2649    /// their buffers until the `Mux` drops, at the latest when the linger's
2650    /// short deadline (`shared::ERROR_GOAWAY_LINGER_TIMEOUT`) closes it.
2651    ///
2652    /// Idempotent: a second call finds nothing open and no backend. A
2653    /// graceful linger is left alone: its stream table is already empty.
2654    fn release_streams_and_backends_for_error_linger(&mut self, proxy: &Rc<RefCell<dyn L7Proxy>>) {
2655        if !self.frontend.lingers_after_error_goaway() {
2656            return;
2657        }
2658        // Pre: the `metrics.start` filter below cannot skip a stream that
2659        // could reach a backend. Every entry into `StreamState::Link` arms
2660        // the start first (`SessionMetrics::service_start`) or re-links a
2661        // stream that was already linked, and no `SessionMetrics::reset`
2662        // leaves a stream in either state.
2663        debug_assert!(
2664            !self.context.streams.iter().any(|s| {
2665                matches!(s.state, StreamState::Link | StreamState::Linked(_))
2666                    && s.metrics.start.is_none()
2667            }),
2668            "a stream queued for or linked to a backend carries its request start"
2669        );
2670        let has_open_stream = self
2671            .context
2672            .streams
2673            .iter()
2674            .any(|s| s.state.is_open() && s.metrics.start.is_some());
2675        if !has_open_stream && self.router.backends.is_empty() {
2676            return;
2677        }
2678        debug!(
2679            "{} H2 error GOAWAY linger: releasing {} backend connection(s) and the open streams",
2680            log_context!(self),
2681            self.router.backends.len()
2682        );
2683        self.log_and_recycle_open_streams();
2684        self.close_backend_connections(proxy);
2685        let tokens: Vec<Token> = (&self.router.backends)
2686            .into_iter()
2687            .map(|(token, _)| *token)
2688            .collect();
2689        for token in tokens {
2690            // Dropping the connection closes its socket, which leaves the
2691            // epoll set with its last close (see `Mux::close`).
2692            drop(self.router.backends.remove(&token));
2693        }
2694        self.timeouts.backends.clear();
2695        debug_assert!(
2696            self.router.backends.is_empty() && self.context.backend_streams.is_empty(),
2697            "an error-GOAWAY linger holds no backend connection"
2698        );
2699        debug_assert!(
2700            !self
2701                .context
2702                .streams
2703                .iter()
2704                .any(|s| matches!(s.state, StreamState::Link | StreamState::Linked(_))),
2705            "an error-GOAWAY linger keeps no stream that could reach a backend"
2706        );
2707    }
2708
2709    /// Fulfil a [`router::ConnectPlan::Dial`]: select a backend, dial it,
2710    /// build the connection, register it, and hand it to
2711    /// `Router::commit_dialed`.
2712    ///
2713    /// This is the embedder half of Question 6's split
2714    /// ([#1340](https://github.com/sozu-proxy/sozu/issues/1340)). Every step
2715    /// here reaches something the core has no business holding — the
2716    /// `BackendMap`, a `connect(2)`, `setsockopt`, the session slab, the mio
2717    /// registry — and the order is the one `Router::plan_connect` used, unchanged.
2718    ///
2719    /// The one side-effect taken before the dial is the reservation selection
2720    /// raises on the backend (`active_connections += 1`,
2721    /// `BackendMap::reserve_backend`), and this function releases it on
2722    /// every exit that returns `Err`: a failed `connect(2)` through
2723    /// `BackendChange::DialFailed`, which also records the backend's
2724    /// failure, and every later exit — all of [`Self::attach_dialed`]'s —
2725    /// through one `BackendChange::ConnectionClosed`, which records none,
2726    /// because there the backend did not fail (#1713).
2727    ///
2728    /// Taken as free functions over `&mut` fields rather than `&mut self` so
2729    /// the caller can keep its `&mut self.context` binding across the call.
2730    #[allow(clippy::too_many_arguments)]
2731    fn dial_backend(
2732        router: &mut Router,
2733        backend_registry: &mut BackendRegistry,
2734        backends: &RefCell<BackendMap>,
2735        stream_id: GlobalStreamId,
2736        context: &mut Context<L>,
2737        session: &Rc<RefCell<dyn ProxySession>>,
2738        proxy: &Rc<RefCell<dyn L7Proxy>>,
2739        // Owned: moved into the new connection's `Position::Client`, a handle
2740        // on the stream's routed id rather than a copy of it (#1583).
2741        cluster_id: sozu_command::state::ClusterId,
2742        h2: bool,
2743        frontend_should_stick: bool,
2744    ) -> Result<(), BackendConnectionError> {
2745        // One `borrow_mut` of the worker's `BackendMap` for exactly the
2746        // selection and its reservation, and no longer: nothing else on this
2747        // path borrows the map, and the `RefMut` drops before the dial and
2748        // before `add_session` and `register_socket` borrow the proxy below.
2749        let mut backend = {
2750            let mut backends = backends.borrow_mut();
2751            let stream = &mut context.streams[stream_id];
2752            let mut selector = RegistrySelector {
2753                backends: &mut backends,
2754                registry: backend_registry,
2755                now: context.now,
2756                exclude: &stream.tried_backends,
2757            };
2758            router.backend_from_request(
2759                &cluster_id,
2760                frontend_should_stick,
2761                &mut stream.context,
2762                &mut selector,
2763            )?
2764        };
2765        backend.set_metric_incarnation(
2766            context.streams[stream_id]
2767                .context
2768                .cluster_metrics_incarnation(),
2769        );
2770
2771        // Selection reserved a connection on `backend`; the dial is ours
2772        // (#1684). A `connect(2)` that fails releases that reservation and
2773        // records the failure through the ledger, which the drain before the
2774        // next selection applies — the backend is then exactly where a failed
2775        // `Backend::try_connect` used to leave it.
2776        let socket = match mio::net::TcpStream::connect(backend.address) {
2777            Ok(socket) => socket,
2778            Err(io_error) => {
2779                context.backend_deltas.push(BackendDelta {
2780                    slot: backend.slot(),
2781                    change: BackendChange::DialFailed(context.now),
2782                });
2783                context.streams[stream_id]
2784                    .tried_backends
2785                    .push(backend.address);
2786                // The count the backend will hold once the delta is applied,
2787                // as `Backend::try_connect`'s error reported it.
2788                let failures = backend_registry
2789                    .handle(&backend)
2790                    .map_or(1, |handle| handle.borrow().failures + 1);
2791                return Err(BackendConnectionError::Backend(
2792                    BackendError::ConnectionFailures {
2793                        cluster_id: cluster_id.to_string(),
2794                        backend_address: backend.address,
2795                        failures,
2796                        error: io_error.to_string(),
2797                    },
2798                ));
2799            }
2800        };
2801
2802        // Past the connect, the reservation belongs to the connection built
2803        // on `socket`. An exit that abandons it drops the connection
2804        // unregistered, and `Connection` has no `Drop` that would release
2805        // anything, so the release is taken here, once, for every such exit:
2806        // `Connection::new_h2_client` finding no buffer, a refused
2807        // `Connection::start_stream`, and the `register_socket` rollback
2808        // (#1713). It is `ConnectionClosed`, not `DialFailed`: each of those
2809        // is a local failure, and charging it to the backend's failure count
2810        // and backoff would steer traffic away from a healthy backend.
2811        let slot = backend.slot();
2812        let attached = Self::attach_dialed(
2813            router, stream_id, context, session, proxy, cluster_id, h2, backend, socket,
2814        );
2815        if attached.is_err() {
2816            context.backend_deltas.push(BackendDelta {
2817                slot,
2818                change: BackendChange::ConnectionClosed,
2819            });
2820            debug_assert!(
2821                !matches!(context.streams[stream_id].state, StreamState::Linked(_)),
2822                "an abandoned dial must not have linked its stream"
2823            );
2824        }
2825        attached
2826    }
2827
2828    /// Build, start and register the connection of a dial whose `connect(2)`
2829    /// succeeded, and hand it to `Router::commit_dialed`.
2830    ///
2831    /// SECURITY (CWE-400): every stateful side-effect (the
2832    /// `backend.connections` / `backend.pool.size` /
2833    /// `connections_per_backend` gauges, the slab session, the mio
2834    /// registration, the router-map entry, `stream.metrics.backend_start`) is
2835    /// deferred until AFTER both the `Connection` constructor and
2836    /// `start_stream` have succeeded. If either fails this returns `Err`
2837    /// without leaking a slab entry, an epoll registration, a gauge counter
2838    /// or a router-map entry. The `TcpStream` lives on the stack and is moved
2839    /// into the `Connection`; on failure the `Connection` — or the raw
2840    /// `TcpStream`, for the pool-exhaustion branch that drops inside
2841    /// `Connection::new_h2_client` — is dropped, closing the fd. No token is
2842    /// allocated before that point, so there is nothing to roll back. The
2843    /// backend's reservation is the caller's to release: every `Err` here
2844    /// drops the connection, and [`Self::dial_backend`] releases it once for
2845    /// all of them.
2846    #[allow(clippy::too_many_arguments)]
2847    fn attach_dialed(
2848        router: &mut Router,
2849        stream_id: GlobalStreamId,
2850        context: &mut Context<L>,
2851        session: &Rc<RefCell<dyn ProxySession>>,
2852        proxy: &Rc<RefCell<dyn L7Proxy>>,
2853        cluster_id: sozu_command::state::ClusterId,
2854        h2: bool,
2855        backend: BackendId,
2856        socket: mio::net::TcpStream,
2857    ) -> Result<(), BackendConnectionError> {
2858        if let Err(e) = socket.set_nodelay(true) {
2859            error!(
2860                "{} error setting nodelay on back socket({:?}): {:?}",
2861                log_module_context!(context.http_context(stream_id)),
2862                socket,
2863                e
2864            );
2865        }
2866
2867        // Cache the backend's configured address so SOCKET log lines fired on
2868        // ECONNREFUSED (or any failed async `connect()`) can still render
2869        // `peer=<backend>` — `getpeername(2)` returns ENOTCONN in that state,
2870        // so the live lookup path would show `peer=None` exactly when the
2871        // operator needs the backend id.
2872        let backend_peer = Some(backend.address);
2873        let socket = SessionTcpStream::new(socket, context.session_ulid, backend_peer);
2874
2875        let flood_config = context.listener.borrow().get_h2_flood_config();
2876        let connection_config = context.listener.borrow().get_h2_connection_config();
2877        let stream_idle_timeout = context.listener.borrow().get_h2_stream_idle_timeout();
2878        let graceful_shutdown_deadline = context
2879            .listener
2880            .borrow()
2881            .get_h2_graceful_shutdown_deadline();
2882        let backend_id_for_gauge = Rc::clone(&backend.backend_id);
2883        let metric_incarnation = backend.metric_incarnation();
2884        let mut connection = if h2 {
2885            match Connection::new_h2_client(
2886                context.session_ulid,
2887                socket,
2888                cluster_id,
2889                backend,
2890                &mut *context.buffers,
2891                router.configured_connect_timeout,
2892                flood_config,
2893                connection_config,
2894                stream_idle_timeout,
2895                graceful_shutdown_deadline,
2896            ) {
2897                Some(connection) => connection,
2898                // Pool exhaustion: the socket was already dropped by
2899                // `new_h2_client` and no side-effect has been committed.
2900                None => return Err(BackendConnectionError::MaxBuffers),
2901            }
2902        } else {
2903            Connection::new_h1_client(
2904                context.session_ulid,
2905                socket,
2906                cluster_id,
2907                backend,
2908                router.configured_connect_timeout,
2909            )
2910        };
2911
2912        // Check the backend can accept a new stream BEFORE committing any
2913        // registry state. `start_stream` records its `StreamsStarted` delta
2914        // only once the start has succeeded, so a refusal leaves the backend
2915        // accounting untouched rather than needing a rollback.
2916        if !connection.start_stream(stream_id, context) {
2917            error!(
2918                "{} Backend rejected stream start (max concurrent streams reached)",
2919                log_module_context!(context.http_context(stream_id))
2920            );
2921            // `connection` (socket + pending timeout deadline) drops here; no
2922            // wheel entry was ever armed for it.
2923            return Err(BackendConnectionError::MaxSessionsMemory);
2924        }
2925
2926        // --- Happy path: commit side-effects in one atomic-ish block ---
2927        let stream = &mut context.streams[stream_id];
2928        stream.metrics.backend_start();
2929        stream.metrics.backend_id = stream.context.backend_id.clone();
2930        gauge_add!(names::backend::CONNECTIONS, 1);
2931        // `backend.pool.size` mirrors `backend.connections` exactly: one entry
2932        // per `Router::backends` token. The `-1` partners live in
2933        // `connection.rs::pre_close_client_bookkeeping` (graceful close) and
2934        // in `Mux::close` (session teardown). Symmetric pairing with both
2935        // decrement sites is the only defence against the gauge underflow
2936        // class of bug fixed by ff401b54 / aadb3fa4.
2937        gauge_add!(names::backend::POOL_SIZE, 1);
2938        // The cluster id moved into the connection; the stream's own copy,
2939        // stored by `Router::plan_connect`, names the same cluster.
2940        gauge_add!(
2941            names::backend::CONNECTIONS_PER_BACKEND,
2942            1,
2943            stream.context.cluster_id.as_deref(),
2944            Some(&backend_id_for_gauge),
2945            metric_incarnation
2946        );
2947
2948        let token = proxy.borrow().add_session(session.clone());
2949
2950        {
2951            let socket_ref = connection.socket_mut();
2952            if let Err(e) = proxy.borrow().register_socket(
2953                socket_ref,
2954                token,
2955                Interest::READABLE | Interest::WRITABLE,
2956            ) {
2957                // SECURITY (CWE-400): treat mio registration failure as a hard
2958                // connect failure. Without this rollback the gauges
2959                // (`backend.connections`, `backend.pool.size`,
2960                // `connections_per_backend`) and the slab session leak until
2961                // the connect timeout fires. Under fd pressure
2962                // (EMFILE/ENFILE) this can occur in tight bursts and poison
2963                // capacity dashboards.
2964                error!(
2965                    "{} error registering back socket: {:?} — rolling back",
2966                    log_module_context!(context.http_context(stream_id)),
2967                    e
2968                );
2969                // Undo the gauge increments committed above.
2970                gauge_add!(names::backend::CONNECTIONS, -1);
2971                gauge_add!(names::backend::POOL_SIZE, -1);
2972                gauge_add!(
2973                    names::backend::CONNECTIONS_PER_BACKEND,
2974                    -1,
2975                    context.http_context(stream_id).cluster_id.as_deref(),
2976                    Some(&backend_id_for_gauge),
2977                    metric_incarnation
2978                );
2979                // Release the `active_requests` charge `start_stream` took.
2980                //
2981                // The comment this replaces claimed the drop released it "via
2982                // the regular session drop path
2983                // (`pre_close_client_bookkeeping`)". That was never true:
2984                // `Connection` has no `impl Drop`, so dropping it runs no
2985                // bookkeeping at all. It was harmless only because the charge
2986                // is guarded on `BackendStatus::Connected` and a
2987                // freshly-dialled connection is `Connecting`, so there was
2988                // nothing to release. Emitting the release explicitly, under
2989                // the same guard the charge uses, makes the pair hold however
2990                // the status is reached rather than by accident.
2991                connection.release_start_stream_charge(context);
2992                proxy.borrow().remove_session(token);
2993                return Err(BackendConnectionError::MaxSessionsMemory);
2994            }
2995        }
2996
2997        router.commit_dialed(stream_id, context, token, connection);
2998        Ok(())
2999    }
3000
3001    fn ready_inner(
3002        &mut self,
3003        session: Rc<RefCell<dyn ProxySession>>,
3004        proxy: Rc<RefCell<dyn L7Proxy>>,
3005        _metrics: &mut SessionMetrics,
3006    ) -> SessionResult {
3007        let mut counter = 0;
3008
3009        // A linger decided outside this function (a timeout, a soft stop)
3010        // releases at the next pass; see the call after the frontend write.
3011        self.release_streams_and_backends_for_error_linger(&proxy);
3012
3013        if self.frontend.is_lingering() && self.frontend.readiness().event.is_hup() {
3014            // A lingering frontend reads to the client's EOF before it closes:
3015            // a close with its last bytes unread would still reset.
3016            self.frontend.readiness_mut().event.insert(Ready::READABLE);
3017        } else if self.frontend.readiness().event.is_hup() && self.frontend_exchange_in_flight() {
3018            // A half-closed client still gets the response to what it sent:
3019            // neither close nor queue `close_notify` while it is in flight.
3020            // What is already pending is flushed as a delayed close would.
3021            if self
3022                .frontend
3023                .has_pending_write_including_streams(&self.context)
3024            {
3025                self.frontend.readiness_mut().arm_writable();
3026            }
3027        } else if self.frontend.readiness().event.is_hup()
3028            && !self.delay_close_for_frontend_flush("frontend HUP")
3029        {
3030            debug!(
3031                "{} Mux closing on frontend HUP: {:?}",
3032                log_context!(self),
3033                self.frontend
3034            );
3035            return SessionResult::Close;
3036        }
3037
3038        // Start service timers on all active streams after the HUP check.
3039        // This mirrors session-level service_start/service_stop in Http(s)Session::ready()
3040        // to measure only CPU processing time, excluding epoll wait between cycles.
3041        for stream in &mut self.context.streams {
3042            if stream.state.is_open() {
3043                stream.metrics.service_start();
3044            }
3045        }
3046
3047        let start = Instant::now();
3048        self.context.debug.push(DebugEvent::ReadyTimestamp(
3049            std::time::SystemTime::now()
3050                .duration_since(std::time::UNIX_EPOCH)
3051                .unwrap_or_default()
3052                .as_millis() as usize,
3053        ));
3054        trace!("{} {:?}", log_context!(self), start);
3055        // Set once the inner loop has spent its budget and waits for the
3056        // client. The outer loop then re-enters the inner one at most once
3057        // more, for a stream this sweep just linked (an attached pooled
3058        // backend is armed by a synthetic event no epoll edge repeats), and
3059        // that iteration stops at the spent budget without counting or
3060        // logging the same limit again.
3061        let mut budget_spent = false;
3062        let mut budget_extra_pass = false;
3063        loop {
3064            // The mux's clock sample for this pass. Everything time-based
3065            // below — flood and back-pressure windows, the SETTINGS-ACK
3066            // deadline, per-stream liveness and flow-control-stall deadlines
3067            // — reads `context.now` (mirrored into `ConnectionH2::now` at
3068            // each entry point) rather than calling `Instant::now()` itself.
3069            //
3070            // Sampled per OUTER iteration so the inner loop DELIBERATELY
3071            // shares one instant: that is the property the snapshot exists
3072            // for — every frame processed in one inner sweep is weighed
3073            // against the same "now", so a burst cannot decay a rate window
3074            // out from under itself mid-sweep. It is not a bound on how long
3075            // the sweep may take: `MAX_LOOP_ITERATIONS` is a count, and a
3076            // count bounds iterations, not wall clock. `counter` is declared
3077            // above BOTH loops, so that budget is shared across every outer
3078            // iteration of one `ready()` call rather than reset per pass.
3079            // `start` above stays a separate sample so the trace reports the
3080            // true entry instant.
3081            self.context.now = Instant::now();
3082            self.context.now_wall_ms = unix_epoch_ms();
3083            self.context.debug.push(DebugEvent::LoopStart);
3084            loop {
3085                self.context.debug.push(DebugEvent::LoopIteration(counter));
3086                if self.frontend.readiness().filter_interest().is_readable() {
3087                    let res = {
3088                        let context = &mut self.context;
3089                        let res = self
3090                            .frontend
3091                            .readable(context, EndpointClient(&mut self.router));
3092                        context.debug.push(DebugEvent::SR(
3093                            self.frontend_token,
3094                            res,
3095                            self.frontend.readiness().clone(),
3096                        ));
3097                        res
3098                    };
3099                    match res {
3100                        MuxResult::Continue => {}
3101                        MuxResult::CloseSession => {
3102                            if !self.delay_close_for_frontend_flush("frontend readable") {
3103                                debug!(
3104                                    "{} Mux close from frontend readable: {:?}",
3105                                    log_context!(self),
3106                                    self.frontend
3107                                );
3108                                return SessionResult::Close;
3109                            }
3110                        }
3111                        MuxResult::Upgrade => {
3112                            self.sync_upgrade_buffers();
3113                            return SessionResult::Upgrade;
3114                        }
3115                    }
3116                }
3117
3118                let mut all_backends_readiness_are_empty = true;
3119                self.router.dead_backends.clear();
3120                let mut backend_close: Option<(&'static str, Token)> = None;
3121                for (token, client) in self.router.backends.iter_mut() {
3122                    // A WRITABLE on a connecting socket is not proof it
3123                    // connected: a dial that failed can surface as WRITABLE
3124                    // alone, its error in `SO_ERROR`, and sozu raises
3125                    // WRITABLE itself to flush queued output, as an HTTP/2
3126                    // backend's preface, while the dial is still in flight.
3127                    // Read the outcome before the WRITABLE arm below would
3128                    // mark the connection established: a failure takes the
3129                    // same connect-failure branch as a refused dial, a dial
3130                    // still in flight waits for the kernel's completion edge
3131                    // (sozu-proxy/sozu#1800).
3132                    if client.readiness().event.is_writable()
3133                        && matches!(
3134                            client.position(),
3135                            Position::Client(_, _, BackendStatus::Connecting(_))
3136                        )
3137                    {
3138                        match connect_outcome(client.socket()) {
3139                            ConnectOutcome::Connected => {}
3140                            ConnectOutcome::Pending => {
3141                                client.readiness_mut().event.remove(Ready::WRITABLE);
3142                            }
3143                            ConnectOutcome::Failed => {
3144                                client.readiness_mut().event.insert(Ready::ERROR);
3145                            }
3146                        }
3147                    }
3148                    let readiness = client.readiness_mut();
3149                    // Check the raw event for HUP/ERROR, not filter_interest():
3150                    // the event is what the poller reported, whatever the
3151                    // interest holds.
3152                    let dead = readiness.event.is_hup() || readiness.event.is_error();
3153                    if dead {
3154                        trace!(
3155                            "{} Backend({:?}) -> {:?}",
3156                            log_context_lite!(self),
3157                            token,
3158                            readiness
3159                        );
3160                        readiness.event.remove(Ready::WRITABLE);
3161                    }
3162
3163                    if client.readiness().filter_interest().is_writable() {
3164                        let position = client.position_mut();
3165                        match position {
3166                            Position::Client(
3167                                cluster_id,
3168                                backend,
3169                                BackendStatus::Connecting(start),
3170                            ) => {
3171                                #[cfg(debug_assertions)]
3172                                self.context
3173                                    .debug
3174                                    .push(DebugEvent::CCS(*token, cluster_id.clone()));
3175
3176                                // Health and latency are not invariant 14 and
3177                                // the load balancer READS `retry_policy`, so
3178                                // these three writes stay immediate, against
3179                                // the handle the slot resolves to. Only the
3180                                // `active_requests` charge below is a delta.
3181                                let Some(handle) = self.backend_registry.get(backend.slot()) else {
3182                                    error!(
3183                                        "{} connected backend names a slot this session never                                          minted: {:?}",
3184                                        log_context_lite!(self),
3185                                        backend
3186                                    );
3187                                    continue;
3188                                };
3189                                let mut backend_borrow = handle.borrow_mut();
3190                                if backend_borrow.retry_policy.is_down() {
3191                                    info!(
3192                                        "{} backend server {} at {} is up",
3193                                        log_context_lite!(self),
3194                                        backend.backend_id,
3195                                        backend.address
3196                                    );
3197                                    incr!(
3198                                        names::backend::UP,
3199                                        Some(cluster_id),
3200                                        Some(&backend.backend_id),
3201                                        backend.metric_incarnation()
3202                                    );
3203                                    gauge!(
3204                                        names::backend::AVAILABLE,
3205                                        1,
3206                                        Some(cluster_id),
3207                                        Some(&backend.backend_id),
3208                                        backend.metric_incarnation()
3209                                    );
3210                                    push_event(Event {
3211                                        kind: EventKind::BackendUp as i32,
3212                                        backend_id: Some(backend.backend_id.to_string()),
3213                                        address: Some(backend.address.into()),
3214                                        cluster_id: Some(cluster_id.to_string()),
3215                                        metric_detail: None,
3216                                    });
3217                                }
3218
3219                                //successful connection, reset failure counter
3220                                backend_borrow.failures = 0;
3221                                backend_borrow
3222                                    .set_connection_time(start.elapsed(), self.context.now);
3223                                backend_borrow.retry_policy.succeed(self.context.now);
3224                                drop(backend_borrow);
3225
3226                                // These streams linked while the connection
3227                                // was still `Connecting`, so
3228                                // `Connection::start_stream`'s `Connected`
3229                                // guard skipped their charge. This is where
3230                                // every first stream of a fresh dial is
3231                                // charged — which is why the ledger cannot be
3232                                // core-only and still balance.
3233                                let mut started = 0;
3234                                if let Some(ids) = self.context.backend_streams.get(token) {
3235                                    started = ids.len();
3236                                    for &stream_id in ids.iter() {
3237                                        self.context.streams[stream_id].metrics.backend_connected();
3238                                    }
3239                                }
3240                                if started > 0 {
3241                                    let slot = backend.slot();
3242                                    self.context.backend_deltas.push(BackendDelta {
3243                                        slot,
3244                                        change: BackendChange::StreamsStarted(started),
3245                                    });
3246                                }
3247                                trace!(
3248                                    "{} connection success: {:?}",
3249                                    log_context_lite!(self),
3250                                    backend
3251                                );
3252                                *position = Position::Client(
3253                                    std::mem::take(cluster_id),
3254                                    backend.clone(),
3255                                    BackendStatus::Connected,
3256                                );
3257                                client.set_timeout_duration(
3258                                    self.router.configured_backend_timeout,
3259                                    self.context.now,
3260                                );
3261                            }
3262                            Position::Client(..) => {}
3263                            Position::Server => {
3264                                error!(
3265                                    "{} backend connection cannot be in Server position",
3266                                    log_context_lite!(self)
3267                                );
3268                            }
3269                        }
3270                        let res = {
3271                            let context = &mut self.context;
3272                            let res = client.writable(context, EndpointServer(&mut self.frontend));
3273                            context.debug.push(DebugEvent::CW(
3274                                *token,
3275                                res,
3276                                client.readiness().clone(),
3277                            ));
3278                            res
3279                        };
3280                        match res {
3281                            MuxResult::Continue => {}
3282                            MuxResult::Upgrade => {
3283                                error!(
3284                                    "{} only frontend connections can trigger Upgrade",
3285                                    log_context_lite!(self)
3286                                );
3287                            }
3288                            MuxResult::CloseSession => {
3289                                backend_close = Some(("backend writable", *token));
3290                                break;
3291                            }
3292                        }
3293                        // Cross-readiness: backend wrote → wake frontend reader
3294                        let context = &mut self.context;
3295                        self.frontend.try_resume_reading(context);
3296                    }
3297
3298                    if client.readiness().filter_interest().is_readable() {
3299                        let res = {
3300                            let context = &mut self.context;
3301                            let res = client.readable(context, EndpointServer(&mut self.frontend));
3302                            context.debug.push(DebugEvent::CR(
3303                                *token,
3304                                res,
3305                                client.readiness().clone(),
3306                            ));
3307                            res
3308                        };
3309                        match res {
3310                            MuxResult::Continue => {}
3311                            MuxResult::Upgrade => {
3312                                error!(
3313                                    "{} only frontend connections can trigger Upgrade (readable)",
3314                                    log_context_lite!(self)
3315                                );
3316                            }
3317                            MuxResult::CloseSession => {
3318                                backend_close = Some(("backend readable", *token));
3319                                break;
3320                            }
3321                        }
3322                    }
3323
3324                    if dead
3325                        && !client.readiness().filter_interest().is_readable()
3326                        && !client.has_buffer_pressure(&self.context)
3327                    {
3328                        self.context
3329                            .debug
3330                            .push(DebugEvent::CH(*token, client.readiness().clone()));
3331                        trace!("{} Closing {:#?}", log_context_lite!(self), client);
3332                        match client.position() {
3333                            Position::Client(cluster_id, backend, BackendStatus::Connecting(_)) => {
3334                                // A dial that never completed charged nothing
3335                                // to `active_requests`, so this arm touches
3336                                // only the health counters — not the ledger.
3337                                let Some(handle) = self.backend_registry.get(backend.slot()) else {
3338                                    error!(
3339                                        "{} failed backend names a slot this session never                                          minted: {:?}",
3340                                        log_context_lite!(self),
3341                                        backend
3342                                    );
3343                                    continue;
3344                                };
3345                                let mut backend_borrow = handle.borrow_mut();
3346                                backend_borrow.failures += 1;
3347
3348                                let already_unavailable = backend_borrow.retry_policy.is_down();
3349                                // The jitter comes from the worker's backend
3350                                // map, the one generator selection draws
3351                                // from; nothing else borrows the map here.
3352                                backend_borrow
3353                                    .retry_policy
3354                                    .fail(self.context.now, self.backends.borrow_mut().rng());
3355                                incr!(
3356                                    names::backend::CONNECTIONS_ERROR,
3357                                    Some(cluster_id),
3358                                    Some(&backend.backend_id),
3359                                    backend.metric_incarnation()
3360                                );
3361                                if !already_unavailable && backend_borrow.retry_policy.is_down() {
3362                                    error!(
3363                                        "{} backend server {} at {} is down",
3364                                        log_context_lite!(self),
3365                                        backend.backend_id,
3366                                        backend.address
3367                                    );
3368                                    incr!(
3369                                        names::backend::DOWN,
3370                                        Some(cluster_id),
3371                                        Some(&backend.backend_id),
3372                                        backend.metric_incarnation()
3373                                    );
3374                                    gauge!(
3375                                        names::backend::AVAILABLE,
3376                                        0,
3377                                        Some(cluster_id),
3378                                        Some(&backend.backend_id),
3379                                        backend.metric_incarnation()
3380                                    );
3381                                    push_event(Event {
3382                                        kind: EventKind::BackendDown as i32,
3383                                        backend_id: Some(backend.backend_id.to_string()),
3384                                        address: Some(backend.address.into()),
3385                                        cluster_id: Some(cluster_id.to_string()),
3386                                        metric_detail: None,
3387                                    });
3388                                }
3389                                drop(backend_borrow);
3390                                // Every stream this dial carried failed to
3391                                // reach `backend`: a retry of any of them
3392                                // prefers another one (sozu-proxy/sozu#1800).
3393                                if let Some(ids) = self.context.backend_streams.get(token) {
3394                                    for &stream_id in ids.iter() {
3395                                        self.context.streams[stream_id]
3396                                            .tried_backends
3397                                            .push(backend.address);
3398                                    }
3399                                }
3400                                trace!(
3401                                    "{} connection fail: {:?}",
3402                                    log_context_lite!(self),
3403                                    backend
3404                                );
3405                            }
3406                            Position::Client(_, backend, _) => {
3407                                // A backend that dies past `Connecting` still
3408                                // owes the charge every stream it carries
3409                                // took — the same `StreamsEnded(count)` the
3410                                // teardown path emits.
3411                                let count = self
3412                                    .context
3413                                    .backend_streams
3414                                    .get(token)
3415                                    .map_or(0, |ids| ids.len());
3416                                self.context.backend_deltas.push(BackendDelta {
3417                                    slot: backend.slot(),
3418                                    change: BackendChange::StreamsEnded(count),
3419                                });
3420                            }
3421                            Position::Server => {
3422                                error!(
3423                                    "{} dead backend cannot be in Server position",
3424                                    log_context_lite!(self)
3425                                );
3426                            }
3427                        }
3428                        client.close(&mut self.context, EndpointServer(&mut self.frontend));
3429                        self.router.dead_backends.push(*token);
3430                    }
3431
3432                    if client.has_loop_work(&self.context) {
3433                        all_backends_readiness_are_empty = false;
3434                    }
3435                }
3436                // Remove dead backends from the map BEFORE handling
3437                // backend_close. client.close() already decremented
3438                // connections_per_backend / backend.connections gauges in
3439                // the loop above; if we return SessionResult::Close before
3440                // removing them, Mux::close() would decrement again
3441                // (double-decrement → gauge underflow).
3442                if !self.router.dead_backends.is_empty() {
3443                    for token in &self.router.dead_backends {
3444                        let proxy_borrow = proxy.borrow();
3445                        // The reverse-index entry `remove_backend_stream`
3446                        // kept for its capacity leaves with its connection.
3447                        // Only an emptied one: an entry still naming a stream
3448                        // is the drift the debug check at the end of this
3449                        // pass reports, and removing it here would hide it.
3450                        if self
3451                            .context
3452                            .backend_streams
3453                            .get(token)
3454                            .is_some_and(|ids| ids.is_empty())
3455                        {
3456                            self.context.backend_streams.remove(token);
3457                        }
3458                        if let Some(mut client) = self.router.backends.remove(token) {
3459                            // No explicit timer cancel: the token has left
3460                            // `router.backends`, so `Mux::reschedule` drops its
3461                            // handle on the way out of `ready` and
3462                            // `TimeoutContainer::drop` cancels the entry.
3463                            // No `EPOLL_CTL_DEL`: `client` drops at the end of
3464                            // this block, and closing its socket removes it from
3465                            // the epoll set — see `HttpsSession::close`
3466                            // (`lib/src/https.rs`).
3467                            let peer_closed = client.peer_closed();
3468                            let socket = client.socket_mut();
3469                            // invariant: write-only shutdown — Shutdown::Both on a TLS frontend
3470                            // discards the receive buffer and elicits TCP RST, truncating the
3471                            // already-queued response. Canonical write-up: `HttpsSession::close`
3472                            // (`lib/src/https.rs`). Backend sockets follow the same discipline for symmetry.
3473                            // Skipped once the backend has closed: see `shutdown_write`.
3474                            if let Err(e) = shutdown_write(socket, peer_closed)
3475                                && e.kind() != ErrorKind::NotConnected
3476                            {
3477                                error!(
3478                                    "{} error shutting down back socket({:?}): {:?}",
3479                                    log_context!(self),
3480                                    socket,
3481                                    e
3482                                );
3483                            }
3484                        } else {
3485                            error!("{} session {:?} has no backend!", log_context!(self), token);
3486                        }
3487                        if !proxy_borrow.remove_session(*token) {
3488                            error!(
3489                                "{} session {:?} was already removed!",
3490                                log_context!(self),
3491                                token
3492                            );
3493                        }
3494                    }
3495                    trace!("{} FRONTEND: {:#?}", log_context!(self), self.frontend);
3496                    trace!(
3497                        "{} BACKENDS: {:#?}",
3498                        log_context!(self),
3499                        self.router.backends
3500                    );
3501                }
3502                if let Some((reason, token)) = backend_close {
3503                    if !self.delay_close_for_frontend_flush(reason) {
3504                        debug!(
3505                            "{} Mux close from {} token={:?}: frontend={:?}",
3506                            log_context!(self),
3507                            reason,
3508                            token,
3509                            self.frontend
3510                        );
3511                        return SessionResult::Close;
3512                    }
3513                    all_backends_readiness_are_empty = false;
3514                }
3515
3516                if self.frontend.readiness().filter_interest().is_writable() {
3517                    let res = {
3518                        let context = &mut self.context;
3519                        let res = self
3520                            .frontend
3521                            .writable(context, EndpointClient(&mut self.router));
3522                        context.debug.push(DebugEvent::SW(
3523                            self.frontend_token,
3524                            res,
3525                            self.frontend.readiness().clone(),
3526                        ));
3527                        res
3528                    };
3529                    match res {
3530                        MuxResult::Continue => {}
3531                        MuxResult::CloseSession => {
3532                            if !self.delay_close_for_frontend_flush("frontend writable") {
3533                                debug!(
3534                                    "{} Mux close from frontend writable: {:?}",
3535                                    log_context!(self),
3536                                    self.frontend
3537                                );
3538                                return SessionResult::Close;
3539                            }
3540                        }
3541                        MuxResult::Upgrade => {
3542                            self.sync_upgrade_buffers();
3543                            return SessionResult::Upgrade;
3544                        }
3545                    }
3546                    // The pass that decides an error-GOAWAY linger, pending
3547                    // on `close_notify` or already draining, releases its
3548                    // streams and backends before anything below can touch
3549                    // them, `pending_links` included.
3550                    self.release_streams_and_backends_for_error_linger(&proxy);
3551                    // Cross-readiness: the frontend write can hand work back to
3552                    // a backend — a parked one resumes once its peer drained
3553                    // buffer space, and forwarding an interim 1xx re-arms the
3554                    // backend read so the response bytes already buffered
3555                    // behind it get parsed (`ConnectionH1::writable`'s interim
3556                    // arms, `ConnectionH2::handle_1xx_reset`). The
3557                    // `all_backends_readiness_are_empty` flag was recorded
3558                    // before this write, so it is refreshed from every
3559                    // backend's readiness now: a stale `true` would exit the
3560                    // loop with a backend armed and no socket event left to
3561                    // bring it back.
3562                    let context = &mut self.context;
3563                    for backend in self.router.backends.values_mut() {
3564                        if backend.try_resume_reading(context) || backend.has_loop_work(context) {
3565                            all_backends_readiness_are_empty = false;
3566                        }
3567                    }
3568                }
3569
3570                // A pooled connection whose backend left the configuration
3571                // is never reused (`Router::decide_after_gate`); once it
3572                // carries no stream it has nothing left to do, so drop it
3573                // rather than leave it idle until the session ends. The HUP
3574                // `force_disconnect` raises sends it through the dead-backend
3575                // sweep above on the next iteration, which releases its
3576                // connection on the backend and lets a `Closing` backend
3577                // reach `Closed`.
3578                for (token, backend) in self.router.backends.iter_mut() {
3579                    let retired = backend
3580                        .idle_pooled_backend()
3581                        .is_some_and(|id| self.backend_registry.is_retired(id))
3582                        && self
3583                            .context
3584                            .backend_streams
3585                            .get(token)
3586                            .is_none_or(|ids| ids.is_empty());
3587                    if retired {
3588                        debug!(
3589                            "{} closing idle backend connection {:?}: its backend was removed",
3590                            log_context_lite!(self),
3591                            token
3592                        );
3593                        backend.force_disconnect();
3594                        all_backends_readiness_are_empty = false;
3595                    }
3596                }
3597
3598                // A frontend HUP is handled here as it is on entry: nothing
3599                // else in this loop acts on one. Once no output is left to
3600                // flush, this check closes the session, including after a
3601                // flush the entry check delayed the close for and the
3602                // writable arm above completed. A lingering frontend is left
3603                // to its drain: `ConnectionH1::start_linger` raises READABLE,
3604                // which `Mux::frontend_exchange_in_flight` counts as unread
3605                // input until the drain reads the client's EOF, so the
3606                // client's last bytes are never left unread to reset the
3607                // connection. While output is
3608                // pending, the HUP alone is not work this loop can progress:
3609                // it stops when the frontend has nothing to read or write,
3610                // and the next readiness event resumes the flush. Counting
3611                // the HUP as work spun the loop to `MAX_LOOP_ITERATIONS`
3612                // after every client hang-up. A HUP that comes from a
3613                // half-close leaves the exchange in flight to be served
3614                // (`Mux::frontend_exchange_in_flight`): the session closes
3615                // here once its response is complete.
3616                if self.frontend.readiness().event.is_hup()
3617                    && !self
3618                        .frontend
3619                        .has_pending_write_including_streams(&self.context)
3620                    && !self.frontend_exchange_in_flight()
3621                    && !self.delay_close_for_frontend_flush("frontend HUP")
3622                {
3623                    debug!(
3624                        "{} Mux closing on frontend HUP: {:?}",
3625                        log_context!(self),
3626                        self.frontend
3627                    );
3628                    return SessionResult::Close;
3629                }
3630                let frontend_work = self.frontend.readiness().filter_interest();
3631                if !frontend_work.is_readable()
3632                    && !frontend_work.is_writable()
3633                    && !frontend_work.is_error()
3634                    && all_backends_readiness_are_empty
3635                {
3636                    break;
3637                }
3638
3639                counter += 1;
3640                if counter >= MAX_LOOP_ITERATIONS {
3641                    if budget_spent {
3642                        break;
3643                    }
3644                    if self.wait_for_client_at_loop_limit() {
3645                        budget_spent = true;
3646                        break;
3647                    }
3648                    return SessionResult::Close;
3649                }
3650            }
3651
3652            let context = &mut self.context;
3653            let mut dirty = false;
3654            while let Some(stream_id) = context.pending_links.pop_front() {
3655                let Some(stream) = context.streams.get(stream_id) else {
3656                    continue;
3657                };
3658                if stream.state != StreamState::Link {
3659                    continue;
3660                }
3661                // Before the first request triggers a stream Link, the frontend timeout is set
3662                // to a shorter request_timeout, here we switch to the longer nominal timeout.
3663                //
3664                // Only on a request's first link. A re-link — failover after a
3665                // failed dial, a stale-upstream replay — keeps the deadline armed
3666                // then, so `front_timeout` bounds the whole failover: re-arming it
3667                // on every attempt let a request spend `max_connection_attempts`
3668                // times `connect_timeout` before any answer. When it expires
3669                // during failover, the frontend-timeout branch of
3670                // `Mux::timeout_inner` answers as for any request still waiting on
3671                // a backend (sozu-proxy/sozu#1800).
3672                if stream.attempts == 0 {
3673                    self.frontend
3674                        .set_timeout_duration(self.configured_frontend_timeout, context.now);
3675                }
3676                let front_readiness = self.frontend.readiness_mut();
3677                dirty = true;
3678                // Settle the ledger before the ONLY reader of backend load
3679                // state on this path runs: the load balancer reached from
3680                // `Mux::dial_backend` below weighs `active_requests` and
3681                // `connection_time`. Draining here is what keeps a second
3682                // stream linking in the same pass seeing the first stream's
3683                // charge, exactly as it did when the charge was applied in
3684                // place. It is also what makes the view each selection reads
3685                // current: selection reserves `active_connections` itself
3686                // (`BackendMap::reserve_backend`), and a `connect(2)` that
3687                // failed in the previous iteration left a `DialFailed`
3688                // release on the ledger, applied here before the next
3689                // selection reads the counters (#1684).
3690                self.backend_registry
3691                    .apply_all(&mut context.backend_deltas, &self.backends);
3692                // Build the routing view once for this decision, from a single
3693                // `proxy.borrow()`. Holding one `Ref` for the call is what
3694                // gives every read inside it the same cluster map; the two
3695                // sites used to take separate borrows. A second immutable
3696                // borrow inside (`plan_connect`'s limit gate, and
3697                // `attach_dialed`'s `add_session` / `register_socket`) is fine
3698                // — nothing on this path borrows the proxy mutably.
3699                let proxy_ref = proxy.borrow();
3700                let backend_registry = &self.backend_registry;
3701                let retired = |backend: &BackendId| backend_registry.is_retired(backend);
3702                let max_connection_attempts = proxy_ref.sessions().borrow().max_connection_attempts;
3703                let view = router::RoutingView::new(proxy_ref.clusters(), proxy_ref.kind())
3704                    .with_backend_retired(&retired)
3705                    .with_max_connection_attempts(max_connection_attempts);
3706                match self
3707                    .router
3708                    .plan_connect(stream_id, context, &view)
3709                    .and_then(|step| match step {
3710                        router::ConnectStep::Decided(plan) => Ok(plan),
3711                        // The core resolved a cluster and paused: the
3712                        // per-(cluster, source-IP) gate is a consult AND a
3713                        // mutation of worker-global session state, so the core
3714                        // cannot perform it. Answer it here, then resume.
3715                        //
3716                        // The track must land BEFORE the decision resumes, not
3717                        // after: `cluster_ip_at_limit` is read by every other
3718                        // session on this worker and by `lib/src/tcp.rs`'s own
3719                        // gate, so a stream admitted but not yet counted is a
3720                        // slot a concurrent stream can take twice.
3721                        router::ConnectStep::CheckIpLimit(resume) => {
3722                            // `plan_connect` stored the routed cluster before
3723                            // it paused; an unset one is refused, not gated
3724                            // against an empty key.
3725                            let Some(cluster_id) =
3726                                context.http_context(stream_id).cluster_id.as_ref()
3727                            else {
3728                                return Err(BackendConnectionError::MaxSessionsMemory);
3729                            };
3730                            let verdict = consult_ip_gate(
3731                                &proxy.borrow().sessions(),
3732                                self.frontend_token,
3733                                cluster_id,
3734                                &resume,
3735                            );
3736                            self.router
3737                                .plan_connect_resume(stream_id, context, &view, resume, verdict)
3738                        }
3739                    })
3740                    .and_then(|plan| match plan {
3741                        router::ConnectPlan::Attached => Ok(()),
3742                        router::ConnectPlan::Dial {
3743                            cluster_id,
3744                            h2,
3745                            frontend_should_stick,
3746                        } => Self::dial_backend(
3747                            &mut self.router,
3748                            &mut self.backend_registry,
3749                            &self.backends,
3750                            stream_id,
3751                            context,
3752                            &session,
3753                            &proxy,
3754                            cluster_id,
3755                            h2,
3756                            frontend_should_stick,
3757                        ),
3758                    }) {
3759                    Ok(_) => {
3760                        let state = context.streams[stream_id].state;
3761                        context.debug.push(DebugEvent::CC(stream_id, state));
3762                    }
3763                    Err(error) => {
3764                        trace!("{} Connection error: {}", log_module_context!(), error);
3765                        let stream = &mut context.streams[stream_id];
3766                        // The registry this stream captured when its request
3767                        // arrived, not whatever the listener holds now.
3768                        let answers_rc = stream.answers.clone();
3769                        let answers = answers_rc.borrow();
3770                        use BackendConnectionError as BE;
3771                        match error {
3772                            BE::MaxConnectionRetries(_)
3773                            | BE::MaxSessionsMemory
3774                            | BE::MaxBuffers => {
3775                                warn!(
3776                                    "{} backend retry budget exhausted: {}",
3777                                    log_module_context!(stream.context),
3778                                    error
3779                                );
3780                                set_default_answer(stream, front_readiness, 503, &answers);
3781                            }
3782                            BE::Backend(BackendError::NoBackendForCluster(ref cluster_id)) => {
3783                                // A cluster that has backends, none of them
3784                                // selectable, is in an outage: its backends
3785                                // failed their health checks or the
3786                                // connections Sōzu opened to them. A cluster
3787                                // with no backend at all is a routing miss.
3788                                stream.context.backends_unavailable = self
3789                                    .backends
3790                                    .borrow()
3791                                    .backends
3792                                    .get(cluster_id.as_str())
3793                                    .is_some_and(|list| !list.backends.is_empty());
3794                                set_default_answer(stream, front_readiness, 503, &answers);
3795                            }
3796                            BE::RetrieveClusterError(RetrieveClusterError::RetrieveFrontend(
3797                                ref err,
3798                            )) => {
3799                                // RFC 9110 §15.5.1: a malformed authority is a
3800                                // 400. A syntactically valid authority that
3801                                // simply has no matching frontend stays on the
3802                                // historical 404 path.
3803                                let code = match err {
3804                                    FrontendFromRequestError::HostParse { .. }
3805                                    | FrontendFromRequestError::InvalidCharsAfterHost(_) => 400,
3806                                    FrontendFromRequestError::NoClusterFound(_) => 404,
3807                                };
3808                                set_default_answer(stream, front_readiness, code, &answers);
3809                            }
3810                            BE::RetrieveClusterError(RetrieveClusterError::UnauthorizedRoute) => {
3811                                set_default_answer(stream, front_readiness, 401, &answers);
3812                            }
3813                            BE::RetrieveClusterError(
3814                                RetrieveClusterError::SniAuthorityMismatch { .. },
3815                            ) => {
3816                                // RFC 9110 §15.5.20: 421 Misdirected Request is the
3817                                // semantically correct status for an authority that
3818                                // does not belong to this TLS connection. The
3819                                // http.sni_authority_mismatch metric emitted in
3820                                // `route_from_request` remains the durable signal;
3821                                // the 421 body here is what a client sees and may
3822                                // retry on a fresh TLS connection with a matching SNI.
3823                                set_default_answer(stream, front_readiness, 421, &answers);
3824                            }
3825                            BE::RetrieveClusterError(RetrieveClusterError::HttpsRedirect) => {
3826                                // Use the redirect status stashed by `Router::route_from_request`
3827                                // (#1009). Falls back to 301 for the legacy
3828                                // `cluster.https_redirect = true` path that does
3829                                // not set the field.
3830                                let code = stream.context.redirect_status.unwrap_or(301);
3831                                set_default_answer(stream, front_readiness, code, &answers);
3832                            }
3833
3834                            // `connect(2)` itself failed: the dial was refused
3835                            // before it could be registered. `Mux::dial_backend`
3836                            // recorded the failure and the address; like a dial
3837                            // refused later, the request goes to another
3838                            // backend while its attempts last, and
3839                            // `Router::plan_connect` answers 503 once they are
3840                            // spent (sozu-proxy/sozu#1800). The stream is still
3841                            // `Link`.
3842                            BE::Backend(BackendError::ConnectionFailures { ref error, .. }) => {
3843                                debug!(
3844                                    "{} backend connect failed, retrying: {}",
3845                                    log_module_context!(stream.context),
3846                                    error
3847                                );
3848                                debug_assert_eq!(stream.state, StreamState::Link);
3849                                context.pending_links.push_back(stream_id);
3850                            }
3851                            BE::Backend(ref e) => {
3852                                error!("{} backend connection error: {}", log_module_context!(), e);
3853                                set_default_answer(stream, front_readiness, 503, &answers);
3854                            }
3855                            BE::RetrieveClusterError(ref other) => {
3856                                error!(
3857                                    "{} unexpected RetrieveClusterError variant: {:?}",
3858                                    log_module_context!(),
3859                                    other
3860                                );
3861                                set_default_answer(stream, front_readiness, 503, &answers);
3862                            }
3863                            // TCP specific error
3864                            BE::NotFound(ref msg) => {
3865                                error!(
3866                                    "{} NotFound is TCP-specific, not reachable in mux: {:?}",
3867                                    log_module_context!(),
3868                                    msg
3869                                );
3870                                set_default_answer(stream, front_readiness, 503, &answers);
3871                            }
3872                            // Per-(cluster, source-IP) connection limit reached.
3873                            // Emit HTTP 429 with the resolved `Retry-After`. The
3874                            // value is computed in `Router::plan_connect` (where the
3875                            // SessionManager + cluster override are reachable)
3876                            // and stashed on the stream context just before the
3877                            // error is returned, so the answer engine can render
3878                            // (or elide) the header without re-deriving the
3879                            // resolution chain here.
3880                            BE::TooManyConnectionsPerIp { ref cluster_id } => {
3881                                debug!(
3882                                    "{} per-(cluster, source-IP) limit hit for cluster {:?}",
3883                                    log_module_context!(),
3884                                    cluster_id
3885                                );
3886                                let retry_after = stream.context.retry_after_seconds;
3887                                set_default_answer_with_retry_after(
3888                                    stream,
3889                                    front_readiness,
3890                                    429,
3891                                    &answers,
3892                                    retry_after,
3893                                );
3894                            }
3895                            // A stale-upstream replay whose preconditions no
3896                            // longer hold (sozu-proxy/sozu#1442). 502 is the
3897                            // answer this stream received before the replay
3898                            // existed, so refusing costs the client nothing it
3899                            // would not already have paid — and costs it far
3900                            // less than H1 wire bytes framed as an HTTP/2 DATA
3901                            // payload, or a second partial copy of the request
3902                            // trailing the first.
3903                            BE::ReplayRefused(reason) => {
3904                                warn!(
3905                                    "{} stale-upstream replay refused: {}",
3906                                    log_module_context!(stream.context),
3907                                    reason
3908                                );
3909                                set_default_answer(stream, front_readiness, 502, &answers);
3910                            }
3911                        }
3912                        context.debug.push(DebugEvent::CCF(stream_id, error));
3913                    }
3914                }
3915                // Every routing error arm sets a default answer, moving the
3916                // stream out of Link state, except a failed `connect(2)`,
3917                // which re-queues it above for another backend.
3918            }
3919            if !dirty || budget_extra_pass {
3920                break;
3921            }
3922            budget_extra_pass = budget_spent;
3923        }
3924
3925        // Stop service timers before yielding to epoll, so idle wait time is excluded
3926        // from the service_time metric. For Close/Upgrade returns, close() handles cleanup.
3927        for stream in &mut self.context.streams {
3928            if stream.state.is_open() {
3929                stream.metrics.service_stop();
3930            }
3931        }
3932
3933        #[cfg(debug_assertions)]
3934        {
3935            use std::collections::HashMap;
3936
3937            // Verify backend_streams index matches actual stream states.
3938            let mut expected: HashMap<Token, Vec<GlobalStreamId>> = HashMap::new();
3939            for (id, stream) in self.context.streams.iter().enumerate() {
3940                if let StreamState::Linked(token) = stream.state {
3941                    expected.entry(token).or_default().push(id);
3942                }
3943            }
3944            // An emptied entry is capacity kept for the next link on the same
3945            // connection (#1583), not a link: it is left out of the count,
3946            // and it must name a connection the router still holds, which is
3947            // what keeps the index bounded by the live backends.
3948            for (token, ids) in self.context.backend_streams.iter() {
3949                assert!(
3950                    !ids.is_empty() || self.router.backends.contains_key(token),
3951                    "backend_streams keeps an emptied entry for {token:?}, \
3952                     which is not a live backend connection"
3953                );
3954            }
3955            assert_eq!(
3956                expected.len(),
3957                self.context
3958                    .backend_streams
3959                    .values()
3960                    .filter(|ids| !ids.is_empty())
3961                    .count(),
3962                "backend_streams index key count mismatch: expected={:?}, actual={:?}",
3963                expected,
3964                self.context.backend_streams
3965            );
3966            for (token, mut expected_ids) in expected {
3967                let mut actual_ids = self
3968                    .context
3969                    .backend_streams
3970                    .get(&token)
3971                    .map_or_else(Vec::new, |ids| ids.to_vec());
3972                expected_ids.sort();
3973                actual_ids.sort();
3974                assert_eq!(
3975                    expected_ids, actual_ids,
3976                    "backend_streams index mismatch for token {token:?}",
3977                );
3978            }
3979        }
3980
3981        SessionResult::Continue
3982    }
3983
3984    fn timeout_inner(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult {
3985        trace!("{} MuxState::timeout({:?})", log_context!(self), token);
3986        // `timeout` runs outside `ready()`, so it is its own sampling point.
3987        // `cancel_timed_out_streams` below reaps on this snapshot.
3988        self.context.now = Instant::now();
3989        self.context.now_wall_ms = unix_epoch_ms();
3990        // Consume the wheel entry and re-validate it BEFORE the per-token
3991        // branches, so both of them share one gate. See
3992        // `Mux::consume_timer_entry`.
3993        if !self.consume_timer_entry(token) {
3994            trace!(
3995                "{} MuxState::timeout: early wheel delivery on {:?}, re-armed",
3996                log_context!(self),
3997                token
3998            );
3999            return StateResult::Continue;
4000        }
4001        let front_is_h2 = match self.frontend {
4002            Connection::H1(_) => false,
4003            Connection::H2(_) => true,
4004        };
4005        if self.frontend_token == token && self.frontend.is_lingering() {
4006            // The lingering close's deadline: stop draining and close.
4007            debug!(
4008                "{} Mux lingering close deadline reached: {:?}",
4009                log_context!(self),
4010                self.frontend
4011            );
4012            return StateResult::CloseSession;
4013        }
4014        let mut should_close = true;
4015        let mut should_write = false;
4016        if self.frontend_token == token {
4017            trace!(
4018                "{} MuxState::timeout_frontend({:#?})",
4019                log_context!(self),
4020                self.frontend
4021            );
4022            // The per-stream reaper (bidirectional-idle + outbound
4023            // flow-control-stall guards) normally runs only from `readable()`,
4024            // but a fully-silent peer never triggers a read event. Run it on the
4025            // connection-timeout path too so a window-stalled stream — a buffered
4026            // response the peer refuses to drain by holding its receive window
4027            // shut — is reaped and its MAX_CONCURRENT_STREAMS slot freed, instead
4028            // of lingering until the 30-minute zombie checker. The reaper queues
4029            // an `RST_STREAM(CANCEL)`; because `has_pending_write()` does NOT
4030            // observe the `H2ControlTx` queue (it gates connection close, so a
4031            // queued RST must not read as "keep open"), set `should_write` via
4032            // the dedicated `has_pending_control_write()` probe so the reset is
4033            // actually flushed to the peer before the connection closes — without
4034            // it, a fully-silent peer's stalled stream is freed but the peer sees
4035            // only EOF, never the RST(CANCEL).
4036            if let Connection::H2(h2) = &mut self.frontend {
4037                h2.cancel_timed_out_streams(
4038                    &mut self.context,
4039                    &mut EndpointClient(&mut self.router),
4040                );
4041                if h2.core.has_pending_control_write() {
4042                    should_write = true;
4043                }
4044            }
4045            if self.frontend.has_pending_write() {
4046                should_write = true;
4047            }
4048            let front_readiness = self.frontend.readiness_mut();
4049            for stream_id in 0..self.context.streams.len() {
4050                match self.context.streams[stream_id].state {
4051                    StreamState::Idle => {
4052                        // In h1 an Idle stream is always the first request, so we can send a 408
4053                        // In h2 an Idle stream doesn't necessarily hold a request yet,
4054                        // in most cases it was just reserved, so we can just ignore them.
4055                        if !front_is_h2 {
4056                            // Each stream answers out of the registry it
4057                            // captured when its request arrived; a listener
4058                            // reload since then belongs to the next one.
4059                            let answers_rc = self.context.streams[stream_id].answers.clone();
4060                            let answers = answers_rc.borrow();
4061                            let stream = &mut self.context.streams[stream_id];
4062                            stream.context.access_log_message = Some("client_timeout");
4063                            set_default_answer(stream, front_readiness, 408, &answers);
4064                            should_write = true;
4065                        }
4066                    }
4067                    StreamState::Link => {
4068                        // This is an unusual case, as we have both a complete request and no
4069                        // available backend yet. For now, we answer with 503.
4070                        // Not a timeout-driven outcome from the operator's
4071                        // perspective — leave access_log_message as None.
4072                        let answers_rc = self.context.streams[stream_id].answers.clone();
4073                        let answers = answers_rc.borrow();
4074                        let stream = &mut self.context.streams[stream_id];
4075                        set_default_answer(stream, front_readiness, 503, &answers);
4076                        should_write = true;
4077                    }
4078                    StreamState::Linked(_) => {
4079                        // The frontend timed out while a stream is linked to a backend.
4080                        // The backend timeout should handle this, but in case the backend
4081                        // is also stalled, send a 504 and terminate the stream.
4082                        if !self.context.streams[stream_id].back.consumed {
4083                            self.context.unlink_stream(stream_id);
4084                            let answers_rc = self.context.streams[stream_id].answers.clone();
4085                            let answers = answers_rc.borrow();
4086                            let stream = &mut self.context.streams[stream_id];
4087                            stream.context.access_log_message =
4088                                Some("client_timeout_during_response");
4089                            set_default_answer(stream, front_readiness, 504, &answers);
4090                            should_write = true;
4091                        } else if self.context.streams[stream_id].back.is_completed() {
4092                            // Response fully proxied, stream can be closed
4093                        } else if self.context.streams[stream_id].back.is_terminated()
4094                            || self.context.streams[stream_id].back.is_error()
4095                        {
4096                            // Response is terminated/error but not fully written to frontend.
4097                            // Keep the session alive briefly to flush remaining data.
4098                            should_close = false;
4099                        } else {
4100                            // Partial response in progress — forcefully terminate
4101                            self.context.unlink_stream(stream_id);
4102                            let stream = &mut self.context.streams[stream_id];
4103                            stream.context.access_log_message =
4104                                Some("client_timeout_during_response");
4105                            forcefully_terminate_answer(
4106                                stream,
4107                                front_readiness,
4108                                H2Error::InternalError,
4109                            );
4110                            should_write = true;
4111                        }
4112                        // end_stream is called in a second pass below to avoid
4113                        // borrow conflicts on context.streams.
4114                    }
4115                    StreamState::Unlinked => {
4116                        // A stream Unlinked already has a response and its backend closed.
4117                        // In case it hasn't finished proxying we wait. Otherwise it is a stream
4118                        // kept alive for a new request, which can be killed.
4119                        if !self.context.streams[stream_id].back.is_completed() {
4120                            should_close = false;
4121                        }
4122                    }
4123                    StreamState::Recycle => {
4124                        // A recycled stream is an h2 stream which doesn't hold a request anymore.
4125                        // We can ignore it.
4126                    }
4127                }
4128            }
4129            // Second pass: end streams that were linked to backends.
4130            // This is done separately to avoid borrow conflicts on context.streams.
4131            let linked_streams: Vec<(GlobalStreamId, Token)> = self
4132                .context
4133                .streams
4134                .iter()
4135                .enumerate()
4136                .filter_map(|(id, stream)| {
4137                    if let StreamState::Linked(back_token) = stream.state {
4138                        Some((id, back_token))
4139                    } else {
4140                        None
4141                    }
4142                })
4143                .collect();
4144            for (stream_id, back_token) in linked_streams {
4145                if let Some(backend) = self.router.backends.get_mut(&back_token) {
4146                    #[cfg(debug_assertions)]
4147                    debug_assert_h1_owns_stream(backend, stream_id);
4148                    backend.end_stream(stream_id, &mut self.context);
4149                }
4150            }
4151        } else if let Some(backend) = self.router.backends.get_mut(&token) {
4152            // Captured before the backend borrow so the re-arm below can reach
4153            // the pass's clock snapshot.
4154            let now = self.context.now;
4155            trace!(
4156                "{} MuxState::timeout_backend({:#?})",
4157                log_context_lite!(self),
4158                backend
4159            );
4160            // A dial still `Connecting` when its timer fires is the
4161            // `connect_timeout` armed by `Mux::dial_backend`: the backend
4162            // accepted nothing, so no byte of any request reached it. That is
4163            // a backend connect failure exactly like a refused dial, and it
4164            // takes the same path: flag the connection dead as the kernel
4165            // flags a refused one, and let `ready_inner`'s dead-backend branch
4166            // count the failure (`failures`, `retry_policy`, `backend.down`),
4167            // close the connection and re-queue its streams for another
4168            // backend through `end_stream_decision`'s `Reconnect`. Answering
4169            // 504 here left the failure uncounted, so the load balancer kept
4170            // choosing the dead backend (sozu-proxy/sozu#1800).
4171            //
4172            // The pass that does this runs right after this timeout:
4173            // `Mux::has_pending_backend_failure` reports the flag and
4174            // `Server::timeout` calls `ready` on it (`ProxySession::
4175            // needs_ready_pass`). Nothing is re-armed for this backend: that
4176            // pass removes it.
4177            if matches!(
4178                backend.position(),
4179                Position::Client(_, _, BackendStatus::Connecting(_))
4180            ) {
4181                debug!(
4182                    "{} backend connect timeout on {:?}, failing over",
4183                    log_context_lite!(self),
4184                    token
4185                );
4186                backend.readiness_mut().event.insert(Ready::ERROR);
4187                return StateResult::Continue;
4188            }
4189            let front_readiness = self.frontend.readiness_mut();
4190            let linked_ids: Vec<GlobalStreamId> = self
4191                .context
4192                .backend_streams
4193                .get(&token)
4194                .map_or_else(Vec::new, |ids| ids.to_vec());
4195            for stream_id in linked_ids {
4196                // This stream is linked to the backend that timedout
4197                if self.context.streams[stream_id].back.is_terminated()
4198                    || self.context.streams[stream_id].back.is_error()
4199                {
4200                    trace!(
4201                        "{} Stream terminated or in error, do nothing, just wait a bit more",
4202                        log_module_context!()
4203                    );
4204                    // Nothing to do, simply wait for the remaining bytes to be proxied
4205                    if !self.context.streams[stream_id].back.is_completed() {
4206                        should_close = false;
4207                    }
4208                } else if !self.context.streams[stream_id].back.consumed {
4209                    // The response has not started yet
4210                    trace!(
4211                        "{} Stream still waiting for response, send 504",
4212                        log_module_context!()
4213                    );
4214                    self.context.unlink_stream(stream_id);
4215                    let answers_rc = self.context.streams[stream_id].answers.clone();
4216                    let answers = answers_rc.borrow();
4217                    let stream = &mut self.context.streams[stream_id];
4218                    stream.context.access_log_message = Some("backend_timeout");
4219                    set_default_answer(stream, front_readiness, 504, &answers);
4220                    should_write = true;
4221                } else {
4222                    trace!(
4223                        "{} Stream waiting for end of response, forcefully terminate it",
4224                        log_module_context!()
4225                    );
4226                    self.context.unlink_stream(stream_id);
4227                    let stream = &mut self.context.streams[stream_id];
4228                    stream.context.access_log_message = Some("backend_response_timeout");
4229                    forcefully_terminate_answer(stream, front_readiness, H2Error::InternalError);
4230                    should_write = true;
4231                }
4232                #[cfg(debug_assertions)]
4233                debug_assert_h1_owns_stream(backend, stream_id);
4234                backend.end_stream(stream_id, &mut self.context);
4235            }
4236            // Re-arm the backend timeout if the session stays alive (draining streams).
4237            // Without this, the timeout is consumed and the session becomes immortal
4238            // until the zombie checker runs.
4239            //
4240            // The `!should_close` condition looks like a hole, and is not. On
4241            // `should_close`, control falls through to the shared tail below,
4242            // which can still return `Continue` — through the `should_write`
4243            // writable loop, or through `delay_close_for_frontend_flush` —
4244            // having re-armed only the FRONTEND container. That would be a
4245            // lost wakeup if this backend could still be carrying work with no
4246            // timer of its own. It cannot, and the chain is four links, all in
4247            // this file and `connection.rs`/`h1.rs`/`h2.rs`:
4248            //
4249            // 1. the loop above iterates `context.backend_streams[&token]` —
4250            //    every stream currently linked to this backend, and nothing
4251            //    else — and calls `Connection::end_stream` on EVERY iteration,
4252            //    outside the if/else chain, so no arm can skip it;
4253            // 2. `ConnectionH2::end_stream` (`h2.rs`) starts with
4254            //    `context.unlink_stream(...)` unconditionally.
4255            //    `ConnectionH1::end_stream` (`h1.rs`) does NOT: it first guards
4256            //    `if self.stream != Some(stream) { error!(..); return; }` and
4257            //    that early return skips the unlink. The two are dispatched by
4258            //    `Connection::end_stream` in `connection.rs`;
4259            // 3. the H1 guard cannot reject a stream this loop passes it,
4260            //    because an H1 connection carries at most one linked stream and
4261            //    it is exactly `self.stream`. That holds because
4262            //    `self.stream = Some(..)` occurs only in
4263            //    `ConnectionH1::start_stream`, paired with
4264            //    `Context::link_stream`, and `self.stream = None` occurs only
4265            //    inside `end_stream` itself, after the unlink has run. So while
4266            //    `backend_streams[&token]` names a stream, `self.stream` names
4267            //    the same one — and `debug_assert_h1_owns_stream`, called
4268            //    just above every `end_stream` in this function, is the
4269            //    tripwire that fires if it ever stops being true;
4270            // 4. so by the time the tail runs, this backend has no linked
4271            //    stream left. The index-consistency `assert_eq!` at the end of
4272            //    `Mux::ready` pins the other direction (`backend_streams` is
4273            //    exactly the set of `StreamState::Linked` streams).
4274            //
4275            // `Context::unlink_stream` is the eviction point this path relies
4276            // on, not the only one in the module: `remove_backend_stream` also
4277            // has direct callers in `h1.rs` and `h2.rs`. Extra eviction can
4278            // only strengthen the conclusion, but the premise is "this loop
4279            // evicts", not "nothing else can".
4280            //
4281            // Work attached to this backend AFTERWARDS does not depend on
4282            // this site either, and since the adapter landed it no longer even
4283            // depends on the connection doing I/O: a pooled backend stays in
4284            // `router.backends`, so `Mux::reschedule` keeps its handle armed at
4285            // whatever deadline the core holds, every pass. (`arm_timeout()` at
4286            // the top of `ConnectionH1::{readable,writable}` and in
4287            // `ConnectionH2::{handle_write,handle_headers_frame}` pushes that
4288            // deadline out on the first pass that moves a stream's bytes — not
4289            // on every pass, LIFECYCLE §9 invariant 9 — but nothing hangs on
4290            // it.) That is what covers the pool-reuse branch of
4291            // `Router::plan_connect` never arming a timeout, unlike the fresh-dial
4292            // branch.
4293            //
4294            // The assertion below is the cheap tripwire for link 3: if the
4295            // impossible becomes possible (a new arm that skips `end_stream`,
4296            // or an `end_stream` that stops unlinking), it fires here rather
4297            // than as an immortal backend in production.
4298            #[cfg(debug_assertions)]
4299            debug_assert!(
4300                self.context
4301                    .backend_streams
4302                    .get(&token)
4303                    .is_none_or(|ids| ids.is_empty()),
4304                "backend {token:?} still holds linked streams {:?} after its \
4305                 timeout reaped them: on the should_close path nothing re-arms \
4306                 its container, so those streams would have no backend timer",
4307                self.context.backend_streams.get(&token),
4308            );
4309            if !should_close {
4310                backend.arm_timeout(now);
4311            }
4312        } else {
4313            // Session received a timeout for an unknown token, ignore it
4314            return StateResult::Continue;
4315        }
4316        if should_write {
4317            // Drain as much pending data as possible before closing.
4318            // A single writable() call is insufficient for large responses —
4319            // the TLS buffer may need multiple flushes. Without this loop,
4320            // the session is killed with unflushed TLS data, causing the
4321            // client to receive a truncated TLS record ("decode error").
4322            //
4323            // The constant 16 is empirical: it papers over a missing
4324            // invariant-15 hop in the H2 mux state machine where the
4325            // writable readiness signal is not always re-armed after a
4326            // partial flush. Long-term plan: reach invariant-15 closure
4327            // and remove this loop. See `lib/src/protocol/mux/LIFECYCLE.md`.
4328            let mut result = StateResult::Continue;
4329            for _ in 0..16 {
4330                result = match self
4331                    .frontend
4332                    .writable(&mut self.context, EndpointClient(&mut self.router))
4333                {
4334                    MuxResult::Continue => StateResult::Continue,
4335                    MuxResult::Upgrade => StateResult::Upgrade,
4336                    MuxResult::CloseSession => StateResult::CloseSession,
4337                };
4338                if result != StateResult::Continue
4339                    || !self.frontend.readiness_mut().interest.is_writable()
4340                {
4341                    break;
4342                }
4343            }
4344            // Re-arm the frontend timeout so the session doesn't become immortal.
4345            // The writable call may have partially flushed the response — we need
4346            // the timeout to fire again if the flush stalls.
4347            if result == StateResult::Continue {
4348                self.frontend.arm_timeout(self.context.now);
4349            }
4350            return result;
4351        }
4352        if should_close {
4353            if self.delay_close_for_frontend_flush("timeout") {
4354                debug!(
4355                    "{} Mux timeout delaying close for frontend flush: token={:?}, frontend={:?}",
4356                    log_context!(self),
4357                    token,
4358                    self.frontend
4359                );
4360                self.frontend.arm_timeout(self.context.now);
4361                return StateResult::Continue;
4362            }
4363            if front_is_h2 {
4364                debug!(
4365                    "{} Mux timeout returning CloseSession: token={:?}, frontend={:?}",
4366                    log_context!(self),
4367                    token,
4368                    self.frontend
4369                );
4370                for (idx, stream) in self.context.streams.iter().enumerate() {
4371                    if stream.state != StreamState::Recycle {
4372                        debug!(
4373                            "{}   timeout stream[{}]: state={:?}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
4374                            log_context!(self),
4375                            idx,
4376                            stream.state,
4377                            stream.front.parsing_phase,
4378                            stream.back.parsing_phase,
4379                            stream.front.is_completed(),
4380                            stream.back.is_completed()
4381                        );
4382                    }
4383                }
4384            }
4385            StateResult::CloseSession
4386        } else {
4387            // Re-arm the frontend timeout. Without this, the timeout is consumed
4388            // by triggered() and the session stays alive indefinitely until the
4389            // zombie checker runs (default: 30 minutes).
4390            self.frontend.arm_timeout(self.context.now);
4391            StateResult::Continue
4392        }
4393    }
4394
4395    fn shutting_down_inner(&mut self) -> SessionIsToBeClosed {
4396        // `shut_down_sessions()` drives this outside `ready()`, so it is its
4397        // own sampling point, and it is load-bearing rather than belt-and-braces.
4398        // The chain is: this line refreshes `context.now`;
4399        // `drive_frontend_shutdown_io` below always reaches `readable()` for an
4400        // H2 frontend (`force_h2_read` is unconditionally true, so the
4401        // early return above it cannot fire), and `readable` mirrors
4402        // `context.now` into `ConnectionH2::now`; the forced-close check that
4403        // follows then reads a fresh snapshot. Drop this line and a silent
4404        // draining session propagates the snapshot of its last `ready()` pass
4405        // forever, so the graceful-shutdown budget never expires. Pinned by
4406        // `shutting_down_refreshes_the_snapshot_so_the_drain_budget_expires`.
4407        let now = Instant::now();
4408        self.context.now = now;
4409        self.context.now_wall_ms = unix_epoch_ms();
4410        // RFC 9113 §6.8: initiate graceful shutdown with double-GOAWAY pattern.
4411        // Only send the initial GOAWAY once. The final GOAWAY (with the real
4412        // last_stream_id) is handled by finalize_write() when all streams drain.
4413        // Calling graceful_goaway() again would send the final GOAWAY
4414        // prematurely and force-disconnect before in-flight streams complete.
4415        if !self.frontend.is_draining() {
4416            match self.frontend.graceful_goaway(now) {
4417                MuxResult::CloseSession => return true,
4418                MuxResult::Continue => {
4419                    // graceful_goaway() queued a GOAWAY frame. Flush it directly
4420                    // since the event loop uses edge-triggered epoll and won't
4421                    // deliver a new WRITABLE event for an already-writable socket.
4422                    self.frontend.flush_output_buffer();
4423                }
4424                _ => {}
4425            }
4426        } else {
4427            trace!(
4428                "{} shutting_down: already draining, skipping duplicate GOAWAY",
4429                log_context!(self)
4430            );
4431            // A peer GOAWAY drains without arming the graceful-shutdown
4432            // budget, which only `graceful_goaway` arms: arm it here, once,
4433            // so the forced-close check below still bounds this session.
4434            self.frontend.arm_graceful_shutdown_deadline(now);
4435            // shut_down_sessions() runs outside ready(), so retry flushing any
4436            // previously-buffered GOAWAY/TLS records on each pass.
4437            self.frontend.flush_output_buffer();
4438        }
4439        if self.drive_frontend_shutdown_io() {
4440            return true;
4441        }
4442        // Forced-close deadline: once the H2 listener's
4443        // `h2_graceful_shutdown_deadline_seconds` budget has elapsed from
4444        // the moment `graceful_goaway` armed `drain.started_at`, stop
4445        // waiting for streams and tear the session down. `drive_frontend_
4446        // shutdown_io` above already had a chance to flush any pending
4447        // TLS/GOAWAY records; this branch accepts that some bytes may be
4448        // lost in exchange for honoring the operator-configured SLA.
4449        // Listeners that disable the knob (`= 0` → `None`) short-circuit
4450        // the check inside `graceful_shutdown_deadline_elapsed`.
4451        //
4452        // RFC 9113 §6.8: the close is still preceded by the final GOAWAY, so
4453        // the peer learns which streams were processed; the advisory one at
4454        // `2^31-1` says nothing about it (sozu-proxy/sozu#1647). It is flushed
4455        // once, best effort, like the advisory GOAWAY above.
4456        if self.frontend.graceful_shutdown_deadline_elapsed() {
4457            debug!(
4458                "{} Mux shutting_down: graceful-shutdown deadline elapsed, forcing close",
4459                log_context!(self)
4460            );
4461            // A lingering connection already sent its final GOAWAY and shut
4462            // its write side down: the operator's budget closes it as is.
4463            if !self.frontend.is_lingering() {
4464                let _ = self.frontend.goaway_before_forced_close(&self.context);
4465                self.frontend.flush_output_buffer();
4466            }
4467            return true;
4468        }
4469        if self.frontend.is_lingering() {
4470            // Bounded by its own deadline (`request_timeout` from the
4471            // linger's start), which the timer enforces. For H2 also by the
4472            // graceful-shutdown budget just above, once that budget is armed:
4473            // `graceful_goaway` arms it when this soft stop sends the first
4474            // GOAWAY, and the lingering passes keep the connection's clock
4475            // fresh so it can elapse. A drain the client's own GOAWAY started
4476            // never armed it, so such a linger runs to its own deadline.
4477            return false;
4478        }
4479        if matches!(self.frontend, Connection::H2(_)) && self.frontend.is_draining() {
4480            for stream in &mut self.context.streams {
4481                if stream.front_received_end_of_stream {
4482                    continue;
4483                }
4484                if !matches!(stream.state, StreamState::Linked(_) | StreamState::Unlinked) {
4485                    continue;
4486                }
4487                if stream.front.consumed
4488                    && stream.front.storage.is_empty()
4489                    && stream.front.is_completed()
4490                {
4491                    stream.front_received_end_of_stream = true;
4492                    self.frontend
4493                        .readiness_mut()
4494                        .interest
4495                        .insert(Ready::WRITABLE);
4496                    self.frontend.readiness_mut().signal_pending_write();
4497                }
4498            }
4499        }
4500        // An H2 request that has not reached a backend yet still holds the
4501        // session, bounded by the forced-close deadline above: a field block
4502        // still being reassembled (its stream is `StreamState::Idle`), and a
4503        // complete one awaiting `Mux::ready`'s link pass (`StreamState::Link`),
4504        // which is where a CONTINUATION read by `drive_frontend_shutdown_io`
4505        // leaves it. Closing on either loses a request the advisory GOAWAY
4506        // invited, and skips the final GOAWAY `ConnectionH2::finalize_write`
4507        // sends once the stream is done (sozu-proxy/sozu#1647).
4508        let front_is_h2 = matches!(self.frontend, Connection::H2(_));
4509        let mut can_stop = !self.frontend.peer_header_block_in_progress();
4510        for stream in &mut self.context.streams {
4511            match stream.state {
4512                StreamState::Linked(_) => {
4513                    can_stop = false;
4514                }
4515                StreamState::Link if front_is_h2 => {
4516                    can_stop = false;
4517                }
4518                StreamState::Unlinked => {
4519                    kawa::debug_kawa(&stream.front);
4520                    kawa::debug_kawa(&stream.back);
4521                    if stream.is_quiesced() {
4522                        continue;
4523                    }
4524                    stream.context.closing = true;
4525                    can_stop = false;
4526                }
4527                _ => {}
4528            }
4529        }
4530        if self.frontend.has_pending_write() {
4531            return false;
4532        }
4533        if can_stop {
4534            let active_h2_streams = self
4535                .context
4536                .streams
4537                .iter()
4538                .enumerate()
4539                .filter(|(_, s)| {
4540                    if s.state == StreamState::Recycle {
4541                        return false;
4542                    }
4543                    if s.state == StreamState::Unlinked && s.is_quiesced() {
4544                        return false;
4545                    }
4546                    true
4547                })
4548                .collect::<Vec<_>>();
4549            if matches!(self.frontend, Connection::H2(_)) && !active_h2_streams.is_empty() {
4550                debug!(
4551                    "{} Mux shutting_down returning true with active H2 streams: {:?}",
4552                    log_context!(self),
4553                    self.frontend
4554                );
4555                for (idx, stream) in active_h2_streams {
4556                    debug!(
4557                        "{}   shutdown stream[{}]: state={:?}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
4558                        log_context!(self),
4559                        idx,
4560                        stream.state,
4561                        stream.front.parsing_phase,
4562                        stream.back.parsing_phase,
4563                        stream.front.is_completed(),
4564                        stream.back.is_completed()
4565                    );
4566                }
4567            }
4568        }
4569        can_stop
4570    }
4571}
4572
4573/// The embedder's [`router::BackendSelector`]: the worker's backend set and
4574/// this session's registry, lent together for one selection.
4575///
4576/// It is the one place a registry handle becomes an opaque [`BackendId`]: the
4577/// backend is chosen and reserved by the `BackendMap`, named by the session's
4578/// table, and the `Rc` goes no further. Everything past
4579/// [`router::BackendSelector::select`] — the dial in `Mux::dial_backend`, and
4580/// every `Position::Client` built from it — holds the id, never the handle.
4581struct RegistrySelector<'a> {
4582    backends: &'a mut BackendMap,
4583    registry: &'a mut BackendRegistry,
4584    /// The pass's clock sample, [`Context::now`]: selection judges backoff
4585    /// windows and connection-time decay against it rather than reading a
4586    /// clock (#1684).
4587    now: Instant,
4588    /// The backends the stream being linked already failed to connect to,
4589    /// [`Stream::tried_backends`]: selection prefers any other one
4590    /// (sozu-proxy/sozu#1800).
4591    exclude: &'a [std::net::SocketAddr],
4592}
4593
4594impl router::BackendSelector for RegistrySelector<'_> {
4595    fn select(
4596        &mut self,
4597        cluster_id: &str,
4598        affinity: router::Affinity<'_>,
4599        key: Option<u64>,
4600    ) -> Result<router::SelectedBackend, BackendError> {
4601        let handle = match affinity {
4602            router::Affinity::Sticky(Some(cookie)) => {
4603                self.backends.reserve_sticky_backend_excluding(
4604                    cluster_id,
4605                    cookie,
4606                    key,
4607                    self.now,
4608                    self.exclude,
4609                )?
4610            }
4611            router::Affinity::Sticky(None) | router::Affinity::Unpinned => self
4612                .backends
4613                .reserve_backend_excluding(cluster_id, key, self.now, self.exclude)?,
4614        };
4615        let sticky_session = match affinity {
4616            router::Affinity::Sticky(_) => {
4617                let chosen = handle.borrow();
4618                Some(
4619                    chosen
4620                        .sticky_id
4621                        .clone()
4622                        .unwrap_or_else(|| chosen.backend_id.to_owned()),
4623                )
4624            }
4625            router::Affinity::Unpinned => None,
4626        };
4627        let backend = self.registry.id_for(&handle);
4628        // The id names the handle the map just reserved, not a copy of it.
4629        debug_assert!(
4630            self.registry
4631                .get(backend.slot())
4632                .is_some_and(|known| Rc::ptr_eq(known, &handle)),
4633            "the minted id must resolve to the very backend that was reserved"
4634        );
4635        Ok(router::SelectedBackend {
4636            backend,
4637            sticky_session,
4638        })
4639    }
4640}
4641
4642/// Minimal listener + `Mux` scaffolding shared by the `mod.rs` and `h2.rs`
4643/// test modules. Lives here rather than in either test module because
4644/// `Context<L>` is generic over the listener and both need the same `L`.
4645#[cfg(test)]
4646pub(crate) mod test_support {
4647    use std::{
4648        cell::RefCell,
4649        collections::BTreeMap,
4650        net::{SocketAddr, TcpListener as StdTcpListener},
4651        rc::Rc,
4652    };
4653
4654    use sozu_command::logging::CachedTags;
4655
4656    use super::*;
4657    use crate::{
4658        FrontendFromRequestError, L7ListenerHandler, ListenerHandler, Protocol,
4659        protocol::http::{answers::HttpAnswers, parser::Method},
4660        router::RouteResult,
4661    };
4662
4663    /// Implements exactly the nine required methods of `ListenerHandler` +
4664    /// `L7ListenerHandler`; every other knob keeps its trait default.
4665    pub(crate) struct TestListener {
4666        address: SocketAddr,
4667        answers: Rc<RefCell<HttpAnswers>>,
4668        sticky_name: String,
4669        elide_x_real_ip: bool,
4670    }
4671
4672    pub(crate) const TEST_STICKY_NAME: &str = "SOZUBALANCEID";
4673
4674    impl TestListener {
4675        pub(crate) fn new() -> Self {
4676            Self {
4677                address: "127.0.0.1:1".parse().expect("test address must parse"),
4678                answers: test_answers(),
4679                sticky_name: TEST_STICKY_NAME.to_owned(),
4680                elide_x_real_ip: false,
4681            }
4682        }
4683
4684        /// Stand in for an operator listener reload: change the per-request
4685        /// knobs and publish a brand-new answer registry under a new `Rc`,
4686        /// exactly as `HttpListener::update_config` does. Returns the newly
4687        /// published registry.
4688        ///
4689        /// The registry must be *published*, not edited: a test that mutated
4690        /// the existing one in place would prove nothing about the per-stream
4691        /// capture, because both streams would still be holding the same
4692        /// object.
4693        pub(crate) fn reload(
4694            &mut self,
4695            sticky_name: &str,
4696            elide_x_real_ip: bool,
4697        ) -> Rc<RefCell<HttpAnswers>> {
4698            self.sticky_name = sticky_name.to_owned();
4699            self.elide_x_real_ip = elide_x_real_ip;
4700            self.answers = test_answers();
4701            self.answers.clone()
4702        }
4703    }
4704
4705    /// A fresh, empty answer registry — what a test `Stream` renders from
4706    /// when the test is not about templates.
4707    pub(crate) fn test_answers() -> Rc<RefCell<HttpAnswers>> {
4708        Rc::new(RefCell::new(
4709            HttpAnswers::new(&BTreeMap::new()).expect("default answers must build"),
4710        ))
4711    }
4712
4713    impl ListenerHandler for TestListener {
4714        fn get_addr(&self) -> &SocketAddr {
4715            &self.address
4716        }
4717        fn get_tags(&self, _key: &str) -> Option<&CachedTags> {
4718            None
4719        }
4720        fn set_tags(&mut self, _key: String, _tags: Option<BTreeMap<String, String>>) {}
4721        fn protocol(&self) -> Protocol {
4722            Protocol::HTTP
4723        }
4724        fn public_address(&self) -> SocketAddr {
4725            self.address
4726        }
4727    }
4728
4729    impl L7ListenerHandler for TestListener {
4730        fn get_sticky_name(&self) -> &str {
4731            &self.sticky_name
4732        }
4733        fn get_elide_x_real_ip(&self) -> bool {
4734            self.elide_x_real_ip
4735        }
4736        fn get_connect_timeout(&self) -> u32 {
4737            10
4738        }
4739        fn frontend_from_request(
4740            &self,
4741            _host: &str,
4742            _uri: &str,
4743            _method: &Method,
4744        ) -> Result<RouteResult, FrontendFromRequestError> {
4745            Err(FrontendFromRequestError::InvalidCharsAfterHost(
4746                "test listener routes nothing".to_owned(),
4747            ))
4748        }
4749        fn get_answers(&self) -> &Rc<RefCell<HttpAnswers>> {
4750            &self.answers
4751        }
4752    }
4753
4754    /// Whether the epoll instance behind `epoll` watches descriptor `fd`,
4755    /// read from the `tfd:` lines Linux lists in `/proc/self/fdinfo`.
4756    ///
4757    /// Reading the kernel's own table, rather than counting calls, is what
4758    /// lets a test tell "never deregistered" from "removed by the close".
4759    #[cfg(target_os = "linux")]
4760    pub(crate) fn epoll_watches(epoll: &impl std::os::fd::AsRawFd, fd: std::os::fd::RawFd) -> bool {
4761        let fdinfo = std::fs::read_to_string(format!("/proc/self/fdinfo/{}", epoll.as_raw_fd()))
4762            .expect("the epoll instance must expose its fdinfo");
4763        fdinfo.lines().any(|line| {
4764            line.strip_prefix("tfd:")
4765                .and_then(|rest| rest.split_whitespace().next())
4766                .and_then(|target| target.parse::<std::os::fd::RawFd>().ok())
4767                == Some(fd)
4768        })
4769    }
4770
4771    /// A live, connected, non-blocking loopback socket. Reads return
4772    /// `WouldBlock` rather than `ECONNREFUSED`, so a `readable()` pass over it
4773    /// is a no-op instead of a forced disconnect. The accepted peer is returned
4774    /// so the caller can keep it alive for the duration of the test.
4775    pub(crate) fn connected_socket() -> (mio::net::TcpStream, std::net::TcpStream) {
4776        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
4777        let address = listener.local_addr().expect("listener local addr");
4778        let client = mio::net::TcpStream::connect(address).expect("connect test client");
4779        let (peer, _) = listener.accept().expect("accept test peer");
4780        peer.set_nonblocking(true).expect("peer nonblocking");
4781        (client, peer)
4782    }
4783
4784    /// A `Context` backed by [`TestListener`] and a small buffer pool.
4785    pub(crate) fn test_context(pool: &Rc<RefCell<Pool>>) -> Context<TestListener> {
4786        Context::new(
4787            Ulid::generate(),
4788            Rc::downgrade(pool),
4789            Rc::new(RefCell::new(TestListener::new())),
4790            None,
4791            "127.0.0.1:1".parse().expect("public address must parse"),
4792        )
4793    }
4794}
4795
4796// Counts `writable()` calls made by `Mux::drive_frontend_shutdown_io`, so a
4797// test can pin how much work one shutdown pass does. Thread-local: each test
4798// runs on its own thread.
4799#[cfg(test)]
4800thread_local! {
4801    static SHUTDOWN_WRITABLE_PASSES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4802    /// Times `Mux::wait_for_client_at_loop_limit` ran on this thread, in
4803    /// every build (the debug history is empty in release builds).
4804    static LOOP_LIMIT_HITS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4805}
4806
4807#[cfg(test)]
4808mod tests {
4809    use std::collections::HashMap;
4810
4811    use super::test_support::{TEST_STICKY_NAME, connected_socket, test_context};
4812    use super::*;
4813    use crate::{
4814        pool::Pool,
4815        protocol::http::parser::Method,
4816        protocol::mux::{
4817            h2::{H2ConnectionConfig, H2State},
4818            h2_flood_detector::H2FloodConfig,
4819        },
4820        timer::TimeoutContainer,
4821    };
4822
4823    /// After its final GOAWAY, an H2 frontend closes while the client may
4824    /// still be sending frames. A close with those frames unread, or followed
4825    /// by more, makes the kernel reset the connection and discard the part of
4826    /// the response still queued in the frontend's socket. The connection
4827    /// must shut its write side down and drain instead (RFC 9112 §9.6, the
4828    /// same lingering close as H1), so the client reads all of it, then EOF.
4829    ///
4830    /// The test emulates the session close `HttpsSession::close` performs —
4831    /// `shutdown_write`, then `close(2)` — whenever `writable` asks for it.
4832    #[test]
4833    fn an_h2_frontend_lingers_after_its_final_goaway_so_the_client_reads_everything() {
4834        use std::io::{Read, Write};
4835
4836        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
4837        let (socket, mut peer) = connected_socket();
4838        // A client that is slow to read: most of the response stays queued
4839        // in the frontend's socket.
4840        socket2::SockRef::from(&peer)
4841            .set_recv_buffer_size(32 * 1024)
4842            .expect("the client receive buffer must be settable");
4843
4844        let mut h2 = h2::H2Shell::new(
4845            Ulid::generate(),
4846            socket,
4847            Position::Server,
4848            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
4849            H2FloodConfig::default(),
4850            H2ConnectionConfig::default(),
4851            Duration::from_secs(30),
4852            None,
4853            Duration::from_secs(10),
4854            None,
4855            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
4856        )
4857        .expect("a pool with free buffers must yield an H2 connection");
4858        // The final GOAWAY is out: the next writable pass closes.
4859        h2.core.state = H2State::GoAway;
4860        h2.core.drain.__test_set_draining();
4861        h2.core.graceful_goaway = true;
4862
4863        // The response, already handed to the kernel.
4864        let mut queued = 0;
4865        let chunk = [b'r'; 16 * 1024];
4866        loop {
4867            match (&h2.socket).write(&chunk) {
4868                Ok(size) => queued += size,
4869                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
4870                Err(e) => panic!("the response write failed: {e}"),
4871            }
4872            if queued >= 1024 * 1024 {
4873                break;
4874            }
4875        }
4876        // The client still sends frames after the GOAWAY.
4877        peer.write_all(&[0u8; 1024])
4878            .expect("the client may still send after the GOAWAY");
4879        std::thread::sleep(Duration::from_millis(20));
4880
4881        let mut frontend = Connection::H2(h2);
4882        let mut context = test_context(&pool);
4883        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
4884        context.now = Instant::now();
4885        let result = frontend.writable(&mut context, EndpointClient(&mut router));
4886        let lingering = frontend.is_lingering();
4887        let mut frontend = Some(frontend);
4888        if matches!(result, MuxResult::CloseSession)
4889            && let Some(closed) = frontend.take()
4890        {
4891            let _ = shutdown_write(closed.socket(), false);
4892            drop(closed);
4893        }
4894
4895        peer.set_nonblocking(false)
4896            .expect("the client reads blocking");
4897        peer.set_read_timeout(Some(Duration::from_secs(5)))
4898            .expect("the client read is bounded");
4899        let mut received = 0;
4900        let mut buf = [0u8; 16 * 1024];
4901        let end = loop {
4902            match peer.read(&mut buf) {
4903                Ok(0) => break Ok(()),
4904                Ok(size) => received += size,
4905                Err(e) => break Err(e),
4906            }
4907        };
4908        assert_eq!(
4909            received, queued,
4910            "the client reads the whole response (end: {end:?})"
4911        );
4912        assert!(
4913            end.is_ok(),
4914            "the response ends with a clean EOF, got {end:?}"
4915        );
4916        assert!(
4917            lingering && matches!(result, MuxResult::Continue),
4918            "the frontend lingers instead of closing, got {result:?}"
4919        );
4920
4921        // The client closes: the drain meets its EOF and the session closes.
4922        let mut frontend = frontend.expect("a lingering frontend is kept");
4923        drop(peer);
4924        let mut after_client_close = MuxResult::Continue;
4925        for _ in 0..1000 {
4926            frontend.readiness_mut().event.insert(Ready::READABLE);
4927            after_client_close = frontend.readable(&mut context, EndpointClient(&mut router));
4928            if matches!(after_client_close, MuxResult::CloseSession) {
4929                break;
4930            }
4931            std::thread::sleep(Duration::from_millis(1));
4932        }
4933        assert!(
4934            matches!(after_client_close, MuxResult::CloseSession),
4935            "the client's EOF ends the drain, got {after_client_close:?}"
4936        );
4937    }
4938
4939    /// Start the lingering close of an H2 frontend that queued its final
4940    /// GOAWAY through `ConnectionH2::goaway` — `NO_ERROR` when `graceful`,
4941    /// `ENHANCE_YOUR_CALM` (a flood) otherwise — with
4942    /// a 10 s `request_timeout`; then have its client send `sent` bytes
4943    /// without closing, move the drain's clock `elapsed` past the linger's
4944    /// start, and drive the drain until it closes or 200 passes go by. Answers the linger the close started with, the instants that
4945    /// bracket it, and whether the drain closed.
4946    fn h2_linger_after_goaway(
4947        graceful: bool,
4948        sent: usize,
4949        elapsed: Duration,
4950    ) -> (shared::Linger, Instant, Instant, bool) {
4951        use std::io::Write;
4952
4953        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
4954        let (socket, peer) = connected_socket();
4955        let mut h2 = h2::H2Shell::new(
4956            Ulid::generate(),
4957            socket,
4958            Position::Server,
4959            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
4960            H2FloodConfig::default(),
4961            H2ConnectionConfig::default(),
4962            Duration::from_secs(30),
4963            None,
4964            Duration::from_secs(10),
4965            None,
4966            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
4967        )
4968        .expect("a pool with free buffers must yield an H2 connection");
4969        // The real entry point: `goaway` queues the final GOAWAY, records
4970        // its error code and asks for the write that flushes it.
4971        let queued = h2.core.goaway(if graceful {
4972            H2Error::NoError
4973        } else {
4974            H2Error::EnhanceYourCalm
4975        });
4976        assert!(
4977            matches!(queued, MuxResult::Continue),
4978            "the GOAWAY is queued"
4979        );
4980
4981        let mut frontend = Connection::H2(h2);
4982        let mut context = test_context(&pool);
4983        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
4984        let before = Instant::now();
4985        context.now = before;
4986        let result = frontend.writable(&mut context, EndpointClient(&mut router));
4987        let after = Instant::now();
4988        assert!(
4989            frontend.is_lingering() && matches!(result, MuxResult::Continue),
4990            "premise: the frontend lingers after its final GOAWAY, got {result:?}"
4991        );
4992        let Connection::H2(h2) = &frontend else {
4993            unreachable!("built as H2")
4994        };
4995        let linger = h2.core.linger;
4996
4997        // The client keeps sending and never closes; it is kept open until
4998        // the drain has decided.
4999        peer.set_nonblocking(false)
5000            .expect("the client writes blocking");
5001        let mut writer = peer.try_clone().expect("the client socket clones");
5002        let sender = std::thread::spawn(move || {
5003            let _ = writer.write_all(&vec![0u8; sent]);
5004        });
5005        context.now = after + elapsed;
5006        let mut closed = false;
5007        for _ in 0..200 {
5008            frontend.readiness_mut().event.insert(Ready::READABLE);
5009            if matches!(
5010                frontend.readable(&mut context, EndpointClient(&mut router)),
5011                MuxResult::CloseSession
5012            ) {
5013                closed = true;
5014                break;
5015            }
5016            std::thread::sleep(Duration::from_millis(1));
5017        }
5018        drop(frontend);
5019        let _ = sender.join();
5020        drop(peer);
5021        (linger, before, after, closed)
5022    }
5023
5024    /// The lingering close after a GOAWAY carrying an error code exists only
5025    /// to deliver that GOAWAY (sozu-proxy/sozu#1861): its deadline is
5026    /// `shared::ERROR_GOAWAY_LINGER_TIMEOUT` from its start and its byte
5027    /// budget `shared::ERROR_GOAWAY_LINGER_MAX_BYTES`, so a client that keeps
5028    /// sending is cut off once that much is read. A graceful GOAWAY keeps the
5029    /// listener's `request_timeout` and `shared::LINGER_MAX_BYTES`, and the
5030    /// same traffic does not close it. With no traffic at all, the short
5031    /// deadline closes the error linger and leaves the graceful one open.
5032    ///
5033    /// TO SEE THIS RED: in `ConnectionH2::linger_budget` (`h2.rs`), return
5034    /// the graceful pair for every GOAWAY.
5035    #[test]
5036    fn an_error_goaway_lingers_under_the_short_budget_and_a_graceful_one_keeps_its_own() {
5037        let sent = shared::ERROR_GOAWAY_LINGER_MAX_BYTES + 64 * 1024;
5038
5039        let (linger, before, after, closed) = h2_linger_after_goaway(false, sent, Duration::ZERO);
5040        let shared::Linger::Draining {
5041            deadline,
5042            remaining,
5043        } = linger
5044        else {
5045            panic!("an error GOAWAY drains at once on a plain socket, got {linger:?}");
5046        };
5047        assert_eq!(remaining, shared::ERROR_GOAWAY_LINGER_MAX_BYTES);
5048        assert!(
5049            deadline >= before + shared::ERROR_GOAWAY_LINGER_TIMEOUT
5050                && deadline <= after + shared::ERROR_GOAWAY_LINGER_TIMEOUT,
5051            "the error linger's deadline is the short one, {:?} past its start",
5052            deadline.saturating_duration_since(before)
5053        );
5054        assert!(
5055            closed,
5056            "the byte budget ends the error linger while the client still sends"
5057        );
5058
5059        let (linger, before, after, closed) = h2_linger_after_goaway(true, sent, Duration::ZERO);
5060        let shared::Linger::Draining {
5061            deadline,
5062            remaining,
5063        } = linger
5064        else {
5065            panic!("a graceful GOAWAY drains at once on a plain socket, got {linger:?}");
5066        };
5067        assert_eq!(remaining, shared::LINGER_MAX_BYTES);
5068        assert!(
5069            deadline >= before + Duration::from_secs(10)
5070                && deadline <= after + Duration::from_secs(10),
5071            "the graceful linger keeps request_timeout, {:?} past its start",
5072            deadline.saturating_duration_since(before)
5073        );
5074        assert!(
5075            !closed,
5076            "a graceful linger keeps draining a client that is still sending"
5077        );
5078
5079        let (.., closed) = h2_linger_after_goaway(false, 0, shared::ERROR_GOAWAY_LINGER_TIMEOUT);
5080        assert!(closed, "the short deadline ends a silent error linger");
5081        let (.., closed) = h2_linger_after_goaway(true, 0, shared::ERROR_GOAWAY_LINGER_TIMEOUT);
5082        assert!(!closed, "the short deadline does not end a graceful linger");
5083    }
5084
5085    /// Run one `Mux::ready` pass over an H2 frontend that queued its final
5086    /// GOAWAY (`NO_ERROR` when `graceful`, `ENHANCE_YOUR_CALM` otherwise)
5087    /// while one stream is still linked to a backend. Answers the `Mux`
5088    /// after the pass and the backend's peer socket.
5089    fn mux_after_final_goaway_with_a_linked_stream(
5090        pool: &Rc<RefCell<Pool>>,
5091        graceful: bool,
5092    ) -> (
5093        Mux<mio::net::TcpStream, test_support::TestListener>,
5094        std::net::TcpStream,
5095        std::net::TcpStream,
5096    ) {
5097        let (socket, front_peer) = connected_socket();
5098        let mut h2 = h2::H2Shell::new(
5099            Ulid::generate(),
5100            socket,
5101            Position::Server,
5102            &mut PoolBufferSource::new(Rc::downgrade(pool)),
5103            H2FloodConfig::default(),
5104            H2ConnectionConfig::default(),
5105            Duration::from_secs(30),
5106            None,
5107            Duration::from_secs(10),
5108            None,
5109            Ready::WRITABLE | Ready::HUP | Ready::ERROR,
5110        )
5111        .expect("a pool with free buffers must yield an H2 connection");
5112        let queued = h2.core.goaway(if graceful {
5113            H2Error::NoError
5114        } else {
5115            H2Error::EnhanceYourCalm
5116        });
5117        assert!(
5118            matches!(queued, MuxResult::Continue),
5119            "the GOAWAY is queued"
5120        );
5121
5122        let mut context = test_context(pool);
5123        context
5124            .create_stream(Ulid::generate(), 1 << 16)
5125            .expect("test context must create a stream");
5126        let mut mux = Mux {
5127            configured_frontend_timeout: Duration::from_secs(10),
5128            frontend_token: Token(0),
5129            frontend: Connection::H2(h2),
5130            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5131            context,
5132            session_ulid: Ulid::generate(),
5133            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(10))),
5134            backend_registry: BackendRegistry::default(),
5135            backends: Rc::default(),
5136        };
5137        let (backend, back_peer) = test_backend_connection(&mut mux, Duration::from_secs(30));
5138        mux.router.backends.insert(Token(1), backend);
5139        mux.context.link_stream(0, Token(1));
5140        mux.timeouts.backends.get_or_insert_with(Token(1), || {
5141            TimeoutContainer::new_empty(Duration::from_secs(30))
5142        });
5143
5144        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
5145        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
5146        let mut metrics = SessionMetrics::new(None);
5147        let result = mux.ready(session, proxy, &mut metrics);
5148        assert!(
5149            matches!(result, SessionResult::Continue) && mux.frontend.is_lingering(),
5150            "premise: the pass that flushes the final GOAWAY starts the linger, got {result:?}"
5151        );
5152        (mux, front_peer, back_peer)
5153    }
5154
5155    /// A final GOAWAY carrying an error code can leave streams open, and none
5156    /// of them will be answered: the pass that starts its linger logs and
5157    /// recycles them and shuts down, accounts for and drops their backend
5158    /// connections, instead of holding them until the linger ends. A graceful
5159    /// linger is left as it was.
5160    ///
5161    /// TO SEE THIS RED: remove both calls to
5162    /// `Mux::release_streams_and_backends_for_error_linger` from
5163    /// `Mux::ready_inner`.
5164    #[test]
5165    fn an_error_goaway_linger_releases_its_streams_and_backends_when_it_starts() {
5166        use std::io::Read;
5167
5168        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
5169        let (mux, _front_peer, mut back_peer) =
5170            mux_after_final_goaway_with_a_linked_stream(&pool, false);
5171        assert!(
5172            mux.router.backends.is_empty(),
5173            "the backend connection is released when the error linger starts"
5174        );
5175        assert!(
5176            mux.context.backend_streams.is_empty() && mux.timeouts.backends.is_empty(),
5177            "no reverse-index entry or backend timer survives the release"
5178        );
5179        assert_eq!(
5180            mux.context.streams[0].state,
5181            StreamState::Recycle,
5182            "the stream that will get no answer is logged and recycled"
5183        );
5184        back_peer
5185            .set_nonblocking(false)
5186            .expect("the backend peer reads blocking");
5187        back_peer
5188            .set_read_timeout(Some(Duration::from_secs(5)))
5189            .expect("the backend peer read is bounded");
5190        let mut buf = [0u8; 64];
5191        assert_eq!(
5192            back_peer.read(&mut buf).ok(),
5193            Some(0),
5194            "the backend socket is closed: its peer reads EOF"
5195        );
5196
5197        let (mux, _front_peer, _back_peer) =
5198            mux_after_final_goaway_with_a_linked_stream(&pool, true);
5199        assert_eq!(
5200            mux.router.backends.len(),
5201            1,
5202            "a graceful linger keeps its backend, as before"
5203        );
5204        assert_eq!(mux.context.streams[0].state, StreamState::Linked(Token(1)));
5205    }
5206
5207    /// A TLS frontend whose final GOAWAY carries an error code and leaves a
5208    /// stream open sends that GOAWAY without `close_notify` behind it
5209    /// (`ConnectionH2::output_flush_closes_connection` wants an empty stream
5210    /// table). The close its next writable pass decides queues the alert and
5211    /// turns into a pending linger (`H2Shell::linger_instead_of_closing`);
5212    /// when the kernel then refuses the alert's record, nothing re-arms the
5213    /// write and `Mux::ready_inner`'s loop ends with the linger still
5214    /// pending. The release must already have run by then: a stream queued
5215    /// in `pending_links` by the same pass is never routed nor dialed.
5216    ///
5217    /// TO SEE THIS RED: gate `Connection::lingers_after_error_goaway` on
5218    /// `is_lingering()` alone, as before. The `pending_links` pass then
5219    /// reaches the routing view for the stream and panics in
5220    /// `NoRoutingProxy` ("pending_links routed a stream of an error-GOAWAY
5221    /// linger").
5222    #[test]
5223    fn an_error_goaway_linger_pending_on_close_notify_releases_before_pending_links() {
5224        use std::io::Write;
5225
5226        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
5227        let (mut h2, _peer, _client) = h2::tests::rustls_h2_connection(&pool, H2State::Header);
5228        let mut context = test_context(&pool);
5229        let stream_id = context
5230            .create_stream(Ulid::generate(), 1 << 16)
5231            .expect("test context must create a stream");
5232        // A request the same read pass parsed whole and queued for a backend.
5233        context.streams[stream_id].state = StreamState::Link;
5234        context.pending_links.push_back(stream_id);
5235        h2.core.__test_insert_wire_mapping_only(1, stream_id);
5236
5237        assert!(
5238            matches!(
5239                h2.core.goaway(H2Error::EnhanceYourCalm),
5240                MuxResult::Continue
5241            ),
5242            "the GOAWAY is queued"
5243        );
5244        h2.flush_output_buffer();
5245        assert!(
5246            h2.core.output_pending().is_empty() && !h2.socket.socket_wants_write(),
5247            "premise: the GOAWAY reached the kernel, with no close_notify behind it"
5248        );
5249        // The client stops reading: fill the frontend's send queue so the
5250        // kernel refuses the close_notify record.
5251        let junk = [0u8; 4096];
5252        let mut filled = false;
5253        for _ in 0..4096 {
5254            match h2.socket.stream.write(&junk) {
5255                Ok(_) => {}
5256                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
5257                    filled = true;
5258                    break;
5259                }
5260                Err(e) => panic!("filling the frontend send queue failed: {e:?}"),
5261            }
5262        }
5263        assert!(filled, "premise: the frontend send queue is full");
5264        h2.core.readiness.event.insert(Ready::WRITABLE);
5265
5266        let mut mux = Mux {
5267            configured_frontend_timeout: Duration::from_secs(10),
5268            frontend_token: Token(0),
5269            frontend: Connection::H2(h2),
5270            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5271            context,
5272            session_ulid: Ulid::generate(),
5273            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(10))),
5274            backend_registry: BackendRegistry::default(),
5275            backends: Rc::default(),
5276        };
5277        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
5278        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(NoRoutingProxy));
5279        let mut metrics = SessionMetrics::new(None);
5280        let result = mux.ready(session, proxy, &mut metrics);
5281
5282        let Connection::H2(h2) = &mux.frontend else {
5283            unreachable!("the frontend was built as H2")
5284        };
5285        assert!(
5286            matches!(result, SessionResult::Continue)
5287                && matches!(h2.core.linger, shared::Linger::Pending { .. }),
5288            "premise: the pass ends with the linger pending on close_notify, got \
5289             {result:?} with {:?}",
5290            h2.core.linger
5291        );
5292        assert_eq!(
5293            mux.context.streams[stream_id].state,
5294            StreamState::Recycle,
5295            "the stream that will get no answer is released, not linked"
5296        );
5297        assert!(
5298            mux.context.pending_links.is_empty() && mux.router.backends.is_empty(),
5299            "nothing is left to link and no backend was dialed"
5300        );
5301    }
5302
5303    /// A soft stop must still close a lingering H2 session once the H2
5304    /// listener's graceful-shutdown budget has elapsed, even when the linger
5305    /// began before the budget ran out. The lingering `readable`/`writable`
5306    /// passes must adopt the pass's clock: `graceful_shutdown_deadline_elapsed`
5307    /// reads the connection's own snapshot, and one frozen at the linger's
5308    /// start would only ever see the budget as it stood then.
5309    ///
5310    /// TO SEE THIS RED: drop the `self.core.adopt_now(context.now)` from the
5311    /// lingering early returns of `H2Shell::readable` and `H2Shell::writable`.
5312    /// `shutting_down` then keeps the session.
5313    #[test]
5314    fn a_soft_stop_closes_a_lingering_h2_session_once_its_budget_elapses() {
5315        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
5316        let (socket, _peer) = connected_socket();
5317
5318        let mut h2 = h2::H2Shell::new(
5319            Ulid::generate(),
5320            socket,
5321            Position::Server,
5322            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
5323            H2FloodConfig::default(),
5324            H2ConnectionConfig::default(),
5325            Duration::from_secs(30),
5326            Some(Duration::from_secs(1)),
5327            Duration::from_secs(30),
5328            None,
5329            Ready::READABLE | Ready::HUP | Ready::ERROR,
5330        )
5331        .expect("a pool with free buffers must yield an H2 connection");
5332
5333        // A 1 s budget armed 3 s ago; the linger began 100 ms into it, with a
5334        // linger deadline far away.
5335        let armed_at = Instant::now() - Duration::from_secs(3);
5336        let linger_started = armed_at + Duration::from_millis(100);
5337        h2.core.state = H2State::Error;
5338        h2.core.drain.__test_arm_draining(armed_at);
5339        h2.core.adopt_now(linger_started);
5340        h2.core.linger = shared::Linger::Draining {
5341            deadline: linger_started + Duration::from_secs(60),
5342            remaining: shared::LINGER_MAX_BYTES,
5343        };
5344        let frontend = Connection::H2(h2);
5345        assert!(frontend.is_lingering(), "premise: the session lingers");
5346
5347        let mut mux = Mux {
5348            configured_frontend_timeout: Duration::from_secs(30),
5349            frontend_token: Token(0),
5350            frontend,
5351            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5352            context: test_context(&pool),
5353            session_ulid: Ulid::generate(),
5354            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(30))),
5355            backend_registry: BackendRegistry::default(),
5356            backends: Rc::default(),
5357        };
5358
5359        assert!(
5360            mux.shutting_down(),
5361            "the elapsed graceful-shutdown budget closes the lingering session"
5362        );
5363    }
5364
5365    /// `Mux::shutting_down` runs outside `ready()`, driven by
5366    /// `shut_down_sessions()`. It is therefore its own clock-sampling point,
5367    /// and that sample is load-bearing: it refreshes `context.now`, which
5368    /// `drive_frontend_shutdown_io` -> `readable()` mirrors into
5369    /// `ConnectionH2::now`, which the forced-close check then reads.
5370    ///
5371    /// A silent draining session gets no read/write events, so nothing else
5372    /// refreshes the snapshot. Without this sample the connection would carry
5373    /// the snapshot of its last `ready()` pass forever and the
5374    /// graceful-shutdown budget would never expire — the session would hang
5375    /// past the operator-configured SLA, which is the exact failure the
5376    /// forced-close deadline exists to prevent.
5377    ///
5378    /// The frontend is already draining, so `graceful_goaway` is NOT called on
5379    /// this path — that is what makes the test discriminating, because the
5380    /// `now` parameter of `graceful_goaway` cannot cover it.
5381    ///
5382    /// To SEE THIS RED: delete `self.context.now = now;` from
5383    /// `Mux::shutting_down` (keeping `let now` so `graceful_goaway(now)` still
5384    /// compiles). `readable()` then mirrors the frozen snapshot, the budget
5385    /// reads as 0s elapsed against a 5s deadline, and the final assertion fails
5386    /// with `assertion failed: mux.shutting_down()`.
5387    /// One `shutting_down()` pass on an idle draining H2 session makes exactly
5388    /// one `writable()` call: the forced pass, with nothing queued after it.
5389    ///
5390    /// `shut_down_sessions()` calls `shutting_down()` on every draining session
5391    /// at each 100 ms shutdown tick, so the work in one pass is multiplied by
5392    /// both. The loop in `Mux::drive_frontend_shutdown_io` used to wait for the
5393    /// WRITABLE event bit to clear, a bit that its own `signal_pending_write`
5394    /// had just set and that `writable()` clears only on `WouldBlock`, so an
5395    /// idle session with a writable socket ran all `MAX_LOOP_ITERATIONS`.
5396    /// Measured on the e2e drain test in a debug build: 10 000 calls and about
5397    /// 29 ms of CPU per pass, uncontended.
5398    ///
5399    /// To SEE THIS RED: restore the old exit condition,
5400    /// `!has_pending_write() && !readiness().event.is_writable()`; the count
5401    /// becomes `MAX_LOOP_ITERATIONS`.
5402    #[test]
5403    fn an_idle_draining_h2_session_makes_one_writable_pass_per_shutdown_tick() {
5404        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
5405        let (socket, _peer) = connected_socket();
5406
5407        let mut h2 = h2::H2Shell::new(
5408            Ulid::generate(),
5409            socket,
5410            Position::Server,
5411            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
5412            H2FloodConfig::default(),
5413            H2ConnectionConfig::default(),
5414            Duration::from_secs(30),
5415            Some(Duration::from_secs(3600)),
5416            Duration::from_secs(30),
5417            None,
5418            Ready::READABLE | Ready::HUP | Ready::ERROR,
5419        )
5420        .expect("a pool with free buffers must yield an H2 connection");
5421
5422        // Already draining, budget far from elapsed: the pass must neither
5423        // send a GOAWAY nor force-close, only drive the frontend I/O.
5424        let armed_at = Instant::now();
5425        h2.core.state = H2State::Header;
5426        h2.core.drain.__test_arm_draining(armed_at);
5427        let mut frontend = Connection::H2(h2);
5428
5429        let mut context = test_context(&pool);
5430        context.now = armed_at;
5431        // One live stream, so the session keeps draining instead of closing.
5432        let stream_id = context
5433            .create_stream(Ulid::generate(), 1 << 16)
5434            .expect("test context must create a stream");
5435        context.streams[stream_id].state = StreamState::Linked(Token(1));
5436        let Connection::H2(h2) = &mut frontend else {
5437            unreachable!("frontend was built as H2")
5438        };
5439        h2.core.__test_insert_wire_mapping_only(1, stream_id);
5440
5441        let mut mux = Mux {
5442            configured_frontend_timeout: Duration::from_secs(30),
5443            frontend_token: Token(0),
5444            frontend,
5445            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5446            context,
5447            session_ulid: Ulid::generate(),
5448            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(30))),
5449            backend_registry: BackendRegistry::default(),
5450            backends: Rc::default(),
5451        };
5452        assert!(
5453            !mux.frontend.has_pending_write(),
5454            "precondition: the idle draining session has nothing queued"
5455        );
5456
5457        SHUTDOWN_WRITABLE_PASSES.with(|passes| passes.set(0));
5458        assert!(
5459            !mux.shutting_down(),
5460            "a draining session with a live stream and budget left must stay open"
5461        );
5462        let passes = SHUTDOWN_WRITABLE_PASSES.with(|passes| passes.get());
5463        assert_eq!(
5464            passes, 1,
5465            "one shutdown pass on an idle draining H2 session must make exactly the \
5466             one forced writable() call, not spin until MAX_LOOP_ITERATIONS \
5467             ({MAX_LOOP_ITERATIONS}); got {passes}"
5468        );
5469        assert!(
5470            !mux.frontend.has_pending_write(),
5471            "the pass must leave nothing queued behind"
5472        );
5473    }
5474
5475    #[test]
5476    fn shutting_down_refreshes_the_snapshot_so_the_drain_budget_expires() {
5477        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
5478        let (socket, _peer) = connected_socket();
5479        let deadline = Duration::from_secs(5);
5480
5481        let mut h2 = h2::H2Shell::new(
5482            Ulid::generate(),
5483            socket,
5484            Position::Server,
5485            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
5486            H2FloodConfig::default(),
5487            H2ConnectionConfig::default(),
5488            Duration::from_secs(30),
5489            Some(deadline),
5490            Duration::from_secs(30),
5491            None,
5492            Ready::READABLE | Ready::HUP | Ready::ERROR,
5493        )
5494        .expect("a pool with free buffers must yield an H2 connection");
5495
5496        // A connection that already sent its initial GOAWAY and armed the
5497        // budget well over the deadline ago. `shutting_down` takes the
5498        // already-draining branch, which never calls `graceful_goaway`.
5499        let armed_at = Instant::now() - Duration::from_secs(60);
5500        h2.core.state = H2State::Header;
5501        h2.core.drain.__test_arm_draining(armed_at);
5502        let mut frontend = Connection::H2(h2);
5503
5504        let mut context = test_context(&pool);
5505        // Freeze the snapshot at the instant the budget was armed, as a
5506        // session that has seen no event since then would carry it.
5507        context.now = armed_at;
5508        // One live stream, so `can_stop` stays false and the forced-close
5509        // deadline is the only path that can return `true`.
5510        let stream_id = context
5511            .create_stream(Ulid::generate(), 1 << 16)
5512            .expect("test context must create a stream");
5513        context.streams[stream_id].state = StreamState::Linked(Token(1));
5514        // Give the connection a live WIRE stream too: with an empty
5515        // `ConnectionH2::streams`, `finalize_write` sends the FINAL GOAWAY and
5516        // force-disconnects, so `drive_frontend_shutdown_io` would return true
5517        // and short-circuit `shutting_down` before the deadline check. The
5518        // per-stream activity maps are deliberately left empty so
5519        // `cancel_timed_out_streams` early-returns and cannot queue an RST.
5520        let Connection::H2(h2) = &mut frontend else {
5521            unreachable!("frontend was built as H2")
5522        };
5523        h2.core.__test_insert_wire_mapping_only(1, stream_id);
5524
5525        let mut mux = Mux {
5526            configured_frontend_timeout: Duration::from_secs(30),
5527            frontend_token: Token(0),
5528            frontend,
5529            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5530            context,
5531            session_ulid: Ulid::generate(),
5532            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(30))),
5533            backend_registry: BackendRegistry::default(),
5534            backends: Rc::default(),
5535        };
5536
5537        assert!(
5538            mux.frontend.is_draining(),
5539            "precondition: the frontend must already be draining, so this test \
5540             exercises the branch that never reaches graceful_goaway"
5541        );
5542
5543        assert!(
5544            mux.shutting_down(),
5545            "shutting_down must refresh the clock snapshot itself, so a silent \
5546             draining session's forced-close budget still expires"
5547        );
5548    }
5549
5550    /// sozu-proxy/sozu#1647: a CONTINUATION that `drive_frontend_shutdown_io`
5551    /// reads completes the block outside `Mux::ready`, so its stream is left
5552    /// `StreamState::Link` until the next `ready` pass links it. A draining
5553    /// H2 session must stay open for it, within budget, rather than drop a
5554    /// request the advisory GOAWAY invited.
5555    #[test]
5556    fn a_draining_h2_session_waits_for_a_stream_awaiting_its_link() {
5557        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
5558        let (socket, _peer) = connected_socket();
5559
5560        let mut h2 = h2::H2Shell::new(
5561            Ulid::generate(),
5562            socket,
5563            Position::Server,
5564            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
5565            H2FloodConfig::default(),
5566            H2ConnectionConfig::default(),
5567            Duration::from_secs(30),
5568            Some(Duration::from_secs(60)),
5569            Duration::from_secs(30),
5570            None,
5571            Ready::READABLE | Ready::HUP | Ready::ERROR,
5572        )
5573        .expect("a pool with free buffers must yield an H2 connection");
5574        // Already draining, budget armed now: nothing but the stream below
5575        // can keep the session open.
5576        h2.core.state = H2State::Header;
5577        h2.core.drain.__test_arm_draining(Instant::now());
5578        let mut frontend = Connection::H2(h2);
5579
5580        let mut context = test_context(&pool);
5581        let stream_id = context
5582            .create_stream(Ulid::generate(), 1 << 16)
5583            .expect("test context must create a stream");
5584        context.streams[stream_id].state = StreamState::Link;
5585        // A live wire stream, so `finalize_write` does not send the final
5586        // GOAWAY and close through `drive_frontend_shutdown_io`.
5587        let Connection::H2(h2) = &mut frontend else {
5588            unreachable!("frontend was built as H2")
5589        };
5590        h2.core.__test_insert_wire_mapping_only(1, stream_id);
5591
5592        let mut mux = Mux {
5593            configured_frontend_timeout: Duration::from_secs(30),
5594            frontend_token: Token(0),
5595            frontend,
5596            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5597            context,
5598            session_ulid: Ulid::generate(),
5599            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(30))),
5600            backend_registry: BackendRegistry::default(),
5601            backends: Rc::default(),
5602        };
5603        assert!(
5604            !mux.frontend.graceful_shutdown_deadline_elapsed(),
5605            "precondition: the forced-close budget has not elapsed"
5606        );
5607
5608        assert!(
5609            !mux.shutting_down(),
5610            "a draining H2 session must stay open for a stream awaiting its link"
5611        );
5612        assert_eq!(
5613            mux.context.streams[stream_id].state,
5614            StreamState::Link,
5615            "the shutdown pass must leave the stream for `Mux::ready` to link"
5616        );
5617    }
5618
5619    #[test]
5620    fn a_short_read_after_hup_keeps_readable_to_read_the_eof() {
5621        let mut readiness = Readiness {
5622            event: Ready::READABLE | Ready::HUP,
5623            interest: Ready::READABLE,
5624        };
5625
5626        let should_yield =
5627            update_readiness_after_read(17, SocketResult::WouldBlock, &mut readiness);
5628
5629        assert!(!should_yield);
5630        assert!(
5631            readiness.event.is_readable(),
5632            "no edge will announce the queued EOF: READABLE must stay"
5633        );
5634    }
5635
5636    /// The case the HUP exception exists for, on a real loopback socket
5637    /// registered edge-triggered with mio: the data and the FIN arrive before
5638    /// the worker polls, so ONE event carries both `readable` and
5639    /// `read_closed`. The short read returns the data and stops before the
5640    /// EOF, and no later event will ever announce it. READABLE must survive
5641    /// `update_readiness_after_read` so the next pass reads `(0, Closed)`.
5642    ///
5643    /// To SEE THIS RED: remove the `is_hup()` exception from
5644    /// `update_readiness_after_read`; READABLE is dropped after the short read
5645    /// and the queued EOF is never read.
5646    #[test]
5647    fn data_and_fin_in_one_edge_keep_readable_until_the_eof_is_read() {
5648        use std::io::Write as _;
5649
5650        use crate::socket::SocketHandler as _;
5651
5652        let token = mio::Token(9);
5653        let mut poll = mio::Poll::new().expect("a poll instance must open");
5654        let mut events = mio::Events::with_capacity(8);
5655        let listener = std::net::TcpListener::bind("127.0.0.1:0")
5656            .expect("test listener must bind to a loopback port");
5657        let mut client = std::net::TcpStream::connect(
5658            listener
5659                .local_addr()
5660                .expect("test listener must report its local address"),
5661        )
5662        .expect("loopback connect must complete");
5663        let (accepted, _) = listener.accept().expect("the connection must be accepted");
5664        accepted
5665            .set_nonblocking(true)
5666            .expect("mio requires a nonblocking stream");
5667        let mut server = mio::net::TcpStream::from_std(accepted);
5668
5669        // Data and FIN are both queued before the socket is registered, so
5670        // the first poll reports them in a single edge.
5671        client.write_all(b"DATA").expect("the client must write");
5672        client
5673            .shutdown(std::net::Shutdown::Write)
5674            .expect("the client must half-close");
5675        poll.registry()
5676            .register(&mut server, token, mio::Interest::READABLE)
5677            .expect("the stream must register");
5678        poll.poll(&mut events, Some(std::time::Duration::from_secs(5)))
5679            .expect("poll must succeed");
5680        let event = events
5681            .iter()
5682            .find(|event| event.token() == token)
5683            .expect("the arrival must raise an event");
5684        assert!(
5685            event.is_readable() && event.is_read_closed(),
5686            "premise: data and FIN must share one edge, got {event:?}"
5687        );
5688        let mut readiness = Readiness {
5689            event: Ready::from(event),
5690            interest: Ready::READABLE,
5691        };
5692
5693        let mut buf = vec![0u8; 16 * 1024];
5694        let (size, status) = server.socket_read(&mut buf);
5695        assert_eq!((size, status), (4, SocketResult::WouldBlock));
5696        assert!(!update_readiness_after_read(size, status, &mut readiness));
5697        assert!(
5698            readiness.event.is_readable(),
5699            "HUP was seen: READABLE must stay so the EOF is read"
5700        );
5701
5702        poll.poll(&mut events, Some(std::time::Duration::from_millis(300)))
5703            .expect("poll must succeed");
5704        assert!(
5705            events.iter().all(|event| event.token() != token),
5706            "edge-triggered: no further event announces the queued EOF"
5707        );
5708
5709        let (size, status) = server.socket_read(&mut buf);
5710        assert_eq!((size, status), (0, SocketResult::Closed));
5711        assert!(update_readiness_after_read(size, status, &mut readiness));
5712        assert!(!readiness.event.is_readable());
5713    }
5714
5715    #[test]
5716    fn a_short_read_without_hup_drops_readable() {
5717        let mut readiness = Readiness {
5718            event: Ready::READABLE,
5719            interest: Ready::READABLE,
5720        };
5721
5722        let should_yield =
5723            update_readiness_after_read(17, SocketResult::WouldBlock, &mut readiness);
5724
5725        assert!(!should_yield);
5726        assert!(!readiness.event.is_readable(), "the next edge re-arms it");
5727    }
5728
5729    #[test]
5730    fn an_empty_read_after_hup_still_drops_readable() {
5731        let mut readiness = Readiness {
5732            event: Ready::READABLE | Ready::HUP,
5733            interest: Ready::READABLE,
5734        };
5735
5736        let should_yield = update_readiness_after_read(0, SocketResult::WouldBlock, &mut readiness);
5737
5738        assert!(should_yield);
5739        assert!(
5740            !readiness.event.is_readable(),
5741            "the exception must not spin"
5742        );
5743    }
5744
5745    #[test]
5746    fn update_readiness_after_read_closed_keeps_writable() {
5747        let mut readiness = Readiness {
5748            event: Ready::READABLE | Ready::WRITABLE | Ready::HUP,
5749            interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP,
5750        };
5751
5752        let should_yield = update_readiness_after_read(17, SocketResult::Closed, &mut readiness);
5753
5754        assert!(!should_yield);
5755        assert!(!readiness.event.is_readable());
5756        assert!(readiness.event.is_writable());
5757        assert!(readiness.event.is_hup());
5758    }
5759
5760    /// Build a minimal H1 frontend `Mux` with one `Idle` stream. The returned
5761    /// peer socket must be kept alive for the duration of the test, otherwise
5762    /// the loopback connection is torn down and `readable()` sees a forced
5763    /// disconnect.
5764    ///
5765    /// An H1 frontend is deliberate: the `StreamState::Idle` arm of
5766    /// `Mux::timeout` writes a 408 and stamps
5767    /// `stream.context.access_log_message = Some("client_timeout")`, which is
5768    /// the crispest available witness that the timeout BODY ran. The H2 `Idle`
5769    /// arm is silently ignored and would witness nothing.
5770    fn h1_mux_with_idle_stream(
5771        pool: &Rc<RefCell<Pool>>,
5772        frontend_timeout: Duration,
5773    ) -> (
5774        Mux<mio::net::TcpStream, test_support::TestListener>,
5775        std::net::TcpStream,
5776    ) {
5777        let (socket, peer) = connected_socket();
5778        let frontend = Connection::new_h1_server(Ulid::generate(), socket, frontend_timeout);
5779        let mut context = test_context(pool);
5780        context
5781            .create_stream(Ulid::generate(), 1 << 16)
5782            .expect("test context must create a stream");
5783        let mux = Mux {
5784            configured_frontend_timeout: frontend_timeout,
5785            frontend_token: Token(0),
5786            frontend,
5787            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
5788            context,
5789            session_ulid: Ulid::generate(),
5790            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(frontend_timeout)),
5791            backend_registry: BackendRegistry::default(),
5792            backends: Rc::default(),
5793        };
5794        (mux, peer)
5795    }
5796
5797    /// Stage the exact shape of sozu-proxy/sozu#1442: a request fully written
5798    /// onto a REUSED keep-alive upstream that then closed without answering.
5799    ///
5800    /// `front.consumed` is what `end_stream_decision` reads to mean "the
5801    /// request left the front buffer"; the armed `retry_buffer` is what
5802    /// `ConnectionH1::writable` fills, and only ever on a connection whose
5803    /// `reused_from_pool` is set.
5804    fn stale_pooled_upstream_stream(
5805        pool: &Rc<RefCell<Pool>>,
5806        method: Method,
5807    ) -> (
5808        Mux<mio::net::TcpStream, test_support::TestListener>,
5809        std::net::TcpStream,
5810    ) {
5811        let (mut mux, peer) = h1_mux_with_idle_stream(pool, Duration::from_secs(60));
5812        let stream = &mut mux.context.streams[0];
5813        // The capture is armed while the method is still `Get`, and the method
5814        // under test is set afterwards. Since sozu-proxy/sozu#1450
5815        // `arm_upstream_replay` refuses to capture a non-idempotent request at
5816        // all, so arming under `method` would leave the POST and `PATCH` cases
5817        // below with no capture — and they would then be observing the ARM
5818        // guard rather than the conjunct they exist to pin. Staging it this way
5819        // keeps `Stream::can_replay_on_fresh_upstream`'s own idempotence
5820        // conjunct as the thing under test: dropping that conjunct still
5821        // reddens them. Production cannot reach this state any more, because a
5822        // request's method does not change after its headers are parsed, which
5823        // is exactly why the conjunct is now defense in depth and why these
5824        // tests are what keep it honest.
5825        stream.context.method = Some(Method::Get);
5826        stream.arm_upstream_replay();
5827        stream
5828            .retry_buffer
5829            .as_mut()
5830            .expect("arm_upstream_replay must install a buffer")
5831            .extend_from_slice(b"GET /api HTTP/1.1\r\nHost: localhost\r\n\r\n");
5832        stream.context.method = Some(method);
5833        stream.front.consumed = true;
5834        // The upstream produced nothing: not in body phase, nothing forwarded,
5835        // nothing buffered.
5836        assert!(
5837            !stream.back.is_main_phase() && !stream.back.consumed && stream.back.storage.is_empty(),
5838            "precondition: the upstream must have produced no response at all"
5839        );
5840        (mux, peer)
5841    }
5842
5843    /// Concatenate every byte `ConnectionH1::writable` would hand the socket,
5844    /// in queue order. Mirrors its own `kawa.out` walk.
5845    fn queued_request_bytes(front: &GenericHttpStream) -> Vec<u8> {
5846        let buffer = front.storage.buffer();
5847        front
5848            .out
5849            .iter()
5850            .map(|block| match block {
5851                kawa::OutBlock::Store(store) => store.data(buffer).to_vec(),
5852                kawa::OutBlock::Delimiter => Vec::new(),
5853            })
5854            .collect::<Vec<_>>()
5855            .concat()
5856    }
5857
5858    /// Stage the partial-write shape of sozu-proxy/sozu#1442 on the front
5859    /// kawa: a real request, parsed and serialized exactly as
5860    /// `ConnectionH1::writable` serializes it, of which the socket accepted
5861    /// everything but the last `unwritten_tail` bytes.
5862    ///
5863    /// `kawa::Kawa::consume` pushes the partially consumed store back to the
5864    /// FRONT of `out` (kawa-0.7.1 `storage/repr.rs`), so the refused tail
5865    /// stays queued ahead of anything appended afterwards. That is what makes
5866    /// the queue position of the replay load-bearing.
5867    ///
5868    /// Returns the complete serialization, which a correct replay reproduces
5869    /// byte-for-byte.
5870    fn stage_partially_written_pooled_request(
5871        stream: &mut Stream,
5872        unwritten_tail: usize,
5873    ) -> Vec<u8> {
5874        let request: &[u8] = b"GET /api HTTP/1.1\r\nHost: localhost\r\nX-Tail: unwritten\r\n\r\n";
5875        stream.front.storage.space()[..request.len()].copy_from_slice(request);
5876        stream.front.storage.fill(request.len());
5877        kawa::h1::parse(&mut stream.front, &mut stream.context);
5878        assert!(
5879            stream.front.is_main_phase(),
5880            "the staged request must parse"
5881        );
5882        stream.front.prepare(&mut kawa::h1::BlockConverter);
5883        assert!(
5884            stream.front.blocks.is_empty(),
5885            "prepare must drain blocks, as it has by the time writable writes"
5886        );
5887
5888        let serialized = queued_request_bytes(&stream.front);
5889        let accepted = serialized
5890            .len()
5891            .checked_sub(unwritten_tail)
5892            .expect("the unwritten tail must fit inside the serialized request");
5893        stream.context.method = Some(Method::Get);
5894        // Through `arm_upstream_replay` rather than by assigning the field:
5895        // the capture owns a charge against `MAX_ARMED_REPLAY_CAPTURES`, so a
5896        // hand-built one would release a charge it never took.
5897        stream.arm_upstream_replay();
5898        stream
5899            .retry_buffer
5900            .as_mut()
5901            .expect("arm_upstream_replay must install a buffer")
5902            .extend_from_slice(&serialized[..accepted]);
5903        stream.front.consume(accepted);
5904        stream.front.consumed = true;
5905        serialized
5906    }
5907
5908    /// The replay must be queued AHEAD of the bytes the kernel refused.
5909    ///
5910    /// `ConnectionH1::writable` handles a partial `socket_write_vectored` by
5911    /// calling `signal_pending_write()` and returning `MuxResult::Continue`,
5912    /// which leaves the unwritten remainder queued: `kawa::Kawa::consume`
5913    /// pushes the partially consumed store back to the FRONT of `out`
5914    /// (kawa-0.7.1 `storage/repr.rs`). `Kawa::push_out` APPENDS, so queueing
5915    /// the capture with it would hand the healthy backend `[tail][head]` — a
5916    /// request mangled mid-token, on the wire, to a backend that never saw
5917    /// the first attempt.
5918    ///
5919    /// Reachable on an idempotent request large enough that the kernel
5920    /// accepts only part of one write to a pooled upstream, followed by that
5921    /// stale peer EOFing without answering.
5922    ///
5923    /// The remainder spans zero, one, three and thirteen `out` entries across
5924    /// the cases below: `consume` pushes back at most ONE partially consumed
5925    /// store and drops every entry before it, so how many survive depends on
5926    /// where the split falls. Prepending is correct for all of them because
5927    /// `VecDeque::push_front` preserves the relative order of what is already
5928    /// queued, and the capture is exactly the complementary prefix.
5929    ///
5930    /// To SEE THIS RED: in `Stream::queue_upstream_replay`, swap the
5931    /// `front.out.push_front(...)` back for `self.front.push_out(...)`.
5932    #[test]
5933    fn a_replay_is_queued_ahead_of_the_unwritten_tail() {
5934        for (unwritten_tail, queued_entries) in [(0, 0), (2, 1), (21, 3), (100, 13)] {
5935            let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
5936            let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
5937            let stream = &mut mux.context.streams[0];
5938            let serialized = stage_partially_written_pooled_request(stream, unwritten_tail);
5939            assert_eq!(
5940                stream.front.out.len(),
5941                queued_entries,
5942                "staging a {unwritten_tail}-byte tail must leave {queued_entries} queued entries"
5943            );
5944            assert_eq!(
5945                shared::end_stream_decision(stream),
5946                shared::EndStreamAction::ReplayOnFreshBackend,
5947                "a partially written request on a stale pooled upstream is still replayable"
5948            );
5949
5950            stream
5951                .queue_upstream_replay()
5952                .expect("the staged stream carries a capture");
5953
5954            assert_eq!(
5955                queued_request_bytes(&stream.front),
5956                serialized,
5957                "with a {unwritten_tail}-byte unwritten tail the fresh backend must \
5958                 receive the request in its original order, not the tail first"
5959            );
5960        }
5961    }
5962
5963    /// The defect of sozu-proxy/sozu#1442. A request written onto a pooled
5964    /// keep-alive upstream that the peer had already closed must be replayed
5965    /// on a fresh backend, not answered `502 Bad Gateway`: nothing was
5966    /// observed by the client, so re-issuing it is unobservable. Measured
5967    /// 2026-09-22 with only the replay decision reverted: 25 failures over
5968    /// 576 pooled `test_issue_806` trials (4.3% per trial), 25 red runs of
5969    /// 25; green on 25 of 25 with the replay in place.
5970    ///
5971    /// To SEE THIS RED: in `super::shared::end_stream_decision`, replace the
5972    /// `can_replay_on_fresh_upstream()` branch with a bare
5973    /// `EndStreamAction::SendDefault(502)` — the pre-fix shape.
5974    #[test]
5975    fn a_stale_pooled_upstream_that_never_answered_is_replayed_not_502() {
5976        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
5977        let (mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Get);
5978        assert_eq!(
5979            shared::end_stream_decision(&mux.context.streams[0]),
5980            shared::EndStreamAction::ReplayOnFreshBackend,
5981        );
5982    }
5983
5984    /// The method veto. nginx keeps `non_idempotent` out of the
5985    /// `proxy_next_upstream` default so `POST, LOCK, PATCH` are "not passed
5986    /// to the next server if a request has been sent to an upstream server";
5987    /// pingora's default `error_while_proxy` calls `set_retry(false)` on
5988    /// `!method.is_idempotent()`. Replaying a POST can duplicate a write the
5989    /// origin may already have committed.
5990    ///
5991    /// To SEE THIS RED: drop the `Method::is_idempotent` conjunct from
5992    /// `Stream::can_replay_on_fresh_upstream`.
5993    #[test]
5994    fn a_non_idempotent_request_is_never_replayed_on_a_stale_upstream() {
5995        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
5996        let (mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Post);
5997        assert_eq!(
5998            shared::end_stream_decision(&mux.context.streams[0]),
5999            shared::EndStreamAction::SendDefault(502),
6000        );
6001    }
6002
6003    /// An extension method sozu does not know the semantics of is treated
6004    /// like POST. nginx's list is a closed `POST, LOCK, PATCH`; sozu parses
6005    /// `PATCH` as `Method::Custom`, so refusing every `Custom` is what makes
6006    /// PATCH non-replayable here without maintaining a second list.
6007    #[test]
6008    fn an_unknown_method_is_never_replayed_on_a_stale_upstream() {
6009        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6010        let (mux, _peer) = stale_pooled_upstream_stream(&pool, Method::new(b"PATCH"));
6011        assert!(matches!(
6012            mux.context.streams[0].context.method,
6013            Some(Method::Custom(_))
6014        ));
6015        assert_eq!(
6016            shared::end_stream_decision(&mux.context.streams[0]),
6017            shared::EndStreamAction::SendDefault(502),
6018        );
6019    }
6020
6021    /// pingora's `RetryType::ReusedOnly`. The same EOF means "the pool handed
6022    /// out a socket the peer had already closed" on a reused connection and
6023    /// "the origin accepted the request and then died" on a fresh dial; only
6024    /// the first is replayable. A fresh dial never arms the capture, so the
6025    /// absent buffer IS the provenance check.
6026    ///
6027    /// To SEE THIS RED: drop the `retry_buffer.is_some()` conjunct from
6028    /// `Stream::can_replay_on_fresh_upstream` — the provenance check itself.
6029    /// NOT `ConnectionH1::writable`'s `reused_from_pool` guard: this test
6030    /// never calls `writable`. It stages the capture through
6031    /// `stale_pooled_upstream_stream` and clears it with
6032    /// `Stream::forget_upstream_replay`, so a recipe naming the write path
6033    /// cannot redden it however plausible it reads.
6034    #[test]
6035    fn a_request_written_to_a_freshly_dialled_upstream_is_not_replayed() {
6036        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6037        let (mut mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Get);
6038        mux.context.streams[0].forget_upstream_replay();
6039        assert_eq!(
6040            shared::end_stream_decision(&mux.context.streams[0]),
6041            shared::EndStreamAction::SendDefault(502),
6042        );
6043    }
6044
6045    /// The boundary itself: once a response byte exists the request is no
6046    /// longer replayable. nginx — "passing a request to the next server is
6047    /// only possible if nothing has been sent to a client yet". A byte merely
6048    /// BUFFERED is already refused here, which is stricter: a partial status
6049    /// line is HAProxy's `junk-response`, absent from every default.
6050    ///
6051    /// To SEE THIS RED: drop the `back.storage.is_empty()` conjunct from
6052    /// `Stream::can_replay_on_fresh_upstream`.
6053    #[test]
6054    fn a_buffered_response_byte_forecloses_the_replay() {
6055        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6056        let (mut mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Get);
6057        let back = &mut mux.context.streams[0].back;
6058        back.storage.space()[..4].copy_from_slice(b"HTTP");
6059        back.storage.fill(4);
6060        assert!(!back.is_main_phase(), "a bare `HTTP` is not yet a response");
6061        assert_eq!(
6062            shared::end_stream_decision(&mux.context.streams[0]),
6063            shared::EndStreamAction::SendDefault(502),
6064        );
6065    }
6066
6067    /// A response byte already FORWARDED to the client is the same refusal,
6068    /// reached through the other conjunct.
6069    ///
6070    /// To SEE THIS RED: drop the `!back.consumed` conjunct from
6071    /// `Stream::can_replay_on_fresh_upstream`.
6072    #[test]
6073    fn a_forwarded_response_byte_forecloses_the_replay() {
6074        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6075        let (mut mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Get);
6076        mux.context.streams[0].back.consumed = true;
6077        assert_eq!(
6078            shared::end_stream_decision(&mux.context.streams[0]),
6079            shared::EndStreamAction::SendDefault(502),
6080        );
6081    }
6082
6083    /// The replay re-serializes the captured bytes and nothing else: they go
6084    /// back into `front.out` as an owned store, `front.blocks` stays empty so
6085    /// `kawa::Kawa::prepare` contributes nothing, and the buffer is taken so
6086    /// one capture cannot be replayed twice. That is not a per-REQUEST bound:
6087    /// `ConnectionH1::start_stream` arms a fresh capture on the next pooled
6088    /// attempt, and `max_connection_attempts` is what bounds the request as a
6089    /// whole.
6090    ///
6091    /// To SEE THIS RED: make `Stream::queue_upstream_replay` clone the buffer
6092    /// instead of taking it.
6093    #[test]
6094    fn queueing_a_replay_moves_the_captured_bytes_into_the_front_buffer() {
6095        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6096        let (mut mux, _peer) = stale_pooled_upstream_stream(&pool, Method::Get);
6097        let stream = &mut mux.context.streams[0];
6098        let captured = stream
6099            .retry_buffer
6100            .as_ref()
6101            .expect("the staged stream carries a capture")
6102            .to_vec();
6103
6104        assert_eq!(stream.queue_upstream_replay(), Some(captured.len()));
6105
6106        assert!(
6107            stream.retry_buffer.is_none(),
6108            "the capture must be taken, not cloned: it cannot outlive its replay"
6109        );
6110        assert!(
6111            stream.front.blocks.is_empty(),
6112            "prepare must have nothing to convert, or it would prepend a \
6113             second copy of the request line"
6114        );
6115        let buffer = stream.front.storage.buffer();
6116        let queued: Vec<u8> = stream
6117            .front
6118            .out
6119            .iter()
6120            .map(|block| match block {
6121                kawa::OutBlock::Store(store) => store.data(buffer).to_vec(),
6122                kawa::OutBlock::Delimiter => Vec::new(),
6123            })
6124            .collect::<Vec<_>>()
6125            .concat();
6126        assert_eq!(
6127            queued, captured,
6128            "the replayed request must be byte-identical to the first attempt"
6129        );
6130        assert_eq!(
6131            shared::end_stream_decision(stream),
6132            shared::EndStreamAction::SendDefault(502),
6133            "with the capture spent and nothing re-arming it, a second stale \
6134             upstream falls back to 502"
6135        );
6136    }
6137
6138    /// Park the frontend core's next deadline at an exact instant, then push it
6139    /// onto the wheel the way a real pass would.
6140    ///
6141    /// Writes `ConnectionH1::timeout_deadline` directly rather than going
6142    /// through `arm_timeout`, so a test can stage a deadline in the past ("the
6143    /// wheel is late") as easily as one in the future, without disturbing the
6144    /// configured duration the re-arm will use.
6145    fn arm_frontend_at(
6146        mux: &mut Mux<mio::net::TcpStream, test_support::TestListener>,
6147        deadline: Option<Instant>,
6148    ) {
6149        let Connection::H1(h1) = &mut mux.frontend else {
6150            unreachable!("the helper builds an H1 frontend")
6151        };
6152        h1.timeout_deadline = deadline;
6153        mux.reschedule();
6154    }
6155
6156    /// Same, for a backend connection that has not been inserted yet.
6157    fn park_backend_deadline(connection: &mut Connection<SessionTcpStream>, deadline: Instant) {
6158        let Connection::H1(h1) = connection else {
6159            unreachable!("the helper builds an H1 backend")
6160        };
6161        h1.timeout_deadline = Some(deadline);
6162    }
6163
6164    /// Fire the frontend timeout of `mux` now, on an H1 frontend whose
6165    /// lingering close may take `linger_timeout`, after `partial` request
6166    /// bytes arrived. Returns the result, the instants bracketing the call,
6167    /// and the frontend's linger state and deadline afterwards.
6168    fn fire_frontend_timeout_after(
6169        partial: &[u8],
6170        linger_timeout: Duration,
6171    ) -> (
6172        StateResult,
6173        Instant,
6174        Instant,
6175        h1::Linger,
6176        Option<Instant>,
6177        std::net::TcpStream,
6178    ) {
6179        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6180        let (mut mux, peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6181        let mut metrics = SessionMetrics::new(None);
6182        {
6183            let Connection::H1(h1) = &mut mux.frontend else {
6184                unreachable!("the helper builds an H1 frontend")
6185            };
6186            h1.linger_timeout = linger_timeout;
6187        }
6188        let storage = &mut mux.context.streams[0].front.storage;
6189        storage.space()[..partial.len()].copy_from_slice(partial);
6190        storage.fill(partial.len());
6191        arm_frontend_at(&mut mux, Some(Instant::now() - Duration::from_millis(1)));
6192
6193        let before = Instant::now();
6194        let result = mux.timeout(Token(0), &mut metrics);
6195        let after = Instant::now();
6196        let Connection::H1(h1) = &mux.frontend else {
6197            unreachable!("the helper builds an H1 frontend")
6198        };
6199        (
6200            result,
6201            before,
6202            after,
6203            h1.linger,
6204            mux.frontend.poll_timeout(),
6205            peer,
6206        )
6207    }
6208
6209    /// A frontend timeout answers a partly received request (408), and the
6210    /// write pass that sends the answer starts the lingering close. The
6211    /// re-arm `Mux::timeout_inner` runs after that pass must not replace the
6212    /// linger deadline (`request_timeout`, 5 s here) with the frontend
6213    /// timeout (60 s).
6214    ///
6215    /// TO SEE THIS RED: remove the `is_lingering()` guard of
6216    /// `ConnectionH1::arm_timeout`. The deadline is then about 60 s out.
6217    #[test]
6218    fn a_linger_started_by_a_timeout_write_keeps_its_own_deadline() {
6219        let linger = Duration::from_secs(5);
6220        let (result, before, after, state, deadline, _peer) =
6221            fire_frontend_timeout_after(b"POST /upl", linger);
6222
6223        assert_eq!(result, StateResult::Continue, "the session lingers");
6224        assert!(
6225            matches!(state, h1::Linger::Draining { .. }),
6226            "a partly received request is drained after its answer, got {state:?}"
6227        );
6228        let deadline = deadline.expect("a lingering frontend has a deadline");
6229        assert!(
6230            deadline >= before + linger && deadline <= after + linger,
6231            "the deadline is the linger's, {:?} from the call",
6232            deadline.saturating_duration_since(before)
6233        );
6234    }
6235
6236    /// A silent client — no byte of a request — gets its 408 and an
6237    /// immediate close: nothing is in flight to drain.
6238    ///
6239    /// TO SEE THIS RED: in `ConnectionH1::writable`, set the linger on
6240    /// `!request_complete` alone, without `request_started`.
6241    #[test]
6242    fn a_408_to_a_silent_client_closes_without_lingering() {
6243        let (result, _, _, state, _, mut peer) =
6244            fire_frontend_timeout_after(b"", Duration::from_secs(5));
6245
6246        assert_eq!(
6247            result,
6248            StateResult::CloseSession,
6249            "nothing was received: the 408 closes at once"
6250        );
6251        assert_eq!(state, h1::Linger::Off, "no linger for a silent client");
6252        peer.set_nonblocking(false)
6253            .expect("the peer reads blocking");
6254        peer.set_read_timeout(Some(Duration::from_secs(1)))
6255            .expect("the peer read is bounded");
6256        let mut head = [0u8; 12];
6257        std::io::Read::read_exact(&mut peer, &mut head).expect("the 408 reaches the client");
6258        assert_eq!(&head, b"HTTP/1.1 408", "the client gets its 408");
6259    }
6260
6261    /// A frontend socket standing for TLS: its output is buffered behind the
6262    /// socket (`socket_wants_write`) until the next write flushes it, and a
6263    /// close queues a `close_notify` the same way.
6264    #[derive(Debug)]
6265    struct BufferedTlsSocket {
6266        stream: mio::net::TcpStream,
6267        buffered: bool,
6268    }
6269
6270    impl SocketHandler for BufferedTlsSocket {
6271        fn socket_read(&mut self, buf: &mut [u8]) -> (usize, SocketResult) {
6272            self.stream.socket_read(buf)
6273        }
6274
6275        fn socket_write(&mut self, buf: &[u8]) -> (usize, SocketResult) {
6276            self.stream.socket_write(buf)
6277        }
6278
6279        fn socket_write_vectored(&mut self, bufs: &[std::io::IoSlice]) -> (usize, SocketResult) {
6280            // What was buffered leaves with this write.
6281            self.buffered = false;
6282            self.stream.socket_write_vectored(bufs)
6283        }
6284
6285        fn socket_wants_write(&self) -> bool {
6286            self.buffered
6287        }
6288
6289        fn socket_close(&mut self) {
6290            self.buffered = true;
6291        }
6292
6293        fn socket_ref(&self) -> &mio::net::TcpStream {
6294            &self.stream
6295        }
6296
6297        fn socket_mut(&mut self) -> &mut mio::net::TcpStream {
6298            &mut self.stream
6299        }
6300
6301        fn peer_addr(&self) -> Option<std::net::SocketAddr> {
6302            self.stream.peer_addr().ok()
6303        }
6304
6305        fn protocol(&self) -> crate::socket::TransportProtocol {
6306            crate::socket::TransportProtocol::Tcp
6307        }
6308
6309        fn read_error(&self) {}
6310
6311        fn write_error(&self) {}
6312    }
6313
6314    /// A TLS client half-closes while the answer to its partly received
6315    /// request is still buffered (a 408 from `Mux::timeout_inner`): the
6316    /// answer is flushed, then the `close_notify`, and the lingering close
6317    /// starts inside the loop (RFC 9112 §9.6, `ConnectionH1::start_linger`).
6318    /// The in-loop HUP check must leave that frontend to its drain: closing it
6319    /// with the client's last bytes unread resets the connection.
6320    ///
6321    /// What is asserted is the socket, not the reader: whichever path
6322    /// consumed the client's last bytes, the request reader or the linger
6323    /// drain, none may be left unread at the close.
6324    ///
6325    /// TO SEE THIS RED: drop `c.readiness.filter_interest().is_readable() ||`
6326    /// from `Connection::has_unread_input`, which is what keeps a lingering
6327    /// frontend (READABLE raised by `ConnectionH1::start_linger`) from the
6328    /// in-loop close: 4 bytes are left unread. Before sozu-proxy/sozu#1779 the
6329    /// `!self.frontend.is_lingering()` guard of that check did this.
6330    #[test]
6331    fn a_lingering_frontend_reads_the_clients_last_bytes_before_closing() {
6332        use std::io::{Read, Write};
6333
6334        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6335        let (socket, mut peer) = connected_socket();
6336        let frontend_timeout = Duration::from_secs(60);
6337        let mut frontend = Connection::new_h1_server(
6338            Ulid::generate(),
6339            BufferedTlsSocket {
6340                stream: socket,
6341                buffered: true,
6342            },
6343            frontend_timeout,
6344        );
6345        let mut context = test_context(&pool);
6346        context
6347            .create_stream(Ulid::generate(), 1 << 16)
6348            .expect("test context must create a stream");
6349        let partial = b"POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 64\r\n\r\npart";
6350        {
6351            let Connection::H1(h1) = &mut frontend else {
6352                unreachable!("new_h1_server builds an H1 connection")
6353            };
6354            h1.stream = Some(0);
6355            let stream = &mut context.streams[0];
6356            let storage = &mut stream.front.storage;
6357            storage.space()[..partial.len()].copy_from_slice(partial);
6358            storage.fill(partial.len());
6359            let answers_rc = stream.answers.clone();
6360            let answers = answers_rc.borrow();
6361            set_default_answer(stream, &mut h1.readiness, 408, &answers);
6362        }
6363        let mut mux = Mux {
6364            configured_frontend_timeout: frontend_timeout,
6365            frontend_token: Token(0),
6366            frontend,
6367            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
6368            context,
6369            session_ulid: Ulid::generate(),
6370            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(frontend_timeout)),
6371            backend_registry: BackendRegistry::default(),
6372            backends: Rc::default(),
6373        };
6374
6375        // The client sends more of its body, then hangs up: one edge
6376        // carries both, as `EPOLLIN|EPOLLRDHUP`.
6377        peer.write_all(b"more").expect("the client peer writes");
6378        peer.shutdown(std::net::Shutdown::Write)
6379            .expect("the client peer sends its FIN");
6380        std::thread::sleep(Duration::from_millis(20));
6381        mux.frontend.readiness_mut().event = Ready::READABLE | Ready::HUP;
6382
6383        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
6384        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
6385        let mut metrics = SessionMetrics::new(None);
6386        let result = mux.ready(session, proxy, &mut metrics);
6387        let Connection::H1(h1) = &mux.frontend else {
6388            unreachable!("the frontend is H1")
6389        };
6390        // What the close must not leave behind: a byte the client sent and
6391        // sozu never read, which turns the close into a reset. Whichever
6392        // reader consumed them, the request reader or the linger drain, a
6393        // peek on the socket must meet the client's EOF, not data.
6394        let mut probe = [0u8; 16];
6395        let unread = match h1.socket.stream.peek(&mut probe) {
6396            Ok(n) => n,
6397            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => 0,
6398            Err(e) => panic!("the frontend socket peek failed: {e}"),
6399        };
6400        assert!(
6401            matches!(result, SessionResult::Close),
6402            "the drain meets the client's EOF and closes, got {result:?}"
6403        );
6404        assert_eq!(
6405            unread, 0,
6406            "the session must read the rest of the request before it closes, {unread} bytes \
6407             left unread, {:?}",
6408            h1.linger
6409        );
6410        drop(mux);
6411
6412        peer.set_nonblocking(false)
6413            .expect("the peer reads blocking");
6414        peer.set_read_timeout(Some(Duration::from_secs(1)))
6415            .expect("the peer read is bounded");
6416        let mut received = Vec::new();
6417        let outcome = peer.read_to_end(&mut received);
6418        assert!(
6419            received.starts_with(b"HTTP/1.1 408"),
6420            "the client gets its answer, got {:?}",
6421            String::from_utf8_lossy(&received)
6422        );
6423        assert!(
6424            outcome.is_ok(),
6425            "the response ends with the FIN, not a reset: {outcome:?}"
6426        );
6427    }
6428
6429    /// A request answered before its head was whole (a 408 from
6430    /// `Mux::timeout_inner` on an `Idle` stream) is never linked to a backend
6431    /// when the rest of its head arrives afterwards: the answer is final, and
6432    /// the bytes behind it are drained after it, never forwarded.
6433    ///
6434    /// TO SEE THIS RED: drop `&& !answered` from the first-seen-request branch
6435    /// of `ConnectionH1::readable`.
6436    #[test]
6437    fn an_answered_request_is_never_linked() {
6438        use std::io::Write;
6439
6440        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6441        let (socket, mut peer) = connected_socket();
6442        let frontend_timeout = Duration::from_secs(60);
6443        let mut frontend = Connection::new_h1_server(Ulid::generate(), socket, frontend_timeout);
6444        let mut context = test_context(&pool);
6445        context
6446            .create_stream(Ulid::generate(), 1 << 16)
6447            .expect("test context must create a stream");
6448        let head_start = b"GET /slow HTTP/1.1\r\nHost: loc";
6449        {
6450            let Connection::H1(h1) = &mut frontend else {
6451                unreachable!("new_h1_server builds an H1 connection")
6452            };
6453            h1.stream = Some(0);
6454            let stream = &mut context.streams[0];
6455            let storage = &mut stream.front.storage;
6456            storage.space()[..head_start.len()].copy_from_slice(head_start);
6457            storage.fill(head_start.len());
6458            let answers_rc = stream.answers.clone();
6459            let answers = answers_rc.borrow();
6460            set_default_answer(stream, &mut h1.readiness, 408, &answers);
6461        }
6462        assert_eq!(
6463            context.streams[0].state,
6464            StreamState::Unlinked,
6465            "premise: the 408 leaves the stream answered"
6466        );
6467
6468        peer.write_all(b"alhost\r\n\r\n")
6469            .expect("the client peer writes the rest of its head");
6470        std::thread::sleep(Duration::from_millis(20));
6471        frontend.readiness_mut().event.insert(Ready::READABLE);
6472        let mut router = Router::new(Duration::from_secs(30), Duration::from_secs(30));
6473        let _ = frontend.readable(&mut context, EndpointClient(&mut router));
6474
6475        assert!(
6476            context.streams[0].front.is_main_phase(),
6477            "premise: the rest of the head was read and parsed"
6478        );
6479        assert_eq!(
6480            context.streams[0].state,
6481            StreamState::Unlinked,
6482            "an answered request must stay answered"
6483        );
6484        assert!(
6485            context.pending_links.is_empty(),
6486            "an answered request must not be queued for a backend"
6487        );
6488    }
6489
6490    /// An H1 frontend whose request was received whole and whose response
6491    /// has not started, as one waiting for its backend, with nothing queued
6492    /// for the client. `event` is the readiness the next pass sees.
6493    fn waiting_request_pass(event: Ready) -> SessionResult {
6494        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6495        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6496        {
6497            let stream = &mut mux.context.streams[0];
6498            let request = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
6499            let storage = &mut stream.front.storage;
6500            storage.space()[..request.len()].copy_from_slice(request);
6501            storage.fill(request.len());
6502            kawa::h1::parse(&mut stream.front, &mut stream.context);
6503            assert!(
6504                stream.front.is_terminated(),
6505                "premise: the request is whole"
6506            );
6507            stream.state = StreamState::Unlinked;
6508        }
6509        let readiness = mux.frontend.readiness_mut();
6510        readiness.interest.remove(Ready::READABLE);
6511        readiness.event = event;
6512        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
6513        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
6514        let mut metrics = SessionMetrics::new(None);
6515        mux.ready(session, proxy, &mut metrics)
6516    }
6517
6518    /// A full hang-up whose reset a `read` or `write` already consumed comes
6519    /// as `EPOLLHUP` without `EPOLLERR`: HUP and WRITE_CLOSED, no ERROR. It
6520    /// closes the session at once, even with a request in flight and no write
6521    /// pending to fail; a half-close (HUP alone) keeps it for the response.
6522    ///
6523    /// TO SEE THIS RED: drop the `event.is_write_closed()` clause from
6524    /// `Mux::frontend_exchange_in_flight`.
6525    #[test]
6526    fn a_hang_up_without_error_closes_a_waiting_request() {
6527        assert!(
6528            matches!(
6529                waiting_request_pass(Ready::READABLE | Ready::HUP | Ready::WRITE_CLOSED),
6530                SessionResult::Close
6531            ),
6532            "a hang-up must close the session at once"
6533        );
6534        assert!(
6535            matches!(
6536                waiting_request_pass(Ready::READABLE | Ready::HUP),
6537                SessionResult::Continue
6538            ),
6539            "a half-close keeps the session for the response"
6540        );
6541    }
6542
6543    /// A write that meets the client's reset before `epoll` reports it fails
6544    /// (`ECONNRESET`/`EPIPE`): the session closes at once, instead of
6545    /// waiting for an event that only says what the write already learned.
6546    /// Any failed write closes it, since nothing more can be flushed.
6547    ///
6548    /// TO SEE THIS RED: make `ConnectionH1::client_write_failed` return
6549    /// `None`.
6550    #[test]
6551    #[cfg(target_os = "linux")]
6552    fn a_failed_write_to_the_client_closes_the_session() {
6553        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6554        let (mut mux, peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6555        {
6556            let Connection::H1(h1) = &mut mux.frontend else {
6557                unreachable!("the helper builds an H1 frontend")
6558            };
6559            let stream = &mut mux.context.streams[0];
6560            let request = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
6561            let storage = &mut stream.front.storage;
6562            storage.space()[..request.len()].copy_from_slice(request);
6563            storage.fill(request.len());
6564            kawa::h1::parse(&mut stream.front, &mut stream.context);
6565            let answers_rc = stream.answers.clone();
6566            let answers = answers_rc.borrow();
6567            set_default_answer(stream, &mut h1.readiness, 503, &answers);
6568        }
6569        // The client resets: `SO_LINGER` zero, then close.
6570        let linger = libc::linger {
6571            l_onoff: 1,
6572            l_linger: 0,
6573        };
6574        // SAFETY: `linger` is a valid `libc::linger` and `peer` owns the
6575        // descriptor for the duration of the call.
6576        let rc = unsafe {
6577            libc::setsockopt(
6578                std::os::fd::AsRawFd::as_raw_fd(&peer),
6579                libc::SOL_SOCKET,
6580                libc::SO_LINGER,
6581                &linger as *const _ as *const libc::c_void,
6582                std::mem::size_of::<libc::linger>() as libc::socklen_t,
6583            )
6584        };
6585        assert_eq!(rc, 0, "SO_LINGER zero must set");
6586        drop(peer);
6587        std::thread::sleep(Duration::from_millis(20));
6588        // The pass runs on a WRITABLE edge only: the reset is learned by the
6589        // write, not from the event.
6590        mux.frontend.readiness_mut().event = Ready::WRITABLE;
6591
6592        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
6593        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
6594        let mut metrics = SessionMetrics::new(None);
6595        let result = mux.ready(session, proxy, &mut metrics);
6596        assert!(
6597            matches!(result, SessionResult::Close),
6598            "a failed write to the client must close the session, got {result:?}"
6599        );
6600    }
6601
6602    /// A client that never sends its EOF is closed at the linger deadline.
6603    #[test]
6604    fn a_silent_client_is_closed_at_the_linger_deadline() {
6605        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6606        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6607        let mut metrics = SessionMetrics::new(None);
6608        let deadline = Instant::now() - Duration::from_millis(1);
6609        {
6610            let Connection::H1(h1) = &mut mux.frontend else {
6611                unreachable!("the helper builds an H1 frontend")
6612            };
6613            h1.linger = h1::Linger::Draining {
6614                deadline,
6615                remaining: h1::LINGER_MAX_BYTES,
6616            };
6617        }
6618        arm_frontend_at(&mut mux, Some(deadline));
6619
6620        assert_eq!(
6621            mux.timeout(Token(0), &mut metrics),
6622            StateResult::CloseSession,
6623            "the linger deadline closes the session"
6624        );
6625        assert_eq!(
6626            mux.context.streams[0].context.access_log_message, None,
6627            "no per-stream timeout answer for a lingering frontend"
6628        );
6629    }
6630
6631    /// The timer wheel rounds a delay to the NEAREST tick and `Timer::poll`
6632    /// fires everything whose tick has come, so an entry armed for deadline `D`
6633    /// is handed over from `tick * round(D / tick) - tick/2` onwards — up to
6634    /// 99 ms early with the default 100 ms tick. An early delivery is not an
6635    /// expiry: `Mux::timeout` must leave the session alone.
6636    ///
6637    /// To SEE THIS RED: make `Mux::consume_timer_entry` return `true`
6638    /// unconditionally (drop its `core_deadline.is_some_and(...)` early
6639    /// return). The body then runs on an early delivery and this fails with
6640    /// `access_log_message == Some("client_timeout")` — a session closed up to
6641    /// 99 ms before its configured `front_timeout`.
6642    #[test]
6643    fn an_early_wheel_delivery_does_not_run_the_timeout_body() {
6644        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6645        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6646        let mut metrics = SessionMetrics::new(None);
6647
6648        // A minute out, then delivered immediately: early, exaggerated so the
6649        // test cannot be flaky.
6650        arm_frontend_at(&mut mux, Some(Instant::now() + Duration::from_secs(60)));
6651
6652        let result = mux.timeout(Token(0), &mut metrics);
6653
6654        assert_eq!(
6655            result,
6656            StateResult::Continue,
6657            "an early delivery must leave the session running"
6658        );
6659        assert_eq!(
6660            mux.context.streams[0].context.access_log_message, None,
6661            "the timeout body must not run for a delivery before the deadline"
6662        );
6663    }
6664
6665    /// **The ordering test.** This is the one that proves why the
6666    /// `poll_timeout()` split (stored deadline first, memoized reschedule
6667    /// second) must land in that order and not the other way round.
6668    ///
6669    /// `Mux::sync_timeout` memoizes: it touches the wheel only when what the
6670    /// handle holds differs from what the core wants. On an early delivery the
6671    /// core's deadline has NOT moved — it still wants `D` — so a naive memo
6672    /// says "nothing to do" and emits nothing. But the wheel entry is already
6673    /// gone: the delivery consumed it. The session is then left with no timer
6674    /// at all and nothing to arm one. That is the LOST WAKEUP, and it is
6675    /// exactly the bug fixed for the UDP shell in `UdpManager::handle_timeout`,
6676    /// where `reschedule` emitted `ArmTimer` only when the minimum deadline had
6677    /// MOVED.
6678    ///
6679    /// What prevents it here is consume-then-reschedule:
6680    /// `Mux::consume_timer_entry` calls `TimeoutContainer::triggered`, which
6681    /// clears the handle's deadline, so the memo correctly reads "the wheel
6682    /// holds nothing" and re-arms at the same instant.
6683    ///
6684    /// To SEE THIS RED — this is the C2-without-C1 tree:
6685    /// 1. delete `self.deadline = None;` from `TimeoutContainer::triggered`
6686    ///    (`timer.rs`), and
6687    /// 2. delete the `&& container.is_armed() == next.is_some()` half of the
6688    ///    memo in `Mux::sync_timeout`.
6689    ///
6690    /// `sync_timeout` then sees `deadline() == Some(D) == next` and emits
6691    /// nothing, leaving the session alive with no wheel entry. In a DEBUG build
6692    /// `Mux::debug_assert_timer_coherence` catches it first, inside the
6693    /// `reschedule` on the way out of `timeout`, with `timer coherence for
6694    /// Token(0): armed=false but the core wants Some(..)`; the `is_armed()`
6695    /// assertion below is what catches it with debug assertions off. Either
6696    /// mutation alone is enough; both together are the shape the refactor would
6697    /// have had if the memoized reschedule had landed before the stored
6698    /// deadline.
6699    #[test]
6700    fn an_early_wheel_delivery_leaves_a_live_entry_at_the_same_deadline() {
6701        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6702        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6703        let mut metrics = SessionMetrics::new(None);
6704
6705        let deadline = Instant::now() + Duration::from_secs(60);
6706        arm_frontend_at(&mut mux, Some(deadline));
6707
6708        let result = mux.timeout(Token(0), &mut metrics);
6709        assert_eq!(result, StateResult::Continue);
6710
6711        let container = &mux.timeouts.frontend;
6712        assert!(
6713            container.is_armed(),
6714            "the wheel entry was consumed by the delivery; if the memoized \
6715             reschedule does not put one back the session has no timer at all \
6716             — a lost wakeup"
6717        );
6718        assert_eq!(
6719            container.deadline(),
6720            Some(deadline),
6721            "the entry must go back at the SAME instant: a later one is a \
6722             silently extended timeout, a different one is a moved deadline"
6723        );
6724        assert_eq!(
6725            mux.frontend.poll_timeout(),
6726            Some(deadline),
6727            "an early delivery must not disturb what the core asked for"
6728        );
6729    }
6730
6731    /// The mirror: a delivery at or past the deadline is a real expiry and must
6732    /// run the timeout body. Without this, "treat everything as early" would
6733    /// pass the two tests above and never time anything out.
6734    ///
6735    /// To SEE THIS RED: make `Mux::consume_timer_entry` return `false`
6736    /// unconditionally.
6737    #[test]
6738    fn a_delivery_past_the_deadline_runs_the_timeout_body() {
6739        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6740        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6741        let mut metrics = SessionMetrics::new(None);
6742
6743        arm_frontend_at(&mut mux, Some(Instant::now() - Duration::from_secs(1)));
6744
6745        let _ = mux.timeout(Token(0), &mut metrics);
6746
6747        assert_eq!(
6748            mux.context.streams[0].context.access_log_message,
6749            Some("client_timeout"),
6750            "a delivery at or past the deadline is a real expiry and must run \
6751             the timeout body"
6752        );
6753    }
6754
6755    /// A firing for a core that wants no timer at all must fail OPEN — be
6756    /// reported due — rather than being re-validated away. Failing closed there
6757    /// would make such a session immortal, which is strictly worse than the
6758    /// up-to-99 ms-early close the re-validation exists to prevent.
6759    ///
6760    /// To SEE THIS RED: change `Mux::consume_timer_entry` to return `false`
6761    /// when `core_deadline` is `None`.
6762    #[test]
6763    fn a_firing_with_no_deadline_is_treated_as_due() {
6764        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6765        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6766        let mut metrics = SessionMetrics::new(None);
6767
6768        arm_frontend_at(&mut mux, None);
6769        assert_eq!(mux.frontend.poll_timeout(), None, "precondition: no timer");
6770
6771        let _ = mux.timeout(Token(0), &mut metrics);
6772
6773        assert_eq!(
6774            mux.context.streams[0].context.access_log_message,
6775            Some("client_timeout"),
6776            "a firing with no deadline must fail open and run the body"
6777        );
6778    }
6779
6780    /// A real expiry must CLEAR the core's elapsed deadline rather than leave
6781    /// it in place for `reschedule` to re-arm.
6782    ///
6783    /// The core asked to be called back at `D` and has been. Leaving `D` there
6784    /// makes the memoized reschedule arm an already-elapsed instant, which the
6785    /// wheel re-delivers on the very next tick, and the session spins — the
6786    /// canonical sans-io busy loop that `UdpManager::handle_timeout`'s
6787    /// strict-advance `debug_assert!` exists to catch. Every branch of
6788    /// `Mux::timeout` that keeps the session alive re-arms explicitly with
6789    /// `arm_timeout`, which is what the old `triggered()` + `set(token)` pair
6790    /// did.
6791    ///
6792    /// Staged on a BACKEND token deliberately. A frontend firing runs
6793    /// `ConnectionH1::writable`, whose first act is `arm_timeout`, so the
6794    /// frontend re-arms as a side effect and cannot witness the difference. The
6795    /// backend `should_close` path re-arms nothing — it is the one place the
6796    /// elapsed deadline would survive.
6797    ///
6798    /// To SEE THIS RED: delete the `clear_timeout()` block from
6799    /// `Mux::consume_timer_entry`. The strict-advance `debug_assert!` in
6800    /// `SessionState::timeout` fires first (which is the point of it); with
6801    /// debug assertions off, these assertions catch it.
6802    #[test]
6803    fn a_real_expiry_clears_the_elapsed_deadline_instead_of_re_arming_it() {
6804        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6805        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6806        let mut metrics = SessionMetrics::new(None);
6807        let backend_token = Token(1);
6808
6809        let (mut connection, _backend_peer) =
6810            test_backend_connection(&mut mux, Duration::from_secs(30));
6811        let Connection::H1(h1) = &mut connection else {
6812            unreachable!("new_h1_client builds an H1 connection")
6813        };
6814        h1.stream = Some(0);
6815        let fired_at = Instant::now();
6816        park_backend_deadline(&mut connection, fired_at - Duration::from_secs(1));
6817        mux.router.backends.insert(backend_token, connection);
6818        mux.reschedule();
6819
6820        // One linked stream whose response is already terminated and fully
6821        // proxied: the arm that leaves `should_close` true, so nothing re-arms
6822        // the backend.
6823        mux.context.link_stream(0, backend_token);
6824        mux.context.streams[0].back.consumed = true;
6825        mux.context.streams[0].back.parsing_phase = kawa::ParsingPhase::Terminated;
6826
6827        let _ = mux.timeout(backend_token, &mut metrics);
6828
6829        let core = mux
6830            .router
6831            .backends
6832            .get(&backend_token)
6833            .and_then(Connection::poll_timeout);
6834        assert!(
6835            core.is_none_or(|next| next > fired_at),
6836            "after a real expiry the core must want no timer or a strictly \
6837             later one, never the instant that just fired ({core:?})"
6838        );
6839        assert!(
6840            mux.timeouts
6841                .backends
6842                .get(&backend_token)
6843                .and_then(TimeoutContainer::deadline)
6844                .is_none_or(|next| next > fired_at),
6845            "and the wheel must not be holding the elapsed instant either"
6846        );
6847    }
6848
6849    /// A backend that leaves `router.backends` must take its wheel handle with
6850    /// it. Nothing cancels it explicitly any more — `reschedule`'s `retain`
6851    /// drops the handle and `TimeoutContainer::drop` cancels the entry — so the
6852    /// eviction has to be pinned somewhere.
6853    ///
6854    /// To SEE THIS RED: delete the `self.timeouts.backends.retain(...)` call from
6855    /// `Mux::reschedule`. The handle then outlives its connection and
6856    /// `debug_assert_timer_coherence` panics with "timer handle for Token(1)
6857    /// outlived its connection".
6858    #[test]
6859    fn a_departed_backend_releases_its_wheel_handle() {
6860        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
6861        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
6862        let backend_token = Token(1);
6863        let (connection, _backend_peer) =
6864            test_backend_connection(&mut mux, Duration::from_secs(30));
6865        mux.router.backends.insert(backend_token, connection);
6866
6867        mux.reschedule();
6868        assert!(
6869            mux.timeouts.backends.contains_key(&backend_token),
6870            "a live backend must hold a wheel handle"
6871        );
6872
6873        mux.router.backends.remove(&backend_token);
6874        mux.reschedule();
6875
6876        assert!(
6877            !mux.timeouts.backends.contains_key(&backend_token),
6878            "a departed backend must not leave a wheel handle behind"
6879        );
6880    }
6881
6882    // ── Endpoint::peer_rtt is keyed by token ────────────────────────────
6883    //
6884    // `EndpointClient` keys backends by token; `EndpointServer` has a single
6885    // frontend and ignores the token. The pair matters because the two sides
6886    // populate DIFFERENT access-log cells — `snapshot_rtts` reads the local
6887    // socket for `client_rtt` and this trait method for `server_rtt` — so a
6888    // lookup that returned the wrong connection's RTT would mislabel the
6889    // value rather than lose it, which no downstream assertion would catch.
6890
6891    /// An unknown token yields `None`, not some other backend's RTT.
6892    ///
6893    /// The premise is asserted first: on a live loopback socket
6894    /// `getsockopt(TCP_INFO)` succeeds, so a KNOWN token really does return
6895    /// `Some`. Without that half, this test would pass against a
6896    /// `peer_rtt` that always answered `None`.
6897    ///
6898    /// TO SEE THIS RED: in `EndpointClient::peer_rtt` (`connection.rs`),
6899    /// replace `.get(&token)` with `.values().next()`. The unknown token then
6900    /// resolves to the only backend in the map and the final assertion fails
6901    /// with `an unknown token must not resolve to another backend's RTT`.
6902    #[test]
6903    fn endpoint_client_peer_rtt_is_keyed_by_token() {
6904        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6905        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(30));
6906        let backend_token = Token(1);
6907        let (connection, _backend_peer) =
6908            test_backend_connection(&mut mux, Duration::from_secs(30));
6909        mux.router.backends.insert(backend_token, connection);
6910
6911        let endpoint = EndpointClient(&mut mux.router);
6912
6913        assert!(
6914            endpoint.peer_rtt(backend_token).is_some(),
6915            "premise: TCP_INFO answers on a live loopback backend, so a known \
6916             token returns Some — otherwise the assertion below proves nothing"
6917        );
6918        assert!(
6919            endpoint.peer_rtt(Token(99)).is_none(),
6920            "an unknown token must not resolve to another backend's RTT"
6921        );
6922    }
6923
6924    /// `EndpointServer` ignores the token: it holds one frontend, and every
6925    /// token must report that frontend's RTT rather than `None`.
6926    ///
6927    /// TO SEE THIS RED: in `EndpointServer::peer_rtt` (`connection.rs`),
6928    /// return `None` instead of `self.0.rtt()`. It fails on
6929    /// the first loop iteration with `EndpointServer must report its single
6930    /// frontend's RTT for any token` — the second never runs, which is why
6931    /// the loop is two tokens rather than an assertion about "any".
6932    #[test]
6933    fn endpoint_server_peer_rtt_ignores_the_token() {
6934        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6935        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(30));
6936
6937        let endpoint = EndpointServer(&mut mux.frontend);
6938
6939        for token in [Token(0), Token(7)] {
6940            assert!(
6941                endpoint.peer_rtt(token).is_some(),
6942                "EndpointServer must report its single frontend's RTT for any token"
6943            );
6944        }
6945    }
6946
6947    /// A `Mux` pass keeps the connection's frontend-RTT sample, and takes
6948    /// none when nothing in the pass is logged.
6949    ///
6950    /// Until the per-connection sample, every `Mux` entry point expired the
6951    /// frontend's sample so the next pass read a fresh one: twenty keep-alive
6952    /// requests cost twenty `getsockopt(TCP_INFO)` on the frontend alone. The
6953    /// sample is now the connection's (`memoized_rtt`), so a pass must leave
6954    /// it exactly where the previous one put it.
6955    ///
6956    /// Entered through [`SessionState::timeout`] rather than
6957    /// [`SessionState::ready`]: `ready` takes `Rc<RefCell<dyn ProxySession>>`
6958    /// and `Rc<RefCell<dyn L7Proxy>>` and this crate has no test
6959    /// implementation of either — `L7Proxy::sessions` alone would require a
6960    /// live `SessionManager`. `timeout` needs no mock and is not a lesser
6961    /// path: it is where `cancel_timed_out_streams` reaps a silent peer's
6962    /// streams and emits their access logs, and it was one of the three entry
6963    /// points that used to expire the sample.
6964    ///
6965    /// TO SEE THIS RED: at the top of [`Mux::timeout_inner`], below the
6966    /// early-delivery gate, clear the frontend's `local_rtt` again
6967    /// (`if let Connection::H2(h2) = &self.frontend { h2.local_rtt.set(None) }`),
6968    /// which is what `Mux::expire_client_rtt` did. The first assertion then
6969    /// fails with `a Mux pass must keep the connection's frontend RTT`,
6970    /// `left: None`.
6971    #[test]
6972    fn a_mux_pass_keeps_the_connection_sample_and_reads_none_without_a_log() {
6973        /// A value no loopback SRTT can take, standing for the sample an
6974        /// earlier pass of the connection took.
6975        const CONNECTION_SAMPLE: Duration = Duration::from_secs(4321);
6976
6977        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
6978        let (socket, _peer) = connected_socket();
6979        let h2 = h2::H2Shell::new(
6980            Ulid::generate(),
6981            socket,
6982            Position::Server,
6983            &mut PoolBufferSource::new(Rc::downgrade(&pool)),
6984            H2FloodConfig::default(),
6985            H2ConnectionConfig::default(),
6986            Duration::from_secs(30),
6987            None,
6988            Duration::from_secs(30),
6989            None,
6990            Ready::READABLE | Ready::HUP | Ready::ERROR,
6991        )
6992        .expect("a pool with free buffers must yield an H2 connection");
6993
6994        let mut frontend = Connection::H2(h2);
6995        let Connection::H2(h2) = &mut frontend else {
6996            unreachable!("frontend was built as H2")
6997        };
6998        h2.local_rtt.set(Some(Some(CONNECTION_SAMPLE)));
6999        // A deadline already in the past, so `Mux::consume_timer_entry`
7000        // accepts the firing as a real expiry and the body below it runs.
7001        // Without this the connection carries the 30 s deadline it armed at
7002        // construction, the firing re-validates as an early wheel delivery,
7003        // and `timeout_inner` returns before reaching anything this test is
7004        // about.
7005        h2.core.timeout_deadline = Some(Instant::now() - Duration::from_secs(1));
7006
7007        let mut mux = Mux {
7008            configured_frontend_timeout: Duration::from_secs(30),
7009            frontend_token: Token(0),
7010            frontend,
7011            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
7012            context: test_context(&pool),
7013            session_ulid: Ulid::generate(),
7014            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(30))),
7015            backend_registry: BackendRegistry::default(),
7016            backends: Rc::default(),
7017        };
7018
7019        let mut metrics = SessionMetrics::new(None);
7020        let before = tcp_info_reads();
7021        let _ = mux.timeout(Token(0), &mut metrics);
7022
7023        let Connection::H2(h2) = &mux.frontend else {
7024            unreachable!("frontend was built as H2")
7025        };
7026        assert_eq!(
7027            h2.local_rtt.get(),
7028            Some(Some(CONNECTION_SAMPLE)),
7029            "a Mux pass must keep the connection's frontend RTT"
7030        );
7031        assert_eq!(
7032            tcp_info_reads() - before,
7033            0,
7034            "a Mux pass that logs no stream must not read TCP_INFO"
7035        );
7036    }
7037
7038    /// An `L7Proxy` for [`Mux::close`] on a session that holds no backend:
7039    /// the teardown only borrows the proxy per backend, so no method runs.
7040    struct NoBackendProxy;
7041
7042    impl L7Proxy for NoBackendProxy {
7043        fn kind(&self) -> sozu_command::proto::command::ListenerType {
7044            unreachable!("Mux::close without a backend never asks the proxy")
7045        }
7046        fn register_socket(
7047            &self,
7048            _socket: &mut TcpStream,
7049            _token: Token,
7050            _interest: Interest,
7051        ) -> Result<(), std::io::Error> {
7052            unreachable!("Mux::close without a backend never asks the proxy")
7053        }
7054        fn add_session(&self, _session: Rc<RefCell<dyn ProxySession>>) -> Token {
7055            unreachable!("Mux::close without a backend never asks the proxy")
7056        }
7057        fn remove_session(&self, _token: Token) -> bool {
7058            unreachable!("Mux::close without a backend never asks the proxy")
7059        }
7060        fn clusters(
7061            &self,
7062        ) -> &HashMap<sozu_command::state::ClusterId, sozu_command::proto::command::Cluster>
7063        {
7064            unreachable!("Mux::close without a backend never asks the proxy")
7065        }
7066        fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>> {
7067            unreachable!("Mux::close without a backend never asks the proxy")
7068        }
7069    }
7070
7071    /// Close a mux whose only stream is in `state`, still owing its access log
7072    /// or not, and return the `getsockopt(TCP_INFO)` reads the close made.
7073    ///
7074    /// `SessionMetrics::reset` is what every access-log site runs right after
7075    /// the line goes out, so a reset stream is one whose log was written.
7076    fn tcp_info_reads_of_a_close(state: StreamState, owes_a_log: bool) -> usize {
7077        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
7078        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(30));
7079        mux.context.streams[0].state = state;
7080        if !owes_a_log {
7081            mux.context.streams[0].metrics.reset();
7082        }
7083        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(NoBackendProxy));
7084        let mut metrics = SessionMetrics::new(None);
7085        let before = tcp_info_reads();
7086        mux.close(proxy, &mut metrics);
7087        tcp_info_reads() - before
7088    }
7089
7090    /// `Mux::close` reads the frontend RTT only when a stream still owes an
7091    /// access log — the ordinary close, where every stream was logged when it
7092    /// finished, reads nothing (issue #1590).
7093    ///
7094    /// TO SEE THIS RED: in `Mux::close`, hoist the sample above the loop
7095    /// (`let client_rtt = self.frontend.rtt();`). The first assertion then
7096    /// fails with `left: 1`, `right: 0`.
7097    #[test]
7098    fn mux_close_reads_the_frontend_rtt_only_for_a_stream_it_logs() {
7099        assert_eq!(
7100            tcp_info_reads_of_a_close(StreamState::Link, false),
7101            0,
7102            "a close with every stream already logged must not read TCP_INFO"
7103        );
7104        assert_eq!(
7105            tcp_info_reads_of_a_close(StreamState::Recycle, true),
7106            0,
7107            "a recycled stream is not logged at close, so it costs no read"
7108        );
7109        assert_eq!(
7110            tcp_info_reads_of_a_close(StreamState::Link, true),
7111            1,
7112            "an open stream logged at close costs exactly the frontend read: \
7113             it has no backend, so no server_rtt read"
7114        );
7115    }
7116
7117    /// #1564: naming an already-interned backend for another dial allocates
7118    /// nothing. `BackendRegistry::id_for` runs once per backend connection a
7119    /// session opens; the first dial of a backend pays for its slot, every
7120    /// later one must only bump reference counts. Two backends, so a cache
7121    /// that held a single id instead of one per slot fails too.
7122    #[test]
7123    fn a_redial_of_an_interned_backend_allocates_nothing() {
7124        use std::hint::black_box;
7125
7126        use crate::test_allocations::allocations;
7127
7128        const DIALS: usize = 64;
7129        let new_backend = |id: &str, port: u16| {
7130            Rc::new(RefCell::new(Backend::new(
7131                id,
7132                SocketAddr::from(([127, 0, 0, 1], port)),
7133                None,
7134                None,
7135                None,
7136            )))
7137        };
7138        let first = new_backend("redial-first", 7001);
7139        let second = new_backend("redial-second", 7002);
7140
7141        let mut registry = BackendRegistry::default();
7142        // Warm-up: the first dial of each backend interns it.
7143        let warm = [registry.id_for(&first), registry.id_for(&second)];
7144
7145        let before = allocations();
7146        for _ in 0..DIALS {
7147            black_box(registry.id_for(black_box(&first)));
7148            black_box(registry.id_for(black_box(&second)));
7149        }
7150        let allocated = allocations() - before;
7151        assert_eq!(
7152            allocated, 0,
7153            "{DIALS} redials of two interned backends made {allocated} heap \
7154             allocations, expected none"
7155        );
7156
7157        // The redial still names the right slot, id and address.
7158        let again = registry.id_for(&second);
7159        assert_eq!(again, warm[1]);
7160        assert_eq!(&*again.backend_id, "redial-second");
7161        assert_eq!(again.address, SocketAddr::from(([127, 0, 0, 1], 7002)));
7162        assert_ne!(warm[0].slot(), warm[1].slot());
7163    }
7164
7165    /// #1610: a session's first interned backend sizes the registry for one
7166    /// entry, not the four a first `Vec::push` reserves.
7167    ///
7168    /// The interning pays for the entry and for the `Rc<str>` copy of its id,
7169    /// and for nothing else; the id copy is measured on its own, so the
7170    /// assertion carries no size literal.
7171    ///
7172    /// TO SEE THIS RED: drop the `reserve_exact(1)` in
7173    /// `BackendRegistry::intern`. The first entry then allocates four
7174    /// `RegistryEntry` slots.
7175    #[test]
7176    fn the_first_interned_backend_sizes_the_registry_for_one() {
7177        use crate::test_allocations::bytes;
7178
7179        let backend = Rc::new(RefCell::new(Backend::new(
7180            "interned-first",
7181            SocketAddr::from(([127, 0, 0, 1], 7003)),
7182            None,
7183            None,
7184            None,
7185        )));
7186        let before = bytes();
7187        let id_copy: Rc<str> = Rc::from(backend.borrow().backend_id.as_str());
7188        let id_bytes = bytes() - before;
7189        drop(id_copy);
7190
7191        let mut registry = BackendRegistry::default();
7192        let before = bytes();
7193        let id = registry.id_for(&backend);
7194        let interned = bytes() - before;
7195
7196        assert_eq!(
7197            interned,
7198            std::mem::size_of::<RegistryEntry>() + id_bytes,
7199            "the first interned backend must size the registry for one entry"
7200        );
7201        assert_eq!(&*id.backend_id, "interned-first");
7202    }
7203
7204    /// #1613: the first backend entry of a session's reverse index holds its
7205    /// one stream inline, so linking it allocates nothing. The `HashMap` of
7206    /// `Vec`s it replaces allocated its table and the entry's vector here —
7207    /// two allocations, 156 bytes — once per session, so once per request
7208    /// on a one-request connection. (#1610 had already cut that vector from
7209    /// four slots to one.)
7210    ///
7211    /// TO SEE THIS RED: in `LinkedStreams::push`, spill into the `Vec` on
7212    /// the first link as well (1 allocation); or in
7213    /// `InlineTokenMap::get_or_insert_with`, send every token to `rest`
7214    /// (the `BTreeMap` leaf is allocated).
7215    #[test]
7216    fn a_new_backend_entry_in_the_reverse_index_is_sized_for_one_stream() {
7217        use crate::test_allocations::{allocations, bytes};
7218
7219        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7220        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7221
7222        let (before, before_bytes) = (allocations(), bytes());
7223        mux.context.link_stream(0, Token(1));
7224        let linked = (allocations() - before, bytes() - before_bytes);
7225
7226        assert_eq!(
7227            linked,
7228            (0, 0),
7229            "the first reverse-index entry must hold its one stream without allocating"
7230        );
7231        assert_eq!(
7232            mux.context
7233                .backend_streams
7234                .get(&Token(1))
7235                .map(|ids| ids.to_vec()),
7236            Some(vec![0])
7237        );
7238    }
7239
7240    /// The other side of the inline slot: a second stream linked to the same
7241    /// backend (an H2 backend multiplexing) moves both into one `Vec` sized
7242    /// for four, in link order — the single 32-byte allocation the `Vec` the
7243    /// slot replaced made at the same point — and a later link on that
7244    /// connection reuses it.
7245    #[test]
7246    fn a_second_stream_on_one_backend_spills_the_reverse_index_entry_once() {
7247        use crate::test_allocations::{allocations, bytes};
7248
7249        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7250        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7251        let second = mux
7252            .context
7253            .create_stream(Ulid::generate(), 1 << 16)
7254            .expect("the test pool must hand out a second stream");
7255        mux.context.link_stream(0, Token(1));
7256
7257        let (before, before_bytes) = (allocations(), bytes());
7258        mux.context.link_stream(second, Token(1));
7259        let spilled = (allocations() - before, bytes() - before_bytes);
7260        assert_eq!(
7261            spilled,
7262            (1, 4 * std::mem::size_of::<GlobalStreamId>()),
7263            "the second stream must spill both into one four-slot vector"
7264        );
7265        assert_eq!(
7266            mux.context
7267                .backend_streams
7268                .get(&Token(1))
7269                .map(|ids| ids.to_vec()),
7270            Some(vec![0, second]),
7271            "the spill must keep the link order"
7272        );
7273
7274        mux.context.unlink_stream(0);
7275        let before = allocations();
7276        mux.context.link_stream(0, Token(1));
7277        assert_eq!(
7278            allocations() - before,
7279            0,
7280            "a spilled entry keeps its capacity for the next link (#1583)"
7281        );
7282    }
7283
7284    /// #1613: the wheel handles of the usual session — the frontend's and one
7285    /// backend's — cost no allocation, because the frontend's is a field of
7286    /// `MuxTimeouts` and the first backend's is held inline. The
7287    /// `HashMap<Token, TimeoutContainer>` they replace allocated its table
7288    /// (one allocation, 340 bytes) once per session.
7289    ///
7290    /// Neither core wants a deadline here, so `reschedule` never reaches the
7291    /// timer wheel, and what is measured is the handles' storage alone.
7292    ///
7293    /// TO SEE THIS RED: in `InlineTokenMap::get_or_insert_with`, send every
7294    /// token to `rest` (the `BTreeMap` leaf is allocated).
7295    #[test]
7296    fn a_session_holds_its_frontend_and_first_backend_wheel_handles_without_allocating() {
7297        use crate::test_allocations::{allocations, bytes};
7298
7299        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7300        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7301        let backend_token = Token(1);
7302        let (mut connection, _backend_peer) =
7303            test_backend_connection(&mut mux, Duration::from_secs(30));
7304        connection.clear_timeout();
7305        mux.frontend.clear_timeout();
7306
7307        let (before, before_bytes) = (allocations(), bytes());
7308        mux.router.backends.insert(backend_token, connection);
7309        mux.reschedule();
7310        let held = (allocations() - before, bytes() - before_bytes);
7311
7312        assert_eq!(
7313            held,
7314            (0, 0),
7315            "the frontend and first backend handles must not allocate"
7316        );
7317        assert!(
7318            mux.timeouts.backends.contains_key(&backend_token),
7319            "the live backend must hold a wheel handle"
7320        );
7321    }
7322
7323    /// Move a client connection built `Connecting` to `Connected`, as the
7324    /// WRITABLE edge of an established dial does in `Mux::ready_inner`.
7325    fn mark_connected(h1: &mut ConnectionH1<SessionTcpStream>) {
7326        let Position::Client(_, _, status) = &mut h1.position else {
7327            unreachable!("a backend connection is in client position")
7328        };
7329        *status = BackendStatus::Connected;
7330    }
7331
7332    /// A backend timer that fires while the dial is still `Connecting` is a
7333    /// connect timeout (sozu-proxy/sozu#1800): no byte reached the backend,
7334    /// so the request is neither answered 504 nor unlinked here. The
7335    /// connection is flagged dead as a refused dial is, its streams stay
7336    /// linked for the `ready` pass that counts the failure and re-links them,
7337    /// and the session asks for that pass.
7338    ///
7339    /// To SEE THIS RED: delete the `Connecting` early return from the
7340    /// backend-token branch of `Mux::timeout_inner`.
7341    #[test]
7342    fn a_connect_timeout_flags_the_dial_dead_instead_of_answering_504() {
7343        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7344        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7345        let mut metrics = SessionMetrics::new(None);
7346        let backend_token = Token(1);
7347
7348        let (mut connection, _backend_peer) =
7349            test_backend_connection(&mut mux, Duration::from_secs(3));
7350        let Connection::H1(h1) = &mut connection else {
7351            unreachable!("new_h1_client builds an H1 connection")
7352        };
7353        h1.stream = Some(0);
7354        assert!(
7355            matches!(
7356                h1.position,
7357                Position::Client(_, _, BackendStatus::Connecting(_))
7358            ),
7359            "precondition: the dial has not completed"
7360        );
7361        park_backend_deadline(&mut connection, Instant::now() - Duration::from_secs(1));
7362        mux.router.backends.insert(backend_token, connection);
7363        mux.reschedule();
7364        mux.context.link_stream(0, backend_token);
7365        assert!(!mux.has_pending_backend_failure());
7366
7367        let result = mux.timeout(backend_token, &mut metrics);
7368
7369        assert_eq!(result, StateResult::Continue, "the session stays open");
7370        assert!(
7371            mux.has_pending_backend_failure(),
7372            "the dial is flagged dead for the ready pass"
7373        );
7374        assert_eq!(
7375            mux.context.streams[0].state,
7376            StreamState::Linked(backend_token),
7377            "the stream stays linked until the ready pass re-links it"
7378        );
7379        assert_eq!(
7380            mux.context.streams[0].context.access_log_message, None,
7381            "no backend_timeout answer"
7382        );
7383        assert!(
7384            mux.context.streams[0].back.blocks.is_empty()
7385                && mux.context.streams[0].back.out.is_empty(),
7386            "no default answer was written"
7387        );
7388    }
7389
7390    /// An H1 backend connection on a live loopback socket, plus the peer the
7391    /// caller must keep alive.
7392    fn test_backend_connection(
7393        mux: &mut Mux<mio::net::TcpStream, test_support::TestListener>,
7394        duration: Duration,
7395    ) -> (Connection<SessionTcpStream>, std::net::TcpStream) {
7396        let (backend_socket, backend_peer) = connected_socket();
7397        let backend_address = "127.0.0.1:2".parse().expect("backend address must parse");
7398        let backend = Rc::new(RefCell::new(Backend::new(
7399            "test-backend",
7400            backend_address,
7401            None,
7402            None,
7403            None,
7404        )));
7405        // Through the embedder's table, exactly as a real dial does: the
7406        // handle stops here and the connection carries the opaque id.
7407        let backend = mux.backend_registry.id_for(&backend);
7408        let connection = Connection::new_h1_client(
7409            Ulid::generate(),
7410            SessionTcpStream::new(backend_socket, mux.session_ulid, Some(backend_address)),
7411            "test-cluster".into(),
7412            backend,
7413            duration,
7414        );
7415        (connection, backend_peer)
7416    }
7417
7418    /// The loose end flagged as UNVERIFIED in the `poll_timeout` series: in the
7419    /// backend-token branch of `Mux::timeout`, the backend's timer is consumed
7420    /// unconditionally but re-armed only when `!should_close`, while the shared
7421    /// tail can still return `Continue`. If a backend could reach that tail
7422    /// still holding linked streams, those streams would be left with no
7423    /// backend timer at all — a lost wakeup.
7424    ///
7425    /// It cannot, and this pins the reason: the branch calls
7426    /// `Connection::end_stream` for every id in `context.backend_streams`.
7427    /// `ConnectionH2::end_stream` starts with `Context::unlink_stream`;
7428    /// `ConnectionH1::end_stream` guards `self.stream != Some(stream)` and
7429    /// returns early first, which is safe only because an H1 connection carries
7430    /// at most one linked stream and it equals `self.stream` (the comment at
7431    /// the re-arm site has the full chain). This test sets
7432    /// `h1.stream = Some(0)` by hand, so it exercises the MATCHED path only —
7433    /// the mismatch arm is unreached here. It is covered by that invariant plus
7434    /// the `debug_assert_h1_owns_stream` tripwire that runs at every
7435    /// `end_stream` call in `Mux::timeout`, not by a test that drives the arm
7436    /// itself. It drives the exact shape the issue asked for — a backend
7437    /// stalled after a PARTIAL response (`back.consumed`, not terminated),
7438    /// which is the arm that sets `should_write` while leaving `should_close`
7439    /// true, i.e. the arm that skips the re-arm.
7440    ///
7441    /// To SEE THIS RED: delete the `self.context.unlink_stream(stream_id);`
7442    /// from the "forcefully terminate it" arm AND the
7443    /// `backend.end_stream(stream_id, &mut self.context);` at the bottom of
7444    /// that loop — this arm evicts through both, so either alone still clears
7445    /// the index. The companion test
7446    /// `a_completed_backend_response_timeout_unlinks_through_end_stream_alone`
7447    /// covers the arm where `end_stream` IS the sole eviction.
7448    #[test]
7449    fn a_backend_timeout_leaves_no_linked_stream_behind_on_the_close_path() {
7450        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7451        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7452        let mut metrics = SessionMetrics::new(None);
7453        let backend_token = Token(1);
7454
7455        let (mut connection, _backend_peer) =
7456            test_backend_connection(&mut mux, Duration::from_secs(30));
7457        let Connection::H1(h1) = &mut connection else {
7458            unreachable!("new_h1_client builds an H1 connection")
7459        };
7460        h1.stream = Some(0);
7461        // A backend that has sent part of a response is connected. Left
7462        // `Connecting`, as `new_h1_client` builds it, the timer would be a
7463        // connect timeout, which fails the request over instead
7464        // (sozu-proxy/sozu#1800).
7465        mark_connected(h1);
7466        // Due, not early: the entry really elapsed.
7467        park_backend_deadline(&mut connection, Instant::now() - Duration::from_secs(1));
7468        mux.router.backends.insert(backend_token, connection);
7469        mux.reschedule();
7470
7471        mux.context.link_stream(0, backend_token);
7472        // A response that started but never finished.
7473        mux.context.streams[0].back.consumed = true;
7474        assert_eq!(
7475            mux.context
7476                .backend_streams
7477                .get(&backend_token)
7478                .map(|ids| ids.len()),
7479            Some(1),
7480            "precondition: the backend carries exactly one linked stream"
7481        );
7482
7483        let _ = mux.timeout(backend_token, &mut metrics);
7484
7485        assert!(
7486            mux.context
7487                .backend_streams
7488                .get(&backend_token)
7489                .is_none_or(|ids| ids.is_empty()),
7490            "every stream linked to a timed-out backend must be unlinked before \
7491             the shared tail runs, because the tail may return Continue with \
7492             only the frontend timer re-armed"
7493        );
7494        assert_eq!(
7495            mux.context.streams[0].context.access_log_message,
7496            Some("backend_response_timeout"),
7497            "precondition: this is the partial-response arm, the one that \
7498             leaves should_close true and therefore skips the backend re-arm"
7499        );
7500    }
7501
7502    /// The other half of the loose-end proof. In the "response terminated and
7503    /// fully proxied" arm of the backend-timeout branch there is no
7504    /// `unlink_stream` call of its own: the only thing that evicts the stream
7505    /// from `context.backend_streams` on this arm is the unconditional
7506    /// `backend.end_stream(...)` at the bottom of the loop. As above, this test
7507    /// sets `h1.stream = Some(0)` so `ConnectionH1::end_stream` takes its
7508    /// matched path; the mismatch arm is unreached, and guarded by
7509    /// `debug_assert_h1_owns_stream` rather than by a test. That arm also
7510    /// leaves `should_close` true and `should_write` false, which is exactly
7511    /// the `delay_close_for_frontend_flush("timeout")` path the issue named as
7512    /// the one that can return `Continue` with only the frontend re-armed.
7513    ///
7514    /// To SEE THIS RED: delete `backend.end_stream(stream_id, &mut
7515    /// self.context);` from the bottom of that loop.
7516    #[test]
7517    fn a_completed_backend_response_timeout_unlinks_through_end_stream_alone() {
7518        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7519        let (mut mux, _peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7520        let mut metrics = SessionMetrics::new(None);
7521        let backend_token = Token(1);
7522
7523        let (mut connection, _backend_peer) =
7524            test_backend_connection(&mut mux, Duration::from_secs(30));
7525        let Connection::H1(h1) = &mut connection else {
7526            unreachable!("new_h1_client builds an H1 connection")
7527        };
7528        h1.stream = Some(0);
7529        // A backend whose response was fully proxied is connected; see
7530        // `a_backend_timeout_leaves_no_linked_stream_behind_on_the_close_path`.
7531        mark_connected(h1);
7532        park_backend_deadline(&mut connection, Instant::now() - Duration::from_secs(1));
7533        mux.router.backends.insert(backend_token, connection);
7534        mux.reschedule();
7535
7536        mux.context.link_stream(0, backend_token);
7537        // Terminated AND fully proxied: the arm that does nothing but fall
7538        // through to `end_stream`.
7539        mux.context.streams[0].back.consumed = true;
7540        mux.context.streams[0].back.parsing_phase = kawa::ParsingPhase::Terminated;
7541        assert!(
7542            mux.context.streams[0].back.is_terminated()
7543                && mux.context.streams[0].back.is_completed(),
7544            "precondition: this test needs the terminated-and-completed arm"
7545        );
7546
7547        let _ = mux.timeout(backend_token, &mut metrics);
7548
7549        assert!(
7550            mux.context
7551                .backend_streams
7552                .get(&backend_token)
7553                .is_none_or(|ids| ids.is_empty()),
7554            "end_stream is the sole eviction point on this arm; without it the \
7555             backend reaches the shared tail still holding a linked stream and \
7556             with no timer of its own"
7557        );
7558        assert_eq!(
7559            mux.context.streams[0].context.access_log_message, None,
7560            "precondition: a fully proxied response is not a timeout outcome, \
7561             so this arm leaves should_write false and reaches \
7562             delay_close_for_frontend_flush"
7563        );
7564    }
7565
7566    /// Q7 of sozu-proxy/sozu#1340: a stream runs on the listener
7567    /// configuration that was in force when its request arrived, and the next
7568    /// request on the same connection picks up a reload that landed in
7569    /// between. The staleness window is therefore exactly one request, and
7570    /// this test is where that window is pinned — see `LIFECYCLE.md` §2.5.
7571    ///
7572    /// Both halves matter and they fail for opposite reasons:
7573    ///
7574    /// - freezing the listener at [`Context::new`] would satisfy the first
7575    ///   half and break the second — the reload would never reach the
7576    ///   connection at all, which is the construction-time snapshot the
7577    ///   maintainer rejected because hot reconfiguration is the point of this
7578    ///   proxy;
7579    /// - borrowing the listener live on the datapath satisfies the second and
7580    ///   breaks the first — a request already in flight would answer under a
7581    ///   cookie name, an `X-Real-IP` policy or an error page installed after
7582    ///   it started.
7583    ///
7584    /// To SEE THIS RED: give `Context` back an `elide_x_real_ip: bool` field
7585    /// set in [`Context::new`] from `listener.borrow().get_elide_x_real_ip()`,
7586    /// and make [`Context::create_stream`] pass `self.elide_x_real_ip` to
7587    /// `HttpContext::new` instead of `listener.get_elide_x_real_ip()`. The
7588    /// second stream then still carries the pre-reload value.
7589    #[test]
7590    fn a_listener_reload_between_two_streams_moves_only_the_second() {
7591        setup_test_logger!();
7592        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 10, 16_384)));
7593        let mut context = test_context(&pool);
7594
7595        let before = context.listener.borrow().get_answers().clone();
7596        let first = context
7597            .create_stream(Ulid::generate(), 1 << 16)
7598            .expect("the first stream must check out its buffers");
7599
7600        // The operator reloads the listener while the first request is still
7601        // in flight. `reload` publishes a new answer registry under a new
7602        // `Rc`, exactly as `HttpListener::update_config` does.
7603        let after = context.listener.borrow_mut().reload("RELOADEDID", true);
7604        assert!(
7605            !Rc::ptr_eq(&before, &after),
7606            "precondition: the reload must PUBLISH a registry, not rewrite the \
7607             captured one — otherwise this test cannot tell a snapshot from a \
7608             shared handle"
7609        );
7610
7611        let second = context
7612            .create_stream(Ulid::generate(), 1 << 16)
7613            .expect("the second stream must check out its buffers");
7614        assert_ne!(
7615            first, second,
7616            "precondition: the second request must land in its own slot, not \
7617             recycle the first one's"
7618        );
7619
7620        // The next request picks the reload up.
7621        assert_eq!(
7622            context.streams[second].context.sticky_name, "RELOADEDID",
7623            "a request arriving after the reload must use the new sticky name"
7624        );
7625        assert!(
7626            context.streams[second].context.elide_x_real_ip,
7627            "a request arriving after the reload must use the new X-Real-IP \
7628             policy: a listener frozen at Context::new never sees a reload on \
7629             a connection that is already open"
7630        );
7631        assert!(
7632            Rc::ptr_eq(&context.streams[second].answers, &after),
7633            "a request arriving after the reload must render from the newly \
7634             published answer registry"
7635        );
7636
7637        // The request already in flight does not.
7638        assert_eq!(
7639            context.streams[first].context.sticky_name, TEST_STICKY_NAME,
7640            "a request in flight keeps the sticky name it started under"
7641        );
7642        assert!(
7643            !context.streams[first].context.elide_x_real_ip,
7644            "a request in flight keeps the X-Real-IP policy it started under"
7645        );
7646        assert!(
7647            Rc::ptr_eq(&context.streams[first].answers, &before),
7648            "a request in flight keeps the answer registry it captured: a live \
7649             listener borrow on the write, reset or access-log path would hand \
7650             it a page installed after it started"
7651        );
7652    }
7653
7654    // ── peer-address snapshot in the MUX log envelope ────────────────────
7655    //
7656    // `log_context!` / `log_context_lite!` render `peer=` from
7657    // `Connection::peer_address` (`lib/src/protocol/mux/connection.rs`), the
7658    // snapshot the connection took at construction — not a live
7659    // `getpeername(2)`. Both tests below make the two answers differ on
7660    // purpose: the socket is genuinely connected to a loopback listener, so a
7661    // live lookup succeeds and would print `127.0.0.1:<port>`; only a macro
7662    // reading the snapshot can print `10.0.0.42:12345`.
7663    //
7664    // That gap is the PROXY-protocol symptom in miniature — the frontend's real
7665    // peer is the load balancer while the snapshot holds the advertised client
7666    // — and the same read is what keeps the slot populated after the peer's
7667    // RST, when `getpeername(2)` answers ENOTCONN and a live lookup collapses
7668    // to `None` on exactly the error lines an operator is reading. Until this
7669    // change the `MUX` envelope printed one peer while the `MUX-H1`, `MUX-H2`,
7670    // `SOCKET` and `HTTPS` lines of the same ULID printed another.
7671    //
7672    // Mirrors `h2::tests::log_context_renders_the_cached_peer_not_a_live_lookup`
7673    // and `h1::tests::log_context_renders_the_proxy_advertised_peer_not_the_load_balancer`,
7674    // which pin the same slot one layer down in the per-protocol envelopes.
7675    //
7676    // A counting test would not discriminate here, unlike
7677    // `h1::tests::log_context_reads_the_peer_address_once_per_connection`:
7678    // before this change the macros never reached `SocketHandler::peer_addr` at
7679    // all — they went through `Connection::socket()` to mio's inherent method —
7680    // so a trait-call counter reads zero on both sides. The assertions are
7681    // therefore on the rendered VALUE.
7682
7683    /// A live, established loopback connection plus the address
7684    /// `getpeername(2)` reports for it. The listener is returned so the
7685    /// connection stays up for the whole test: these tests assert the snapshot
7686    /// wins even while the live lookup is perfectly healthy, so a half-dead
7687    /// socket would weaken them rather than strengthen them.
7688    ///
7689    /// Connects with a blocking `std` socket and converts afterwards rather
7690    /// than reusing `connected_socket`, because `mio::net::TcpStream::connect`
7691    /// returns before the handshake completes and the live lookup below is a
7692    /// load-bearing premise, not a convenience.
7693    fn connected_loopback_stream() -> (std::net::TcpListener, TcpStream, SocketAddr) {
7694        let listener = std::net::TcpListener::bind("127.0.0.1:0")
7695            .expect("test listener must bind to a loopback port");
7696        let live_peer = listener
7697            .local_addr()
7698            .expect("test listener must report its local address");
7699        let stream =
7700            std::net::TcpStream::connect(live_peer).expect("loopback connect must complete");
7701        stream
7702            .set_nonblocking(true)
7703            .expect("mio requires a nonblocking stream");
7704        (listener, TcpStream::from_std(stream), live_peer)
7705    }
7706
7707    /// Address the frontend handler snapshots. Deliberately non-loopback so it
7708    /// cannot collide with whatever ephemeral port the live lookup reports.
7709    const SNAPSHOT_PEER: &str = "10.0.0.42:12345";
7710
7711    /// An H1 frontend `Mux` whose connection snapshotted [`SNAPSHOT_PEER`]
7712    /// while its socket is really connected to `live_peer`.
7713    /// `Connection::new_h1_server` takes that snapshot itself, from the
7714    /// handler's `SocketHandler::peer_addr`, so nothing here writes the field
7715    /// behind the production path's back.
7716    ///
7717    /// An H1 frontend only because it is the cheaper of the two to build: the
7718    /// accessor under test resolves both `Connection` arms to the same
7719    /// `peer_address` field through `forward!`, and the two macros are defined
7720    /// once for every frontend.
7721    fn mux_with_snapshotted_peer(
7722        pool: &Rc<RefCell<Pool>>,
7723        stream: TcpStream,
7724    ) -> Mux<SessionTcpStream, test_support::TestListener> {
7725        let session_ulid = Ulid::generate();
7726        let socket = SessionTcpStream::new(
7727            stream,
7728            session_ulid,
7729            Some(
7730                SNAPSHOT_PEER
7731                    .parse()
7732                    .expect("the snapshotted peer literal must parse"),
7733            ),
7734        );
7735        Mux {
7736            configured_frontend_timeout: Duration::from_secs(60),
7737            frontend_token: Token(0),
7738            frontend: Connection::new_h1_server(session_ulid, socket, Duration::from_secs(60)),
7739            router: Router::new(Duration::from_secs(30), Duration::from_secs(30)),
7740            context: test_context(pool),
7741            session_ulid,
7742            timeouts: MuxTimeouts::new(TimeoutContainer::new_empty(Duration::from_secs(60))),
7743            backend_registry: BackendRegistry::default(),
7744            backends: Rc::default(),
7745        }
7746    }
7747
7748    /// To SEE THIS RED: in `log_context!` (mod.rs), put
7749    /// `peer = $self.frontend.socket().peer_addr().ok(),` back in place of
7750    /// `peer = $self.frontend.peer_address(),`. `Connection::socket()` hands
7751    /// back a `&mio::net::TcpStream`, so the slot resolves to mio's *inherent*
7752    /// `peer_addr` — a live `getpeername(2)` — and the rendered line carries
7753    /// the loopback address the socket is really connected to.
7754    #[test]
7755    fn log_context_renders_the_snapshotted_peer_not_a_live_lookup() {
7756        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
7757        let (_listener, stream, live_peer) = connected_loopback_stream();
7758        let mux = mux_with_snapshotted_peer(&pool, stream);
7759
7760        // Premise of the test: the live lookup is healthy and disagrees with the
7761        // snapshot. Without it the assertions below could pass for the wrong
7762        // reason (both answers happening to be the same address).
7763        assert_eq!(
7764            mux.frontend.socket().peer_addr().ok(),
7765            Some(live_peer),
7766            "the test socket must be genuinely connected, so a live lookup succeeds"
7767        );
7768        assert_ne!(
7769            SNAPSHOT_PEER,
7770            live_peer.to_string(),
7771            "the snapshotted and live addresses must differ for this test to discriminate"
7772        );
7773
7774        let rendered = log_context!(mux);
7775
7776        assert!(
7777            rendered.contains(&format!("peer=Some({SNAPSHOT_PEER})")),
7778            "the MUX peer= slot must render the snapshotted address; rendered: {rendered}"
7779        );
7780        assert!(
7781            !rendered.contains(&live_peer.to_string()),
7782            "the MUX peer= slot must not fall back to a live getpeername(2); rendered: {rendered}"
7783        );
7784    }
7785
7786    /// The lighter envelope reads the same slot. Separate from its full
7787    /// sibling because the two macros carry their own copy of the `peer` line:
7788    /// fixing one and not the other would render two different peers for one
7789    /// session depending only on which borrow the callsite happened to hold.
7790    ///
7791    /// To SEE THIS RED: in `log_context_lite!` (mod.rs), put
7792    /// `peer = $self.frontend.socket().peer_addr().ok(),` back in place of
7793    /// `peer = $self.frontend.peer_address(),`.
7794    #[test]
7795    fn log_context_lite_renders_the_snapshotted_peer_not_a_live_lookup() {
7796        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
7797        let (_listener, stream, live_peer) = connected_loopback_stream();
7798        let mux = mux_with_snapshotted_peer(&pool, stream);
7799
7800        assert_eq!(
7801            mux.frontend.socket().peer_addr().ok(),
7802            Some(live_peer),
7803            "the test socket must be genuinely connected, so a live lookup succeeds"
7804        );
7805
7806        let rendered = log_context_lite!(mux);
7807
7808        assert!(
7809            rendered.contains(&format!("peer=Some({SNAPSHOT_PEER})")),
7810            "the lite MUX peer= slot must render the snapshotted address; rendered: {rendered}"
7811        );
7812        assert!(
7813            !rendered.contains(&live_peer.to_string()),
7814            "the lite MUX peer= slot must not fall back to a live getpeername(2); \
7815             rendered: {rendered}"
7816        );
7817    }
7818
7819    /// `Mux::close` issues no `EPOLL_CTL_DEL` for its backend sockets: each
7820    /// stays in the epoll set until the `Mux` drops, and its last close is what
7821    /// removes it. The second assertion is the half that makes skipping the
7822    /// deregister safe — the socket is not duplicated anywhere, so dropping the
7823    /// `Mux` really does take it out of the epoll set.
7824    ///
7825    /// To SEE THIS RED: make `Mux::close`'s backend loop issue an
7826    /// `EPOLL_CTL_DEL` after `let socket = client.socket_mut();`. `L7Proxy`
7827    /// no longer carries `deregister_socket` (sozu#1615), so restore it for
7828    /// the experiment — its `HttpProxy` body was
7829    /// `self.registry.deregister(tcp_stream)` — and call
7830    /// `let _ = proxy_borrow.deregister_socket(socket);` there. The first
7831    /// assertion then fails.
7832    #[cfg(target_os = "linux")]
7833    #[test]
7834    fn close_leaves_backend_sockets_to_their_last_close() {
7835        use std::os::fd::AsRawFd;
7836
7837        use mio::Interest;
7838
7839        use super::test_support::epoll_watches;
7840        use crate::{backends::Backend, http::HttpProxy, socket::SessionTcpStream};
7841
7842        let pool = Rc::new(RefCell::new(Pool::with_capacity(2, 4, 16384)));
7843        let (mut mux, _front_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(30));
7844        let parts =
7845            crate::testing::prebuild_server(4, 16_384, false).expect("test server must build");
7846        let registry = parts
7847            .registry
7848            .try_clone()
7849            .expect("the test registry must clone");
7850        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(HttpProxy::new(
7851            parts.registry,
7852            parts.sessions,
7853            parts.pool,
7854            parts.backends,
7855        )));
7856
7857        let (socket, _back_peer) = connected_socket();
7858        let backend = Rc::new(RefCell::new(Backend::new(
7859            "test-backend",
7860            "127.0.0.1:2".parse().expect("backend address must parse"),
7861            None,
7862            None,
7863            None,
7864        )));
7865        let backend_id = mux.backend_registry.id_for(&backend);
7866        let session_ulid = mux.session_ulid;
7867        let mut connection = Connection::new_h1_client(
7868            session_ulid,
7869            SessionTcpStream::new(socket, session_ulid, None),
7870            "test-cluster".into(),
7871            backend_id,
7872            Duration::from_secs(30),
7873        );
7874        let back_fd = connection.socket().as_raw_fd();
7875        registry
7876            .register(
7877                connection.socket_mut(),
7878                Token(7),
7879                Interest::READABLE | Interest::WRITABLE,
7880            )
7881            .expect("the backend socket must register");
7882        mux.router.backends.insert(Token(7), connection);
7883        assert!(
7884            epoll_watches(&registry, back_fd),
7885            "precondition: the backend socket is in the epoll set"
7886        );
7887
7888        mux.close(proxy, &mut SessionMetrics::new(None));
7889        assert!(
7890            epoll_watches(&registry, back_fd),
7891            "Mux::close must not deregister a backend socket it is about to \
7892             close: the close removes it, the EPOLL_CTL_DEL is a wasted syscall"
7893        );
7894
7895        drop(mux);
7896        assert!(
7897            !epoll_watches(&registry, back_fd),
7898            "dropping the Mux closes the backend socket's only descriptor, \
7899             which must take it out of the epoll set"
7900        );
7901        drop(parts.event_loop);
7902    }
7903
7904    // ── a read that meets the EOF records HUP ──────────────────────────
7905
7906    /// An H1 backend read that meets the EOF records HUP; the same read on
7907    /// the frontend leaves HUP to the event loop.
7908    ///
7909    /// Both peers write bytes and their FIN before the read. Since
7910    /// sozu-proxy/sozu#1606 a read stops on a short read, as HAProxy does
7911    /// (`src/raw_sock.c`, `fd_cant_recv` when `ret < try`): the first
7912    /// `readable` delivers the bytes and stops before the EOF, and the EOF is
7913    /// read by the next `readable`, which the poller's `EPOLLRDHUP` edge (or a
7914    /// HUP already in the same event) triggers. That second read is the one
7915    /// that meets the EOF and must record HUP.
7916    ///
7917    /// TO SEE THIS RED: delete the HUP insertion from `ConnectionH1::readable`
7918    /// (`h1.rs`). The backend assertion fails with `a backend read that met the
7919    /// EOF must record HUP`.
7920    #[test]
7921    fn an_h1_backend_eof_read_records_hup_and_a_frontend_one_does_not() {
7922        use std::io::Write;
7923
7924        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
7925        let (mut mux, mut frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
7926        let (mut backend, mut backend_peer) =
7927            test_backend_connection(&mut mux, Duration::from_secs(30));
7928        let Connection::H1(h1) = &mut backend else {
7929            unreachable!("new_h1_client builds an H1 connection")
7930        };
7931        h1.stream = Some(0);
7932        mux.context.link_stream(0, Token(1));
7933
7934        backend_peer
7935            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello")
7936            .expect("the backend peer must write its response");
7937        backend_peer
7938            .shutdown(std::net::Shutdown::Write)
7939            .expect("the backend peer must send its FIN");
7940        std::thread::sleep(Duration::from_millis(20));
7941        backend.readiness_mut().event = Ready::READABLE;
7942        let _ = backend.readable(&mut mux.context, EndpointServer(&mut mux.frontend));
7943        assert!(
7944            mux.context.streams[0].back.is_terminated(),
7945            "the bytes read before the EOF must be parsed"
7946        );
7947        assert!(
7948            !backend.readiness().event.is_hup(),
7949            "the short read stops before the EOF, so it has not met it (#1606)"
7950        );
7951        let _ = backend.readable(&mut mux.context, EndpointServer(&mut mux.frontend));
7952        assert!(
7953            backend.readiness().event.is_hup(),
7954            "a backend read that met the EOF must record HUP"
7955        );
7956
7957        // The same stream carries the frontend's request.
7958        mux.context.streams[0].state = StreamState::Idle;
7959        frontend_peer
7960            .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
7961            .expect("the client must write its request");
7962        frontend_peer
7963            .shutdown(std::net::Shutdown::Write)
7964            .expect("the client must send its FIN");
7965        std::thread::sleep(Duration::from_millis(20));
7966        mux.frontend.readiness_mut().event = Ready::READABLE;
7967        // The first read takes the request and stops short (#1606); the
7968        // second meets the EOF.
7969        let _ = mux
7970            .frontend
7971            .readable(&mut mux.context, EndpointClient(&mut mux.router));
7972        let _ = mux
7973            .frontend
7974            .readable(&mut mux.context, EndpointClient(&mut mux.router));
7975        assert!(
7976            !mux.frontend.readiness().event.is_readable(),
7977            "precondition: the frontend read reached the EOF"
7978        );
7979        assert!(
7980            !mux.frontend.readiness().event.is_hup(),
7981            "a frontend read must leave HUP to the event loop"
7982        );
7983    }
7984
7985    /// A `ProxySession` for a `Mux::ready` pass that links no stream: only
7986    /// `Mux::dial_backend` reads the session, and nothing in these tests
7987    /// dials.
7988    struct NoDialSession;
7989
7990    impl ProxySession for NoDialSession {
7991        fn protocol(&self) -> Protocol {
7992            unreachable!("a Mux pass that dials nothing never asks the session")
7993        }
7994        fn ready(&mut self, _session: Rc<RefCell<dyn ProxySession>>) -> SessionIsToBeClosed {
7995            unreachable!("a Mux pass that dials nothing never asks the session")
7996        }
7997        fn update_readiness(&mut self, _token: Token, _events: Ready) {
7998            unreachable!("a Mux pass that dials nothing never asks the session")
7999        }
8000        fn close(&mut self) {
8001            unreachable!("a Mux pass that dials nothing never asks the session")
8002        }
8003        fn timeout(&mut self, _t: Token) -> SessionIsToBeClosed {
8004            unreachable!("a Mux pass that dials nothing never asks the session")
8005        }
8006        fn last_event(&self) -> Instant {
8007            unreachable!("a Mux pass that dials nothing never asks the session")
8008        }
8009        fn print_session(&self) {
8010            unreachable!("a Mux pass that dials nothing never asks the session")
8011        }
8012        fn frontend_token(&self) -> Token {
8013            unreachable!("a Mux pass that dials nothing never asks the session")
8014        }
8015        fn shutting_down(&mut self) -> SessionIsToBeClosed {
8016            unreachable!("a Mux pass that dials nothing never asks the session")
8017        }
8018    }
8019
8020    /// An `L7Proxy` for a session that only ever removes backend sessions:
8021    /// the dead-backend sweep of `Mux::ready_inner` and `Mux::close` call
8022    /// `remove_session` once per backend and nothing else.
8023    struct RemoveOnlyProxy;
8024
8025    impl L7Proxy for RemoveOnlyProxy {
8026        fn kind(&self) -> sozu_command::proto::command::ListenerType {
8027            unreachable!("closing a backend only removes its session")
8028        }
8029        fn register_socket(
8030            &self,
8031            _socket: &mut TcpStream,
8032            _token: Token,
8033            _interest: Interest,
8034        ) -> Result<(), std::io::Error> {
8035            unreachable!("closing a backend only removes its session")
8036        }
8037        fn add_session(&self, _session: Rc<RefCell<dyn ProxySession>>) -> Token {
8038            unreachable!("closing a backend only removes its session")
8039        }
8040        fn remove_session(&self, _token: Token) -> bool {
8041            true
8042        }
8043        fn clusters(
8044            &self,
8045        ) -> &HashMap<sozu_command::state::ClusterId, sozu_command::proto::command::Cluster>
8046        {
8047            unreachable!("closing a backend only removes its session")
8048        }
8049        fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>> {
8050            unreachable!("closing a backend only removes its session")
8051        }
8052    }
8053
8054    /// An `L7Proxy` for a pass that must have released every stream before
8055    /// the `pending_links` pass of `Mux::ready_inner` runs: removing a
8056    /// backend session is allowed, and reaching the routing view or
8057    /// registering a backend names the regression.
8058    struct NoRoutingProxy;
8059
8060    impl L7Proxy for NoRoutingProxy {
8061        fn kind(&self) -> sozu_command::proto::command::ListenerType {
8062            unreachable!("pending_links routed a stream of an error-GOAWAY linger")
8063        }
8064        fn register_socket(
8065            &self,
8066            _socket: &mut TcpStream,
8067            _token: Token,
8068            _interest: Interest,
8069        ) -> Result<(), std::io::Error> {
8070            unreachable!("pending_links routed a stream of an error-GOAWAY linger")
8071        }
8072        fn add_session(&self, _session: Rc<RefCell<dyn ProxySession>>) -> Token {
8073            unreachable!("pending_links routed a stream of an error-GOAWAY linger")
8074        }
8075        fn remove_session(&self, _token: Token) -> bool {
8076            true
8077        }
8078        fn clusters(
8079            &self,
8080        ) -> &HashMap<sozu_command::state::ClusterId, sozu_command::proto::command::Cluster>
8081        {
8082            unreachable!("pending_links routed a stream of an error-GOAWAY linger")
8083        }
8084        fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>> {
8085            unreachable!("pending_links routed a stream of an error-GOAWAY linger")
8086        }
8087    }
8088
8089    /// Read what the peer has been sent until it sees the FIN, or until the
8090    /// bytes read end with a non-empty `until`, and report whether the FIN was
8091    /// seen. Bounded: a peer that sees neither within the deadline reports
8092    /// what it has.
8093    fn drain_peer(peer: &mut std::net::TcpStream, until: &[u8]) -> (Vec<u8>, bool) {
8094        use std::io::Read;
8095        let deadline = Instant::now() + Duration::from_secs(2);
8096        let mut bytes = Vec::new();
8097        let mut chunk = [0u8; 4096];
8098        loop {
8099            match peer.read(&mut chunk) {
8100                Ok(0) => return (bytes, true),
8101                Ok(n) => {
8102                    bytes.extend_from_slice(&chunk[..n]);
8103                    if !until.is_empty() && bytes.ends_with(until) {
8104                        return (bytes, false);
8105                    }
8106                }
8107                Err(e) if e.kind() == ErrorKind::WouldBlock => {
8108                    if Instant::now() >= deadline {
8109                        return (bytes, false);
8110                    }
8111                    std::thread::sleep(Duration::from_millis(5));
8112                }
8113                Err(e) => panic!("the test peer failed to read: {e}"),
8114            }
8115        }
8116    }
8117
8118    /// #1610: a session whose backend connections die one pass after another
8119    /// sizes its dead-backend list once, then reuses it.
8120    ///
8121    /// Each pass closes one idle keep-alive backend whose event carries HUP,
8122    /// the shape a backend that closes after every response leaves on a
8123    /// keep-alive frontend. Past the first such pass, a pass allocates
8124    /// nothing at all, so the list must not either.
8125    ///
8126    /// TO SEE THIS RED: in `Mux::ready_inner`, collect the dead tokens in a
8127    /// local `Vec::new()` again instead of `self.router.dead_backends`. Every
8128    /// pass that closes a backend then allocates the list.
8129    #[test]
8130    fn successive_dead_backends_reuse_one_dead_backend_list() {
8131        use crate::test_allocations::allocations;
8132
8133        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8134        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8135        mux.frontend.readiness_mut().event = Ready::EMPTY;
8136        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
8137        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
8138        let mut metrics = SessionMetrics::new(None);
8139
8140        let mut close_one = |mux: &mut Mux<_, _>, token: Token| {
8141            let (mut connection, backend_peer) =
8142                test_backend_connection(mux, Duration::from_secs(30));
8143            let Connection::H1(h1) = &mut connection else {
8144                unreachable!("new_h1_client builds an H1 connection")
8145            };
8146            if let Position::Client(_, _, status) = &mut h1.position {
8147                *status = BackendStatus::KeepAlive;
8148            }
8149            h1.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
8150            h1.readiness.event = Ready::HUP;
8151            mux.router.backends.insert(token, connection);
8152            drop(backend_peer);
8153
8154            let before = allocations();
8155            let _ = mux.ready(session.clone(), proxy.clone(), &mut metrics);
8156            let allocated = allocations() - before;
8157            assert!(
8158                !mux.router.backends.contains_key(&token),
8159                "premise: the HUP must close the backend in this pass"
8160            );
8161            allocated
8162        };
8163
8164        // Warm-up: the session's first dead backend sizes the list, and the
8165        // thread's first pass initialises state that outlives the session.
8166        close_one(&mut mux, Token(1));
8167        for token in [Token(2), Token(3)] {
8168            assert_eq!(
8169                close_one(&mut mux, token),
8170                0,
8171                "a later pass that closes a backend must not allocate"
8172            );
8173        }
8174    }
8175
8176    /// Issues #1603 and #1606: a backend whose last bytes and FIN reached the
8177    /// kernel before `epoll_wait` returned is closed in that same pass, after
8178    /// its bytes were parsed and forwarded.
8179    ///
8180    /// The event carries READABLE and HUP together, as
8181    /// `Ready::from(&mio::event::Event)` builds it for an `EPOLLIN|EPOLLRDHUP`
8182    /// edge (`data_and_fin_in_one_edge_keep_readable_until_the_eof_is_read`
8183    /// shows a real socket reporting both in one edge). The first read stops
8184    /// short before the EOF (#1606, as HAProxy does in `src/raw_sock.c`), and
8185    /// the dead-backend check of `Mux::ready_inner`, which reads the HUP the
8186    /// edge already carried, closes the backend on the pass's next inner
8187    /// iteration. The bytes read before the EOF are forwarded first. (When the
8188    /// stream still needs the EOF, as a close-delimited body does,
8189    /// `update_readiness_after_read` keeps READABLE so it is read first:
8190    /// `data_and_fin_in_one_edge_keep_readable_until_the_eof_is_read`.)
8191    ///
8192    /// TO SEE THIS RED: drop `readiness.event.is_hup() ||` from `dead` in
8193    /// `Mux::ready_inner`. The backend then survives the pass: `the backend
8194    /// that read its EOF must be closed in the same pass`.
8195    #[test]
8196    fn a_backend_eof_read_with_its_last_bytes_closes_the_backend_in_the_same_pass() {
8197        use std::io::Write;
8198
8199        const RESPONSE: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
8200
8201        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8202        let (mut mux, mut frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8203        let backend_token = Token(1);
8204        let (mut connection, mut backend_peer) =
8205            test_backend_connection(&mut mux, Duration::from_secs(30));
8206        let Connection::H1(h1) = &mut connection else {
8207            unreachable!("new_h1_client builds an H1 connection")
8208        };
8209        h1.stream = Some(0);
8210        if let Position::Client(_, _, status) = &mut h1.position {
8211            *status = BackendStatus::Connected;
8212        }
8213        // The request is already written; the backend waits to read. Data and
8214        // FIN share one edge.
8215        h1.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
8216        h1.readiness.event = Ready::READABLE | Ready::HUP;
8217        mux.router.backends.insert(backend_token, connection);
8218        mux.context.link_stream(0, backend_token);
8219        mux.frontend.readiness_mut().event = Ready::EMPTY;
8220
8221        backend_peer
8222            .write_all(RESPONSE)
8223            .expect("the backend peer must write its response");
8224        backend_peer
8225            .shutdown(std::net::Shutdown::Write)
8226            .expect("the backend peer must send its FIN");
8227        // Loopback delivers both before `write`/`shutdown` return; the margin
8228        // only guards a scheduler that defers the softirq.
8229        std::thread::sleep(Duration::from_millis(20));
8230
8231        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
8232        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
8233        let mut metrics = SessionMetrics::new(None);
8234        let _ = mux.ready(session, proxy, &mut metrics);
8235
8236        assert!(
8237            !mux.router.backends.contains_key(&backend_token),
8238            "the backend that read its EOF must be closed in the same pass"
8239        );
8240        let (forwarded, _) = drain_peer(&mut frontend_peer, b"hello");
8241        assert!(
8242            forwarded.ends_with(b"hello"),
8243            "the bytes read together with the EOF must still reach the client, \
8244             got {:?}",
8245            String::from_utf8_lossy(&forwarded)
8246        );
8247    }
8248
8249    /// Issues #1603 and #1606: the FIN lands AFTER the `epoll_wait` that
8250    /// woke the pass, so the event carries READABLE without HUP. The pass
8251    /// reads the response in one short read and stops before the EOF (#1606):
8252    /// it does not issue the `recv` that would find the FIN, exactly as
8253    /// HAProxy stops on a short read and lets the poller report the EOF
8254    /// (`src/raw_sock.c`, `fd_cant_recv` when `ret < try`). The backend is
8255    /// therefore closed one `epoll_wait` round later, on the `EPOLLRDHUP` edge
8256    /// the FIN queued — and not later: that edge is already pending when the
8257    /// pass ends, so a zero-timeout poll returns it.
8258    ///
8259    /// That round is the price of not issuing a `recv` after every short
8260    /// read, which answers EAGAIN whenever no FIN follows (the common case).
8261    /// Real loopback socket, registered edge-triggered with mio.
8262    #[test]
8263    fn a_fin_after_epoll_wait_closes_the_backend_on_the_next_round_at_the_latest() {
8264        use std::io::Write;
8265
8266        const RESPONSE: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
8267
8268        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8269        let (mut mux, mut frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8270        let backend_token = Token(1);
8271        let (mut connection, mut backend_peer) =
8272            test_backend_connection(&mut mux, Duration::from_secs(30));
8273        let mut poll = mio::Poll::new().expect("a poll instance must open");
8274        let mut events = mio::Events::with_capacity(8);
8275        let Connection::H1(h1) = &mut connection else {
8276            unreachable!("new_h1_client builds an H1 connection")
8277        };
8278        poll.registry()
8279            .register(
8280                &mut h1.socket.stream,
8281                backend_token,
8282                mio::Interest::READABLE,
8283            )
8284            .expect("the backend socket must register");
8285        h1.stream = Some(0);
8286        if let Position::Client(_, _, status) = &mut h1.position {
8287            *status = BackendStatus::Connected;
8288        }
8289        h1.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
8290
8291        // The response arrives; `epoll_wait` reports it, without a FIN.
8292        backend_peer
8293            .write_all(RESPONSE)
8294            .expect("the backend peer must write its response");
8295        poll.poll(&mut events, Some(Duration::from_secs(5)))
8296            .expect("poll must succeed");
8297        let event = events
8298            .iter()
8299            .find(|event| event.token() == backend_token)
8300            .expect("the response must raise an edge");
8301        assert!(
8302            event.is_readable() && !event.is_read_closed(),
8303            "premise: no FIN yet in the edge that wakes the pass"
8304        );
8305        h1.readiness.event = Ready::from(event);
8306        mux.router.backends.insert(backend_token, connection);
8307        mux.context.link_stream(0, backend_token);
8308        // A request received whole, from a client that asked to close: the
8309        // response closes the frontend without a lingering drain (which
8310        // would end this backend's stream at once).
8311        mux.context.streams[0].front.parsing_phase = kawa::ParsingPhase::Terminated;
8312        mux.context.streams[0].context.keep_alive_frontend = false;
8313        mux.frontend.readiness_mut().event = Ready::EMPTY;
8314
8315        // The FIN lands after `epoll_wait` returned, before the pass reads.
8316        backend_peer
8317            .shutdown(std::net::Shutdown::Write)
8318            .expect("the backend peer must send its FIN");
8319        std::thread::sleep(Duration::from_millis(20));
8320
8321        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
8322        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
8323        let mut metrics = SessionMetrics::new(None);
8324        let _ = mux.ready(session.clone(), proxy.clone(), &mut metrics);
8325        let (forwarded, _) = drain_peer(&mut frontend_peer, b"hello");
8326        assert!(
8327            forwarded.ends_with(b"hello"),
8328            "the response must reach the client in the first pass, got {:?}",
8329            String::from_utf8_lossy(&forwarded)
8330        );
8331        assert!(
8332            mux.router.backends.contains_key(&backend_token),
8333            "the short read stopped before the EOF: the close waits one round (#1606)"
8334        );
8335
8336        // One round: the RDHUP edge is already queued, a zero-timeout poll
8337        // returns it.
8338        poll.poll(&mut events, Some(Duration::ZERO))
8339            .expect("poll must succeed");
8340        let event = events
8341            .iter()
8342            .find(|event| event.token() == backend_token)
8343            .expect("the FIN's edge must already be pending when the pass ends");
8344        assert!(event.is_read_closed(), "the pending edge is the FIN");
8345        mux.update_readiness(backend_token, Ready::from(event));
8346        let _ = mux.ready(session, proxy, &mut metrics);
8347        assert!(
8348            !mux.router.backends.contains_key(&backend_token),
8349            "the backend must be closed on that round, not later"
8350        );
8351    }
8352
8353    // ── a backend that closed with its response still buffered ─────────
8354
8355    /// Drive one H1 exchange whose backend wrote its whole response and
8356    /// closed before a slow client read it, and return the number of body
8357    /// bytes the client received, the body length, the `SessionResult` of
8358    /// the first `Mux::ready` pass, and how many times the exchange reached
8359    /// the loop limit (`LOOP_LIMIT_HITS`).
8360    ///
8361    /// The backend's last bytes and its FIN wait in the kernel while the
8362    /// stream's response buffer is full and the client's socket would block:
8363    /// the dead-backend check keeps the backend for its buffered bytes, and
8364    /// nothing in the pass can progress until the client reads. With
8365    /// `client_half_closed`, the client also shut its write side after its
8366    /// request, as `test_plain_client_half_close_after_request_receives_large_response`
8367    /// does.
8368    fn slow_client_after_backend_close(
8369        client_half_closed: bool,
8370    ) -> (usize, usize, SessionResult, u32) {
8371        use std::io::{Read, Write};
8372
8373        const BODY: usize = 160 * 1024;
8374        let head = format!("HTTP/1.1 200 OK\r\nContent-Length: {BODY}\r\n\r\n");
8375        let length = head.len() + BODY;
8376
8377        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8378        let (mut mux, mut frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8379        // A slow client: small buffers on both ends of the frontend, so its
8380        // socket would block long before the response is out.
8381        socket2::SockRef::from(&frontend_peer)
8382            .set_recv_buffer_size(16 * 1024)
8383            .expect("the client receive buffer must be settable");
8384        socket2::SockRef::from(mux.frontend.socket())
8385            .set_send_buffer_size(4096)
8386            .expect("the frontend send buffer must be settable");
8387
8388        let backend_token = Token(1);
8389        let (mut connection, mut backend_peer) =
8390            test_backend_connection(&mut mux, Duration::from_secs(30));
8391        // Room in the kernel for the whole response, so the backend can
8392        // write all of it and close before sozu reads it.
8393        socket2::SockRef::from(connection.socket())
8394            .set_recv_buffer_size(1 << 20)
8395            .expect("the backend receive buffer must be settable");
8396        socket2::SockRef::from(&backend_peer)
8397            .set_send_buffer_size(1 << 20)
8398            .expect("the backend peer send buffer must be settable");
8399        let Connection::H1(h1) = &mut connection else {
8400            unreachable!("new_h1_client builds an H1 connection")
8401        };
8402        h1.stream = Some(0);
8403        if let Position::Client(_, _, status) = &mut h1.position {
8404            *status = BackendStatus::Connected;
8405        }
8406        h1.readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
8407        h1.readiness.event = Ready::READABLE | Ready::HUP;
8408        mux.router.backends.insert(backend_token, connection);
8409        mux.context.link_stream(0, backend_token);
8410        // The request was received whole and forwarded.
8411        mux.context.streams[0].front.parsing_phase = kawa::ParsingPhase::Terminated;
8412        mux.frontend.readiness_mut().event = if client_half_closed {
8413            Ready::HUP
8414        } else {
8415            Ready::EMPTY
8416        };
8417
8418        let mut response = head.into_bytes();
8419        response.resize(length, b'b');
8420        let mut written = 0;
8421        let deadline = Instant::now() + Duration::from_secs(5);
8422        while written < response.len() {
8423            match backend_peer.write(&response[written..]) {
8424                Ok(size) => written += size,
8425                Err(e) if e.kind() == ErrorKind::WouldBlock => {
8426                    assert!(
8427                        Instant::now() < deadline,
8428                        "premise: the kernel must hold the whole response, wrote {written}"
8429                    );
8430                    std::thread::sleep(Duration::from_millis(1));
8431                }
8432                Err(e) => panic!("the backend peer failed to write: {e}"),
8433            }
8434        }
8435        backend_peer
8436            .shutdown(std::net::Shutdown::Write)
8437            .expect("the backend peer must send its FIN");
8438        std::thread::sleep(Duration::from_millis(20));
8439
8440        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(NoDialSession));
8441        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
8442        let mut metrics = SessionMetrics::new(None);
8443
8444        LOOP_LIMIT_HITS.with(|hits| hits.set(0));
8445        let first = mux.ready(session.clone(), proxy.clone(), &mut metrics);
8446        let mut result = first;
8447        let mut received = Vec::new();
8448        let mut chunk = [0u8; 4096];
8449        let deadline = Instant::now() + Duration::from_secs(10);
8450        // sozu adds its own response headers: the body is what follows the
8451        // head the client received.
8452        let body_received = |received: &[u8]| {
8453            received
8454                .windows(4)
8455                .position(|window| window == b"\r\n\r\n")
8456                .map_or(0, |end| received.len() - end - 4)
8457        };
8458        loop {
8459            // The client reads what it can, then the poller reports the
8460            // frontend writable again, as epoll does once the client drains.
8461            loop {
8462                match frontend_peer.read(&mut chunk) {
8463                    Ok(0) => break,
8464                    Ok(size) => received.extend_from_slice(&chunk[..size]),
8465                    Err(e) if e.kind() == ErrorKind::WouldBlock => break,
8466                    Err(e) => panic!("the client failed to read: {e}"),
8467                }
8468            }
8469            if body_received(&received) >= BODY
8470                || result == SessionResult::Close
8471                || Instant::now() >= deadline
8472            {
8473                break;
8474            }
8475            mux.update_readiness(Token(0), Ready::WRITABLE);
8476            result = mux.ready(session.clone(), proxy.clone(), &mut metrics);
8477        }
8478        if body_received(&received) < BODY {
8479            // Whatever the last pass queued in the kernel reaches the client.
8480            let (rest, _) = drain_peer(&mut frontend_peer, &[]);
8481            received.extend_from_slice(&rest);
8482        }
8483        let hits = LOOP_LIMIT_HITS.with(|hits| hits.get());
8484        assert!(
8485            mux.context.client_bytes_out >= received.len(),
8486            "every byte the client received was counted as written to it: {} < {}",
8487            mux.context.client_bytes_out,
8488            received.len()
8489        );
8490        (body_received(&received), BODY, first, hits)
8491    }
8492
8493    /// A slow client downloading a response whose backend has already
8494    /// written all of it and closed must receive the whole response.
8495    ///
8496    /// The kept backend's HUP stays in its `filter_interest()`, which is
8497    /// never empty while HUP is in its interest: the inner loop of
8498    /// `Mux::ready_inner` used to count that HUP as work, spin to
8499    /// `MAX_LOOP_ITERATIONS` with nothing to progress, and close the session
8500    /// with the end of the body still in the stream's buffer.
8501    ///
8502    /// TO SEE THIS RED: make `Connection::has_loop_work` return
8503    /// `!readiness.filter_interest().is_empty()` again. The first pass then
8504    /// runs to `MAX_LOOP_ITERATIONS`. With `Mux::wait_for_client_at_loop_limit`
8505    /// also back to checking `frontend.has_pending_write()` alone, that pass
8506    /// returns `SessionResult::Close` and the client is short of bytes.
8507    #[test]
8508    fn a_slow_client_receives_the_whole_response_of_a_backend_that_closed() {
8509        let (received, expected, first, hits) = slow_client_after_backend_close(false);
8510        assert_eq!(
8511            hits, 0,
8512            "every pass must stop once nothing can progress, not at the loop limit"
8513        );
8514        assert_ne!(
8515            first,
8516            SessionResult::Close,
8517            "the first pass must wait for the client, not close"
8518        );
8519        assert_eq!(received, expected, "the client must receive the whole body");
8520    }
8521
8522    /// The same exchange from a client that shut its write side after its
8523    /// request.
8524    #[test]
8525    fn a_half_closed_slow_client_receives_the_whole_response_of_a_backend_that_closed() {
8526        let (received, expected, first, hits) = slow_client_after_backend_close(true);
8527        assert_eq!(
8528            hits, 0,
8529            "every pass must stop once nothing can progress, not at the loop limit"
8530        );
8531        assert_ne!(
8532            first,
8533            SessionResult::Close,
8534            "the first pass must wait for the client, not close"
8535        );
8536        assert_eq!(received, expected, "the client must receive the whole body");
8537    }
8538
8539    /// At the loop limit, a response still queued in an H1 frontend's stream
8540    /// buffer is not dropped: the session waits for the client's next
8541    /// writable event instead of closing. A plain-TCP H1 frontend's
8542    /// connection-level `has_pending_write()` is false whatever its stream
8543    /// holds, which is what the limit used to check (sozu-proxy/sozu#1819).
8544    ///
8545    /// TO SEE THIS RED: make `Mux::wait_for_client_at_loop_limit` compute
8546    /// `output_queued` from `self.frontend.has_pending_write()` alone.
8547    #[test]
8548    fn the_loop_limit_waits_for_the_client_while_a_stream_holds_its_response() {
8549        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8550        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8551        mux.context.streams[0].state = StreamState::Linked(Token(1));
8552        mux.context.streams[0]
8553            .back
8554            .push_out(kawa::Store::Static(b"the end of the body"));
8555        mux.frontend.readiness_mut().event = Ready::WRITABLE;
8556        assert!(
8557            !mux.frontend.has_pending_write(),
8558            "premise: the connection itself has nothing pending"
8559        );
8560
8561        assert!(
8562            mux.wait_for_client_at_loop_limit(),
8563            "a queued response must make the session wait, not close"
8564        );
8565        let readiness = mux.frontend.readiness();
8566        assert!(
8567            readiness.interest.is_writable() && !readiness.event.is_writable(),
8568            "the session waits for the next writable event, got {readiness:?}"
8569        );
8570    }
8571
8572    /// Waiting at the loop limit is bounded: reaching it again with no byte
8573    /// written to the client since closes the session, while a client that
8574    /// drained some of the response in between is waited for again.
8575    ///
8576    /// TO SEE THIS RED: drop `progressed &&` from `output_queued` in
8577    /// `Mux::wait_for_client_at_loop_limit`. The second hit then waits again.
8578    #[test]
8579    fn a_second_loop_limit_without_progress_closes_the_session() {
8580        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8581        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8582        mux.context.streams[0].state = StreamState::Linked(Token(1));
8583        mux.context.streams[0]
8584            .back
8585            .push_out(kawa::Store::Static(b"the end of the body"));
8586
8587        assert!(
8588            mux.wait_for_client_at_loop_limit(),
8589            "the first hit with output queued waits for the client"
8590        );
8591        mux.context.client_bytes_out += 100;
8592        assert!(
8593            mux.wait_for_client_at_loop_limit(),
8594            "the client drained bytes since: the session waits again"
8595        );
8596        assert!(
8597            !mux.wait_for_client_at_loop_limit(),
8598            "no byte reached the client since the last hit: the session must close"
8599        );
8600    }
8601
8602    /// Progress at the loop limit is read from the session's own count of
8603    /// bytes written to the client, not from the per-stream metrics, which
8604    /// `SessionMetrics::reset` zeroes when a stream ends and its slot is
8605    /// reused.
8606    ///
8607    /// TO SEE THIS RED: compute `bytes_out` in
8608    /// `Mux::wait_for_client_at_loop_limit` as the sum of every stream's
8609    /// `metrics.bout` again. A slot reused between two hits then reads as
8610    /// progress when nothing was written, and as no progress when the new
8611    /// stream wrote exactly what the old one had.
8612    #[test]
8613    fn a_stream_slot_reused_between_two_loop_limits_does_not_fake_progress() {
8614        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8615        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8616        mux.context.streams[0].state = StreamState::Linked(Token(1));
8617        mux.context.streams[0]
8618            .back
8619            .push_out(kawa::Store::Static(b"the end of the body"));
8620        mux.context.streams[0].metrics.bout = 100;
8621        mux.context.client_bytes_out = 100;
8622        assert!(
8623            mux.wait_for_client_at_loop_limit(),
8624            "the first hit with output queued waits for the client"
8625        );
8626
8627        // The stream ends and its slot is reused before anything is written.
8628        mux.context.streams[0].metrics.reset();
8629        assert!(
8630            !mux.wait_for_client_at_loop_limit(),
8631            "a reset metric is not progress: the session must close"
8632        );
8633
8634        // A fresh session state: the new stream writes as much as the old one
8635        // had, which is progress.
8636        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8637        mux.context.streams[0].state = StreamState::Linked(Token(1));
8638        mux.context.streams[0]
8639            .back
8640            .push_out(kawa::Store::Static(b"the end of the body"));
8641        mux.context.streams[0].metrics.bout = 100;
8642        mux.context.client_bytes_out = 100;
8643        assert!(mux.wait_for_client_at_loop_limit(), "the first hit waits");
8644        mux.context.streams[0].metrics.reset();
8645        mux.context.streams[0].metrics.bout = 100;
8646        mux.context.client_bytes_out += 100;
8647        assert!(
8648            mux.wait_for_client_at_loop_limit(),
8649            "the reused slot wrote 100 bytes since: the session waits again"
8650        );
8651    }
8652
8653    /// At the loop limit, a session with nothing queued for the client is
8654    /// closed: the limit stays a safety bound. Bytes left in a stream slot
8655    /// that is no longer open are not owed to the client.
8656    #[test]
8657    fn the_loop_limit_closes_a_session_with_nothing_queued() {
8658        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8659        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8660        mux.context.streams[0].state = StreamState::Recycle;
8661        mux.context.streams[0]
8662            .back
8663            .push_out(kawa::Store::Static(b"a recycled slot's leftover"));
8664        assert!(
8665            !mux.wait_for_client_at_loop_limit(),
8666            "nothing queued: the session must close"
8667        );
8668    }
8669
8670    // ── no write-side shutdown once the peer has closed ────────────────
8671
8672    /// Close a mux holding one backend whose readiness is `event` and whose
8673    /// status is `status`, and return the `shutdown(SHUT_WR)` calls the close
8674    /// issued together with what the backend peer then observed.
8675    fn backend_shutdowns_of_a_close(
8676        event: Ready,
8677        status: BackendStatus,
8678        peer_sends_fin: bool,
8679    ) -> (usize, bool) {
8680        let pool = Rc::new(RefCell::new(Pool::with_capacity(4, 8, 16384)));
8681        let (mut mux, _frontend_peer) = h1_mux_with_idle_stream(&pool, Duration::from_secs(60));
8682        mux.context.streams[0].metrics.reset();
8683        let (mut connection, mut backend_peer) =
8684            test_backend_connection(&mut mux, Duration::from_secs(30));
8685        if peer_sends_fin {
8686            backend_peer
8687                .shutdown(std::net::Shutdown::Write)
8688                .expect("the backend peer must send its FIN");
8689        }
8690        *connection.position_mut() = match connection.position() {
8691            Position::Client(cluster_id, backend, _) => {
8692                Position::Client(cluster_id.clone(), backend.clone(), status)
8693            }
8694            Position::Server => unreachable!("new_h1_client builds a client"),
8695        };
8696        connection.readiness_mut().event = event;
8697        mux.router.backends.insert(Token(1), connection);
8698
8699        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(RemoveOnlyProxy));
8700        let mut metrics = SessionMetrics::new(None);
8701        let before = shutdown_writes();
8702        mux.close(proxy, &mut metrics);
8703        let shutdowns = shutdown_writes() - before;
8704        // The descriptor closes when the mux drops, as it does with its
8705        // session in production.
8706        drop(mux);
8707        let (_, saw_fin) = drain_peer(&mut backend_peer, &[]);
8708        (shutdowns, saw_fin)
8709    }
8710
8711    /// Issue #1603: `Mux::close` skips `shutdown(SHUT_WR)` on a backend that
8712    /// has already closed, and the backend still gets its FIN from the close.
8713    ///
8714    /// The peer in the first case half-closed and is still reading: that is
8715    /// the peer the skipped shutdown could have mattered to, and the FIN it
8716    /// observes comes from `close(2)` alone.
8717    ///
8718    /// TO SEE THIS RED: make `shutdown_write` ignore `peer_closed`. The first
8719    /// assertion then fails with `left: 1`, `right: 0`.
8720    #[test]
8721    fn mux_close_skips_the_backend_shutdown_once_the_backend_closed() {
8722        let (shutdowns, saw_fin) =
8723            backend_shutdowns_of_a_close(Ready::HUP, BackendStatus::Connected, true);
8724        assert_eq!(
8725            shutdowns, 0,
8726            "a backend whose FIN was seen must not be shut down before its close"
8727        );
8728        assert!(
8729            saw_fin,
8730            "the close alone must still deliver the FIN to a half-closed peer"
8731        );
8732    }
8733
8734    /// The other half of #1603: a backend that has NOT closed keeps its
8735    /// `shutdown(SHUT_WR)` and observes the FIN — including one sozu itself
8736    /// is dropping, whose readiness `force_disconnect` overwrote with HUP.
8737    #[test]
8738    fn mux_close_keeps_the_backend_shutdown_while_the_backend_is_there() {
8739        for (event, status, why) in [
8740            (
8741                Ready::READABLE | Ready::WRITABLE,
8742                BackendStatus::Connected,
8743                "a live backend",
8744            ),
8745            (
8746                Ready::HUP,
8747                BackendStatus::Disconnecting,
8748                "a backend force_disconnect marked with HUP",
8749            ),
8750        ] {
8751            let (shutdowns, saw_fin) = backend_shutdowns_of_a_close(event, status, false);
8752            assert_eq!(shutdowns, 1, "{why} must keep its shutdown(SHUT_WR)");
8753            assert!(saw_fin, "{why} must observe the FIN");
8754        }
8755    }
8756}