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