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(®istry, 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(®istry, 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(®istry, 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}