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