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::{HashMap, VecDeque},
21 fmt::Debug,
22 io::ErrorKind,
23 net::{Shutdown, SocketAddr},
24 rc::{Rc, Weak},
25 sync::Arc,
26 time::{Duration, Instant},
27};
28
29use mio::{Token, net::TcpStream};
30use rusty_ulid::Ulid;
31use sozu_command::{
32 logging::ansi_palette,
33 proto::command::{Event, EventKind},
34 ready::Ready,
35};
36
37/// Protocol label + session descriptor used as a prefix on every [`Mux`] log
38/// line. Matches the RUSTLS log-context convention:
39/// `[<ulid> - - -]\tMUX\tSession(...)\t >>>`. When colored output is enabled
40/// (via [`ansi_palette`]) the label is wrapped in bold bright-white ANSI
41/// (uniform across every protocol) and the session detail block is rendered
42/// in light grey.
43///
44/// Fields included in the session block:
45/// - `frontend` — mio token of the frontend socket
46/// - `peer` — peer address (or `None` if the socket is gone)
47/// - `streams` — number of streams currently held by the [`Context`]
48/// - `backends` — number of backend connections in the [`Router`]
49/// - `pending_links` — streams waiting to be linked to a backend
50/// - `readiness` — frontend mio readiness snapshot
51macro_rules! log_context {
52 ($self:expr) => {{
53 let (open, reset, grey, gray, white) = ansi_palette();
54 format!(
55 "[{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 >>>",
56 open = open,
57 reset = reset,
58 grey = grey,
59 gray = gray,
60 white = white,
61 ulid = $self.session_ulid,
62 frontend = $self.frontend_token.0,
63 peer = $self.frontend.socket().peer_addr().ok(),
64 streams = $self.context.streams.len(),
65 backends = $self.router.backends.len(),
66 pending_links = $self.context.pending_links.len(),
67 readiness = $self.frontend.readiness(),
68 )
69 }};
70}
71
72/// Lighter variant of [`log_context!`] that omits the
73/// `streams`/`backends`/`pending_links` counts. Used at sites where the
74/// borrow checker forbids reading `self.router.backends` or
75/// `self.context.streams` (e.g. inside a method that already holds a mutable
76/// borrow on one of them). The ULID and frontend snapshot still carry enough
77/// context to correlate the line back to the rest of the session.
78macro_rules! log_context_lite {
79 ($self:expr) => {{
80 let (open, reset, grey, gray, white) = ansi_palette();
81 format!(
82 "[{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 >>>",
83 open = open,
84 reset = reset,
85 grey = grey,
86 gray = gray,
87 white = white,
88 ulid = $self.session_ulid,
89 frontend = $self.frontend_token.0,
90 peer = $self.frontend.socket().peer_addr().ok(),
91 readiness = $self.frontend.readiness(),
92 )
93 }};
94}
95
96/// Module-level prefix for logs emitted from free functions or routing
97/// blocks where no [`Mux`] is in scope. Honours the colored flag.
98///
99/// Two arms:
100/// * `log_module_context!()` — zero-arg, legacy `MUX\t >>>` output. Kept
101/// for sites without an `HttpContext` in scope (e.g. the generic
102/// `trace!` that fires before the variant-specific match).
103/// * `log_module_context!($http_context)` — rich form. `$http_context`
104/// must be `&HttpContext`. Produces the same
105/// `[session req cluster backend]` bracket as RUSTLS/PIPE/TCP followed
106/// by a `Session(...)` block, so MUX lines emitted from variant match
107/// arms stay filterable by session ULID or request ULID. Mirrors
108/// `router.rs:log_module_context!($http_context)` (see there). Custom
109/// methods render only their byte length, and authority renders only its
110/// byte length.
111macro_rules! log_module_context {
112 () => {{
113 let (open, reset, _, _, _) = ansi_palette();
114 format!("{open}MUX{reset}\t >>>", open = open, reset = reset)
115 }};
116 ($http_context:expr) => {{
117 let (open, reset, grey, gray, white) = ansi_palette();
118 let http_ctx: &HttpContext = &$http_context;
119 let ctx = http_ctx.log_context();
120 format!(
121 "{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 >>>",
122 open = open,
123 reset = reset,
124 grey = grey,
125 gray = gray,
126 white = white,
127 ctx = ctx,
128 frontend = http_ctx.session_address,
129 method = http_ctx.method,
130 authority_bytes = http_ctx.authority.as_ref().map(String::len),
131 )
132 }};
133}
134
135pub mod answers;
136pub mod auth;
137pub mod connection;
138mod converter;
139pub mod debug;
140mod h1;
141mod h2;
142pub mod parser;
143mod pkawa;
144pub mod router;
145pub(crate) mod serializer;
146mod shared;
147pub mod stream;
148
149use crate::metrics::names;
150use crate::{
151 BackendConnectionError, FrontendFromRequestError, L7ListenerHandler, L7Proxy, ListenerHandler,
152 ProxySession, Readiness, RetrieveClusterError, SessionIsToBeClosed, SessionMetrics,
153 SessionResult, StateResult,
154 backends::{Backend, BackendError},
155 http::HttpListener,
156 https::HttpsListener,
157 pool::{Checkout, Pool},
158 protocol::{SessionState, http::editor::HttpContext},
159 retry::RetryPolicy,
160 server::push_event,
161 socket::{FrontRustls, SessionTcpStream, SocketHandler, SocketResult, stats::socket_rtt},
162};
163
164pub(crate) use crate::protocol::mux::answers::{
165 forcefully_terminate_answer, set_default_answer, set_default_answer_with_retry_after,
166};
167use crate::protocol::mux::connection::{EndpointClient, EndpointServer};
168pub use crate::protocol::mux::{
169 answers::terminate_default_answer,
170 connection::Connection,
171 debug::{DebugEvent, DebugHistory},
172 h1::ConnectionH1,
173 h2::ConnectionH2,
174 h2::H2ByteAccounting,
175 h2::H2ConnectionConfig,
176 h2::H2DrainState,
177 h2::H2FloodConfig,
178 h2::H2FlowControl,
179 parser::H2Error,
180 router::Router,
181 stream::{Stream, StreamParts, StreamState},
182};
183
184// ── Tuning Constants ─────────────────────────────────────────────────────────
185
186/// Maximum event loop iterations before forcefully closing a session.
187/// Prevents infinite loops from consuming the single-threaded worker.
188const MAX_LOOP_ITERATIONS: i32 = 10_000;
189// ─────────────────────────────────────────────────────────────────────────────
190
191/// Generic Http representation using the Kawa crate using the Checkout of Sozu as buffer
192type GenericHttpStream = kawa::Kawa<Checkout>;
193type StreamId = u32;
194type GlobalStreamId = usize;
195pub type MuxClear = Mux<SessionTcpStream, HttpListener>;
196pub type MuxTls = Mux<FrontRustls, HttpsListener>;
197
198pub enum Position {
199 Client(String, Rc<RefCell<Backend>>, BackendStatus),
200 Server,
201}
202
203impl Debug for Position {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 match self {
206 Self::Client(cluster_id, _, status) => f
207 .debug_tuple("Client")
208 .field(cluster_id)
209 .field(status)
210 .finish(),
211 Self::Server => write!(f, "Server"),
212 }
213 }
214}
215
216impl Position {
217 fn is_server(&self) -> bool {
218 match self {
219 Position::Client(..) => false,
220 Position::Server => true,
221 }
222 }
223 fn is_client(&self) -> bool {
224 !self.is_server()
225 }
226
227 /// Increment the global `count!()` counter for bytes read on this side.
228 pub fn count_bytes_in_counter(&self, size: usize) {
229 match self {
230 Position::Client(..) => count!(names::backend::BACK_BYTES_IN, size as i64),
231 Position::Server => count!(names::backend::BYTES_IN, size as i64),
232 }
233 }
234
235 /// Increment the global `count!()` counter for bytes written on this side.
236 pub fn count_bytes_out_counter(&self, size: usize) {
237 match self {
238 Position::Client(..) => count!(names::backend::BACK_BYTES_OUT, size as i64),
239 Position::Server => count!(names::backend::BYTES_OUT, size as i64),
240 }
241 }
242
243 /// Attribute `size` bytes read to the appropriate `SessionMetrics` field.
244 pub fn count_bytes_in(&self, metrics: &mut SessionMetrics, size: usize) {
245 match self {
246 Position::Client(..) => metrics.backend_bin += size,
247 Position::Server => metrics.bin += size,
248 }
249 }
250
251 /// Attribute `size` bytes written to the appropriate `SessionMetrics` field.
252 pub fn count_bytes_out(&self, metrics: &mut SessionMetrics, size: usize) {
253 match self {
254 Position::Client(..) => metrics.backend_bout += size,
255 Position::Server => metrics.bout += size,
256 }
257 }
258}
259
260#[derive(Debug)]
261pub enum BackendStatus {
262 Connecting(Instant),
263 Connected,
264 KeepAlive,
265 Disconnecting,
266}
267
268#[derive(Debug, Clone, Copy)]
269pub enum MuxResult {
270 Continue,
271 Upgrade,
272 CloseSession,
273}
274
275pub trait Endpoint: Debug {
276 fn readiness(&self, token: Token) -> &Readiness;
277 fn readiness_mut(&mut self, token: Token) -> &mut Readiness;
278 /// Returns the underlying TCP socket for the peer side of a stream.
279 ///
280 /// Used by access-log emission to capture TCP_INFO RTT for the side the
281 /// caller does NOT own directly: a frontend connection (Position::Server)
282 /// reads the backend socket through this method, and a backend connection
283 /// (Position::Client) reads the frontend socket the same way. `token` is
284 /// ignored by [`super::connection::EndpointServer`] (which has a single
285 /// frontend connection) and used as a key by
286 /// [`super::connection::EndpointClient`] (which keys backends by token).
287 /// Returns `None` when the token doesn't resolve, mirroring the existing
288 /// fallback paths in `readiness`/`readiness_mut`.
289 fn socket(&self, token: Token) -> Option<&TcpStream>;
290 /// If end_stream is called on a client it means the stream has PROPERLY finished,
291 /// the server has completed serving the response and informs the endpoint that this stream won't be used anymore.
292 /// If end_stream is called on a server it means the stream was BROKEN, the client was most likely disconnected or encountered an error
293 /// 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
294 /// the backend were read. However it is almost certain that all bytes were not already sent to the client.
295 fn end_stream<L: ListenerHandler + L7ListenerHandler>(
296 &mut self,
297 token: Token,
298 stream: GlobalStreamId,
299 context: &mut Context<L>,
300 );
301 /// If start_stream is called on a client it means the stream should be attached to this endpoint,
302 /// the stream might be recovering from a disconnection, in any case at this point its response MUST be empty.
303 /// If the start_stream is called on a H2 server it means the stream is a server push and its request MUST be empty.
304 /// Returns false if the stream could not be started (e.g. max concurrent streams reached).
305 fn start_stream<L: ListenerHandler + L7ListenerHandler>(
306 &mut self,
307 token: Token,
308 stream: GlobalStreamId,
309 context: &mut Context<L>,
310 ) -> bool;
311}
312
313/// Shared logic for half-close accounting: clear `bit` from `readiness.event` on
314/// socket errors/would-block, and return `true` (yield) when no bytes were
315/// transferred so the caller can park the half. Rationale for clearing only
316/// `bit` (not both halves) on `Closed`: the opposite half may still need one
317/// last pass to flush queued frames or the TLS close_notify.
318fn update_readiness(
319 size: usize,
320 status: SocketResult,
321 readiness: &mut Readiness,
322 bit: Ready,
323) -> bool {
324 trace!(
325 "{} size={}, status={:?}",
326 log_module_context!(),
327 size,
328 status
329 );
330 match status {
331 SocketResult::Continue => {}
332 SocketResult::Closed | SocketResult::Error | SocketResult::WouldBlock => {
333 readiness.event.remove(bit);
334 }
335 }
336 if size > 0 {
337 false
338 } else {
339 readiness.event.remove(bit);
340 true
341 }
342}
343
344fn update_readiness_after_read(
345 size: usize,
346 status: SocketResult,
347 readiness: &mut Readiness,
348) -> bool {
349 update_readiness(size, status, readiness, Ready::READABLE)
350}
351
352fn update_readiness_after_write(
353 size: usize,
354 status: SocketResult,
355 readiness: &mut Readiness,
356) -> bool {
357 update_readiness(size, status, readiness, Ready::WRITABLE)
358}
359pub struct Context<L: ListenerHandler + L7ListenerHandler> {
360 pub streams: Vec<Stream>,
361 /// Streams whose state is `StreamState::Link` and need backend connection.
362 /// Replaces the O(n) scan of `streams` in the ready loop.
363 pub pending_links: VecDeque<GlobalStreamId>,
364 /// Reverse index: backend token -> global stream IDs currently in
365 /// `StreamState::Linked(token)`. Eliminates O(n) scans of `streams`
366 /// when handling backend connect/disconnect/timeout/close events.
367 pub backend_streams: HashMap<Token, Vec<GlobalStreamId>>,
368 pub pool: Weak<RefCell<Pool>>,
369 pub listener: Rc<RefCell<L>>,
370 /// Connection/session ULID — mirrors `Mux.session_ulid`. Stored here so
371 /// per-stream `HttpContext` construction in [`Self::create_stream`] can
372 /// stamp the session slot of the log-context bracket without reaching
373 /// back into the parent [`Mux`].
374 pub session_ulid: Ulid,
375 pub session_address: Option<SocketAddr>,
376 pub public_address: SocketAddr,
377 pub debug: DebugHistory,
378 /// Shrink threshold ratio for recycled stream slots.
379 /// Vec is shrunk when total_slots > active_streams * ratio.
380 pub h2_stream_shrink_ratio: usize,
381 /// TLS SNI value negotiated at handshake, propagated to every
382 /// per-stream [`HttpContext`] so the routing layer can enforce
383 /// the SNI ↔ `:authority` binding on every H2 stream (and the
384 /// single H1 request). `None` for plaintext listeners or when
385 /// the client omitted the SNI extension. Stored pre-lowercased
386 /// and without a port for cheap exact-match comparison.
387 pub tls_server_name: Option<String>,
388 /// Snapshot of the SAN set of the certificate Sōzu actually served at
389 /// the TLS handshake. Captured once in `https.rs::upgrade_handshake`
390 /// from the resolver and frozen for the connection lifetime so H2
391 /// stream coalescing (RFC 7540 §9.1.1 / RFC 9113 §9.1.1) accepts any
392 /// `:authority` covered by the certificate, with RFC 6125 §6.4.3
393 /// wildcard handling. `None` for plaintext listeners or when SNI was
394 /// absent. `Some(empty)` when the default cert was served — every
395 /// `:authority` is rejected. `Arc` so the snapshot is shared across
396 /// every per-stream `HttpContext` without re-allocation.
397 pub tls_cert_names: Option<Arc<Vec<String>>>,
398 /// Whether the routing layer must reject any request whose authority
399 /// host does not exact-match `tls_server_name` (CWE-346 / CWE-444).
400 /// Mirrors `HttpsListenerConfig::strict_sni_binding`; captured once
401 /// at `Context::new` so routing decisions on each stream avoid a
402 /// per-stream `listener.borrow()`.
403 pub strict_sni_binding: bool,
404 /// Whether the request-side block walk must strip any client-supplied
405 /// `X-Real-IP` header before forwarding (anti-spoofing). Mirrors
406 /// `HttpListenerConfig::elide_x_real_ip` /
407 /// `HttpsListenerConfig::elide_x_real_ip`; captured once at
408 /// `Context::new` so per-stream `HttpContext`s do not need to call
409 /// `listener.borrow()` again. Independent of `send_x_real_ip`.
410 pub elide_x_real_ip: bool,
411 /// Whether `on_request_headers` injects a proxy-generated `X-Real-IP`
412 /// header carrying the connection peer IP (post-PROXY-v2 unwrap).
413 /// Mirrors `HttpListenerConfig::send_x_real_ip` /
414 /// `HttpsListenerConfig::send_x_real_ip`; captured once at
415 /// `Context::new`. Independent of `elide_x_real_ip`.
416 pub send_x_real_ip: bool,
417 /// Negotiated TLS protocol version short-form (e.g. `"TLSv1.3"`).
418 /// Captured once at handshake completion in `https.rs` and propagated
419 /// to every per-stream [`HttpContext`] so the access log can record it
420 /// without reaching back into the rustls session per request. `None`
421 /// for plaintext listeners.
422 pub tls_version: Option<&'static str>,
423 /// Negotiated TLS cipher suite short-form (e.g.
424 /// `"TLS_AES_128_GCM_SHA256"`). Captured once at handshake completion
425 /// and propagated to every per-stream [`HttpContext`]. `None` for
426 /// plaintext listeners.
427 pub tls_cipher: Option<&'static str>,
428 /// Negotiated ALPN protocol short-form (e.g. `"h2"`, `"http/1.1"`).
429 /// Captured once at handshake completion and propagated to every
430 /// per-stream [`HttpContext`]. `None` for plaintext listeners or when
431 /// no ALPN was negotiated.
432 pub tls_alpn: Option<&'static str>,
433}
434
435impl<L: ListenerHandler + L7ListenerHandler> Context<L> {
436 pub fn new(
437 session_ulid: Ulid,
438 pool: Weak<RefCell<Pool>>,
439 listener: Rc<RefCell<L>>,
440 session_address: Option<SocketAddr>,
441 public_address: SocketAddr,
442 ) -> Self {
443 let h2_stream_shrink_ratio = listener
444 .borrow()
445 .get_h2_connection_config()
446 .stream_shrink_ratio as usize;
447 let strict_sni_binding = listener.borrow().get_strict_sni_binding();
448 let elide_x_real_ip = listener.borrow().get_elide_x_real_ip();
449 let send_x_real_ip = listener.borrow().get_send_x_real_ip();
450 Self {
451 streams: Vec::new(),
452 pending_links: VecDeque::new(),
453 backend_streams: HashMap::new(),
454 pool,
455 listener,
456 session_ulid,
457 session_address,
458 public_address,
459 debug: DebugHistory::new(),
460 h2_stream_shrink_ratio,
461 tls_server_name: None,
462 tls_cert_names: None,
463 strict_sni_binding,
464 elide_x_real_ip,
465 send_x_real_ip,
466 tls_version: None,
467 tls_cipher: None,
468 tls_alpn: None,
469 }
470 }
471
472 pub fn active_len(&self) -> usize {
473 self.streams
474 .iter()
475 .filter(|s| !matches!(s.state, StreamState::Recycle))
476 .count()
477 }
478
479 /// Shared accessor for the [`HttpContext`] owned by a stream.
480 ///
481 /// Prefer this over `&self.streams[stream_id].context` at call sites
482 /// that only need read access — it keeps the `Stream`/`HttpContext`
483 /// relationship encapsulated and reads the same regardless of whether
484 /// the caller is inside `Router::connect`, the H2 mux, or a free
485 /// helper. Panics on an out-of-bounds `stream_id`, which is the same
486 /// behaviour as the raw `streams[sid]` indexing it replaces.
487 pub fn http_context(&self, stream_id: GlobalStreamId) -> &HttpContext {
488 &self.streams[stream_id].context
489 }
490
491 /// Mutable sibling of [`Self::http_context`]. Use when routing
492 /// decisions need to stamp `cluster_id` / `backend_id` on the stream's
493 /// [`HttpContext`] (e.g. `Router::connect` at the fill-cluster /
494 /// fill-backend points).
495 pub fn http_context_mut(&mut self, stream_id: GlobalStreamId) -> &mut HttpContext {
496 &mut self.streams[stream_id].context
497 }
498
499 /// Register a stream as linked to a backend token in the reverse index.
500 pub fn link_stream(&mut self, stream_id: GlobalStreamId, token: Token) {
501 self.streams[stream_id].state = StreamState::Linked(token);
502 self.backend_streams
503 .entry(token)
504 .or_default()
505 .push(stream_id);
506 }
507
508 /// Remove a stream from the backend reverse index if it is currently
509 /// `Linked`. Returns the backend token if one was removed.
510 pub fn unlink_stream(&mut self, stream_id: GlobalStreamId) -> Option<Token> {
511 if let StreamState::Linked(token) = self.streams[stream_id].state {
512 remove_backend_stream(&mut self.backend_streams, token, stream_id);
513 Some(token)
514 } else {
515 None
516 }
517 }
518
519 pub fn create_stream(&mut self, request_id: Ulid, window: u32) -> Option<GlobalStreamId> {
520 let http_context = {
521 let listener = self.listener.borrow();
522 let mut http_context = HttpContext::new(
523 self.session_ulid,
524 request_id,
525 listener.protocol(),
526 self.public_address,
527 self.session_address,
528 listener.get_sticky_name().to_string(),
529 listener.get_sozu_id_header().to_string(),
530 self.elide_x_real_ip,
531 self.send_x_real_ip,
532 );
533 // Propagate the connection-scoped TLS SNI onto every per-stream
534 // HttpContext so `route_from_request` can enforce the SNI ↔
535 // `:authority` binding for each H2 stream independently.
536 http_context.tls_server_name = self.tls_server_name.clone();
537 // Mirror the frozen-at-handshake SAN snapshot. `Arc` clone is a
538 // refcount bump, not a deep copy — every per-stream
539 // `HttpContext` shares the same `Vec<String>`.
540 http_context.tls_cert_names = self.tls_cert_names.clone();
541 // Mirror the listener's strict_sni_binding flag onto each
542 // HttpContext so the routing layer can honor operator opt-outs
543 // without reaching back into the listener on every request.
544 http_context.strict_sni_binding = self.strict_sni_binding;
545 // Propagate the connection-scoped TLS metadata onto every
546 // per-stream HttpContext so the access log can record it without
547 // touching the rustls session on every request. These are
548 // `&'static str` borrows from the rustls label tables — copy is
549 // a pointer move.
550 http_context.tls_version = self.tls_version;
551 http_context.tls_cipher = self.tls_cipher;
552 http_context.tls_alpn = self.tls_alpn;
553 http_context
554 };
555 let recycle_slot = self
556 .streams
557 .iter()
558 .position(|s| s.state == StreamState::Recycle);
559 if let Some(stream_id) = recycle_slot {
560 let stream = &mut self.streams[stream_id];
561 trace!("{} Reuse stream: {}", log_module_context!(), stream_id);
562 stream.state = StreamState::Idle;
563 stream.attempts = 0;
564 stream.front_received_end_of_stream = false;
565 stream.back_received_end_of_stream = false;
566 stream.front_data_received = 0;
567 stream.back_data_received = 0;
568 stream.request_counted = false;
569 stream.window = i32::try_from(window).unwrap_or(i32::MAX);
570 stream.context = http_context;
571 stream.back.clear();
572 stream.back.storage.clear();
573 stream.front.clear();
574 stream.front.storage.clear();
575 stream.metrics.reset();
576 stream.metrics.mark_request_start();
577 // After recycling a slot, check if the Vec has excessive trailing
578 // Recycle entries (more than 2x active streams of total capacity).
579 let active = self.active_len();
580 let total = self.streams.len();
581 if total > 1 && active > 0 && total > active * self.h2_stream_shrink_ratio {
582 self.shrink_trailing_recycle();
583 }
584 return Some(stream_id);
585 }
586 self.streams
587 .push(Stream::new(self.pool.clone(), http_context, window)?);
588 Some(self.streams.len() - 1)
589 }
590
591 /// Remove consecutive `Recycle` entries from the end of the streams Vec.
592 ///
593 /// This prevents unbounded growth when H2 streams are created and recycled
594 /// over time, reclaiming memory from slots that are no longer needed.
595 pub fn shrink_trailing_recycle(&mut self) {
596 while self
597 .streams
598 .last()
599 .is_some_and(|s| s.state == StreamState::Recycle)
600 {
601 self.streams.pop();
602 }
603 }
604}
605
606/// Remove `stream_id` from the backend-token reverse index for `token`.
607/// Free function to allow split borrows when `context.streams` is already
608/// mutably borrowed (preventing a `Context::unlink_stream` call).
609pub(super) fn remove_backend_stream(
610 index: &mut HashMap<Token, Vec<GlobalStreamId>>,
611 token: Token,
612 stream_id: GlobalStreamId,
613) {
614 if let Some(ids) = index.get_mut(&token) {
615 ids.retain(|&id| id != stream_id);
616 if ids.is_empty() {
617 index.remove(&token);
618 }
619 }
620}
621
622pub struct Mux<Front: SocketHandler, L: ListenerHandler + L7ListenerHandler> {
623 pub configured_frontend_timeout: Duration,
624 pub frontend_token: Token,
625 pub frontend: Connection<Front>,
626 pub router: Router,
627 pub context: Context<L>,
628 /// Per-session correlation ID generated at construction time. Included in
629 /// every log line emitted from this module so all events for a single
630 /// frontend connection can be reassembled (independent of the ephemeral
631 /// per-stream request id used by access logs).
632 pub session_ulid: Ulid,
633}
634
635impl<Front: SocketHandler, L: ListenerHandler + L7ListenerHandler> Mux<Front, L> {
636 pub fn front_socket(&self) -> &TcpStream {
637 self.frontend.socket()
638 }
639}
640
641impl<Front: SocketHandler + std::fmt::Debug, L: ListenerHandler + L7ListenerHandler> Mux<Front, L> {
642 fn sync_upgrade_buffers(&mut self) {
643 for stream in &mut self.context.streams {
644 stream
645 .front
646 .storage
647 .buffer
648 .sync(stream.front.storage.end, stream.front.storage.head);
649 stream
650 .back
651 .storage
652 .buffer
653 .sync(stream.back.storage.end, stream.back.storage.head);
654 }
655 }
656
657 fn delay_close_for_frontend_flush(&mut self, reason: &'static str) -> bool {
658 let _ = self.frontend.initiate_close_notify();
659 // LIFECYCLE §9 invariant 16: consult per-stream back-buffers in
660 // addition to the connection-level pending-write predicate so
661 // shutdown does not close while any open H2 stream still has
662 // kawa bytes queued after a voluntary scheduler yield.
663 if self
664 .frontend
665 .has_pending_write_including_streams(&self.context)
666 {
667 let readiness = self.frontend.readiness_mut();
668 readiness.interest = Ready::WRITABLE | Ready::HUP | Ready::ERROR;
669 readiness.signal_pending_write();
670 debug!(
671 "{} Mux delaying close on {}: {:?}",
672 log_context!(self),
673 reason,
674 self.frontend
675 );
676 true
677 } else {
678 false
679 }
680 }
681
682 /// Drive the frontend I/O path during shutdown, when the server is polling
683 /// `shutting_down()` outside the normal epoll readiness loop.
684 ///
685 /// This is required for H2 graceful shutdown because a stream may still
686 /// need one last readable pass to observe the peer's END_STREAM or one last
687 /// writable pass to retire the stream, emit GOAWAY, or flush TLS records.
688 fn drive_frontend_shutdown_io(&mut self) -> SessionIsToBeClosed {
689 let force_h2_read = matches!(self.frontend, Connection::H2(_));
690 let force_h2_write = matches!(self.frontend, Connection::H2(_));
691 let readiness = self.frontend.readiness().clone();
692 if !force_h2_read
693 && !force_h2_write
694 && readiness.event.is_empty()
695 && !self.frontend.has_pending_write()
696 {
697 return false;
698 }
699
700 if force_h2_read || self.frontend.readiness().event.is_readable() {
701 self.frontend
702 .readiness_mut()
703 .interest
704 .insert(Ready::READABLE);
705 match self
706 .frontend
707 .readable(&mut self.context, EndpointClient(&mut self.router))
708 {
709 MuxResult::Continue => {}
710 MuxResult::CloseSession | MuxResult::Upgrade => return true,
711 }
712 }
713
714 if !force_h2_write
715 && !self.frontend.has_pending_write()
716 && !self.frontend.readiness().event.is_writable()
717 {
718 return false;
719 }
720
721 let mut iterations = 0;
722 loop {
723 self.frontend
724 .readiness_mut()
725 .interest
726 .insert(Ready::WRITABLE);
727 if force_h2_write {
728 self.frontend.readiness_mut().signal_pending_write();
729 }
730 match self
731 .frontend
732 .writable(&mut self.context, EndpointClient(&mut self.router))
733 {
734 MuxResult::Continue => {}
735 MuxResult::CloseSession | MuxResult::Upgrade => return true,
736 }
737
738 iterations += 1;
739 if iterations >= MAX_LOOP_ITERATIONS
740 || (!self.frontend.has_pending_write()
741 && !self.frontend.readiness().event.is_writable())
742 {
743 break;
744 }
745 }
746 false
747 }
748}
749
750impl<Front: SocketHandler + std::fmt::Debug, L: ListenerHandler + L7ListenerHandler> SessionState
751 for Mux<Front, L>
752{
753 fn ready(
754 &mut self,
755 session: Rc<RefCell<dyn ProxySession>>,
756 proxy: Rc<RefCell<dyn L7Proxy>>,
757 _metrics: &mut SessionMetrics,
758 ) -> SessionResult {
759 let mut counter = 0;
760
761 if self.frontend.readiness().event.is_hup()
762 && !self.delay_close_for_frontend_flush("frontend HUP")
763 {
764 debug!(
765 "{} Mux closing on frontend HUP: {:?}",
766 log_context!(self),
767 self.frontend
768 );
769 return SessionResult::Close;
770 }
771
772 // Start service timers on all active streams after the HUP check.
773 // This mirrors session-level service_start/service_stop in Http(s)Session::ready()
774 // to measure only CPU processing time, excluding epoll wait between cycles.
775 for stream in &mut self.context.streams {
776 if stream.state.is_open() {
777 stream.metrics.service_start();
778 }
779 }
780
781 let start = Instant::now();
782 self.context.debug.push(DebugEvent::ReadyTimestamp(
783 std::time::SystemTime::now()
784 .duration_since(std::time::UNIX_EPOCH)
785 .unwrap_or_default()
786 .as_millis() as usize,
787 ));
788 trace!("{} {:?}", log_context!(self), start);
789 loop {
790 self.context.debug.push(DebugEvent::LoopStart);
791 loop {
792 self.context.debug.push(DebugEvent::LoopIteration(counter));
793 if self.frontend.readiness().filter_interest().is_readable() {
794 let res = {
795 let context = &mut self.context;
796 let res = self
797 .frontend
798 .readable(context, EndpointClient(&mut self.router));
799 context.debug.push(DebugEvent::SR(
800 self.frontend_token,
801 res,
802 self.frontend.readiness().clone(),
803 ));
804 res
805 };
806 match res {
807 MuxResult::Continue => {}
808 MuxResult::CloseSession => {
809 if !self.delay_close_for_frontend_flush("frontend readable") {
810 debug!(
811 "{} Mux close from frontend readable: {:?}",
812 log_context!(self),
813 self.frontend
814 );
815 return SessionResult::Close;
816 }
817 }
818 MuxResult::Upgrade => {
819 self.sync_upgrade_buffers();
820 return SessionResult::Upgrade;
821 }
822 }
823 }
824
825 let mut all_backends_readiness_are_empty = true;
826 let mut dead_backends = Vec::new();
827 let mut backend_close: Option<(&'static str, Token)> = None;
828 for (token, client) in self.router.backends.iter_mut() {
829 let readiness = client.readiness_mut();
830 // Check the raw event for HUP/ERROR — not filter_interest(),
831 // because interest only contains READABLE|WRITABLE and would
832 // always mask out HUP (0b01000) and ERROR (0b00100).
833 let dead = readiness.event.is_hup() || readiness.event.is_error();
834 if dead {
835 trace!(
836 "{} Backend({:?}) -> {:?}",
837 log_context_lite!(self),
838 token,
839 readiness
840 );
841 readiness.event.remove(Ready::WRITABLE);
842 }
843
844 if client.readiness().filter_interest().is_writable() {
845 let position = client.position_mut();
846 match position {
847 Position::Client(
848 cluster_id,
849 backend,
850 BackendStatus::Connecting(start),
851 ) => {
852 #[cfg(debug_assertions)]
853 self.context
854 .debug
855 .push(DebugEvent::CCS(*token, cluster_id.clone()));
856
857 let mut backend_borrow = backend.borrow_mut();
858 if backend_borrow.retry_policy.is_down() {
859 info!(
860 "{} backend server {} at {} is up",
861 log_context_lite!(self),
862 backend_borrow.backend_id,
863 backend_borrow.address
864 );
865 incr!(
866 "backend.up",
867 Some(cluster_id),
868 Some(&backend_borrow.backend_id)
869 );
870 gauge!(
871 names::backend::AVAILABLE,
872 1,
873 Some(cluster_id),
874 Some(&backend_borrow.backend_id)
875 );
876 push_event(Event {
877 kind: EventKind::BackendUp as i32,
878 backend_id: Some(backend_borrow.backend_id.to_owned()),
879 address: Some(backend_borrow.address.into()),
880 cluster_id: Some(cluster_id.to_owned()),
881 metric_detail: None,
882 });
883 }
884
885 //successful connection, reset failure counter
886 backend_borrow.failures = 0;
887 backend_borrow.set_connection_time(start.elapsed());
888 backend_borrow.retry_policy.succeed();
889
890 if let Some(ids) = self.context.backend_streams.get(token) {
891 for &stream_id in ids {
892 self.context.streams[stream_id].metrics.backend_connected();
893 backend_borrow.active_requests += 1;
894 }
895 }
896 trace!(
897 "{} connection success: {:#?}",
898 log_context_lite!(self),
899 backend_borrow
900 );
901 drop(backend_borrow);
902 *position = Position::Client(
903 std::mem::take(cluster_id),
904 backend.clone(),
905 BackendStatus::Connected,
906 );
907 client
908 .timeout_container()
909 .set_duration(self.router.configured_backend_timeout);
910 }
911 Position::Client(..) => {}
912 Position::Server => {
913 error!(
914 "{} backend connection cannot be in Server position",
915 log_context_lite!(self)
916 );
917 }
918 }
919 let res = {
920 let context = &mut self.context;
921 let res = client.writable(context, EndpointServer(&mut self.frontend));
922 context.debug.push(DebugEvent::CW(
923 *token,
924 res,
925 client.readiness().clone(),
926 ));
927 res
928 };
929 match res {
930 MuxResult::Continue => {}
931 MuxResult::Upgrade => {
932 error!(
933 "{} only frontend connections can trigger Upgrade",
934 log_context_lite!(self)
935 );
936 }
937 MuxResult::CloseSession => {
938 backend_close = Some(("backend writable", *token));
939 break;
940 }
941 }
942 // Cross-readiness: backend wrote → wake frontend reader
943 let context = &mut self.context;
944 self.frontend.try_resume_reading(context);
945 }
946
947 if client.readiness().filter_interest().is_readable() {
948 let res = {
949 let context = &mut self.context;
950 let res = client.readable(context, EndpointServer(&mut self.frontend));
951 context.debug.push(DebugEvent::CR(
952 *token,
953 res,
954 client.readiness().clone(),
955 ));
956 res
957 };
958 match res {
959 MuxResult::Continue => {}
960 MuxResult::Upgrade => {
961 error!(
962 "{} only frontend connections can trigger Upgrade (readable)",
963 log_context_lite!(self)
964 );
965 }
966 MuxResult::CloseSession => {
967 backend_close = Some(("backend readable", *token));
968 break;
969 }
970 }
971 }
972
973 if dead
974 && !client.readiness().filter_interest().is_readable()
975 && !client.has_buffer_pressure(&self.context)
976 {
977 self.context
978 .debug
979 .push(DebugEvent::CH(*token, client.readiness().clone()));
980 trace!("{} Closing {:#?}", log_context_lite!(self), client);
981 match client.position() {
982 Position::Client(cluster_id, backend, BackendStatus::Connecting(_)) => {
983 let mut backend_borrow = backend.borrow_mut();
984 backend_borrow.failures += 1;
985
986 let already_unavailable = backend_borrow.retry_policy.is_down();
987 backend_borrow.retry_policy.fail();
988 incr!(
989 "backend.connections.error",
990 Some(cluster_id),
991 Some(&backend_borrow.backend_id)
992 );
993 if !already_unavailable && backend_borrow.retry_policy.is_down() {
994 error!(
995 "{} backend server {} at {} is down",
996 log_context_lite!(self),
997 backend_borrow.backend_id,
998 backend_borrow.address
999 );
1000 incr!(
1001 "backend.down",
1002 Some(cluster_id),
1003 Some(&backend_borrow.backend_id)
1004 );
1005 gauge!(
1006 names::backend::AVAILABLE,
1007 0,
1008 Some(cluster_id),
1009 Some(&backend_borrow.backend_id)
1010 );
1011 push_event(Event {
1012 kind: EventKind::BackendDown as i32,
1013 backend_id: Some(backend_borrow.backend_id.to_owned()),
1014 address: Some(backend_borrow.address.into()),
1015 cluster_id: Some(cluster_id.to_owned()),
1016 metric_detail: None,
1017 });
1018 }
1019 trace!(
1020 "{} connection fail: {:#?}",
1021 log_context_lite!(self),
1022 backend_borrow
1023 );
1024 }
1025 Position::Client(_, backend, _) => {
1026 let mut backend_borrow = backend.borrow_mut();
1027 let count = self
1028 .context
1029 .backend_streams
1030 .get(token)
1031 .map_or(0, |ids| ids.len());
1032 backend_borrow.active_requests =
1033 backend_borrow.active_requests.saturating_sub(count);
1034 }
1035 Position::Server => {
1036 error!(
1037 "{} dead backend cannot be in Server position",
1038 log_context_lite!(self)
1039 );
1040 }
1041 }
1042 client.close(&mut self.context, EndpointServer(&mut self.frontend));
1043 dead_backends.push(*token);
1044 }
1045
1046 if !client.readiness().filter_interest().is_empty() {
1047 all_backends_readiness_are_empty = false;
1048 }
1049 }
1050 // Remove dead backends from the map BEFORE handling
1051 // backend_close. client.close() already decremented
1052 // connections_per_backend / backend.connections gauges in
1053 // the loop above; if we return SessionResult::Close before
1054 // removing them, Mux::close() would decrement again
1055 // (double-decrement → gauge underflow).
1056 if !dead_backends.is_empty() {
1057 for token in &dead_backends {
1058 let proxy_borrow = proxy.borrow();
1059 if let Some(mut client) = self.router.backends.remove(token) {
1060 client.timeout_container().cancel();
1061 let socket = client.socket_mut();
1062 if let Err(e) = proxy_borrow.deregister_socket(socket) {
1063 error!(
1064 "{} error deregistering back socket({:?}): {:?}",
1065 log_context!(self),
1066 socket,
1067 e
1068 );
1069 }
1070 // invariant: write-only shutdown — Shutdown::Both on a TLS frontend
1071 // discards the receive buffer and elicits TCP RST, truncating the
1072 // already-queued response. Canonical write-up: `lib/src/https.rs:650-655`.
1073 // Backend sockets follow the same discipline for symmetry.
1074 if let Err(e) = socket.shutdown(Shutdown::Write)
1075 && e.kind() != ErrorKind::NotConnected
1076 {
1077 error!(
1078 "{} error shutting down back socket({:?}): {:?}",
1079 log_context!(self),
1080 socket,
1081 e
1082 );
1083 }
1084 } else {
1085 error!("{} session {:?} has no backend!", log_context!(self), token);
1086 }
1087 if !proxy_borrow.remove_session(*token) {
1088 error!(
1089 "{} session {:?} was already removed!",
1090 log_context!(self),
1091 token
1092 );
1093 }
1094 }
1095 trace!("{} FRONTEND: {:#?}", log_context!(self), self.frontend);
1096 trace!(
1097 "{} BACKENDS: {:#?}",
1098 log_context!(self),
1099 self.router.backends
1100 );
1101 }
1102 if let Some((reason, token)) = backend_close {
1103 if !self.delay_close_for_frontend_flush(reason) {
1104 debug!(
1105 "{} Mux close from {} token={:?}: frontend={:?}",
1106 log_context!(self),
1107 reason,
1108 token,
1109 self.frontend
1110 );
1111 return SessionResult::Close;
1112 }
1113 all_backends_readiness_are_empty = false;
1114 }
1115
1116 if self.frontend.readiness().filter_interest().is_writable() {
1117 let res = {
1118 let context = &mut self.context;
1119 let res = self
1120 .frontend
1121 .writable(context, EndpointClient(&mut self.router));
1122 context.debug.push(DebugEvent::SW(
1123 self.frontend_token,
1124 res,
1125 self.frontend.readiness().clone(),
1126 ));
1127 res
1128 };
1129 match res {
1130 MuxResult::Continue => {}
1131 MuxResult::CloseSession => {
1132 if !self.delay_close_for_frontend_flush("frontend writable") {
1133 debug!(
1134 "{} Mux close from frontend writable: {:?}",
1135 log_context!(self),
1136 self.frontend
1137 );
1138 return SessionResult::Close;
1139 }
1140 }
1141 MuxResult::Upgrade => {
1142 self.sync_upgrade_buffers();
1143 return SessionResult::Upgrade;
1144 }
1145 }
1146 // Cross-readiness: frontend wrote → wake parked backends.
1147 // If any backend resumes, invalidate the stale readiness
1148 // flag so the inner loop continues instead of breaking.
1149 let context = &mut self.context;
1150 for backend in self.router.backends.values_mut() {
1151 if backend.try_resume_reading(context) {
1152 all_backends_readiness_are_empty = false;
1153 }
1154 }
1155 }
1156
1157 if self.frontend.readiness().filter_interest().is_empty()
1158 && all_backends_readiness_are_empty
1159 {
1160 break;
1161 }
1162
1163 counter += 1;
1164 if counter >= MAX_LOOP_ITERATIONS {
1165 incr!(names::http::INFINITE_LOOP_ERROR);
1166 if self.frontend.has_pending_write() {
1167 debug!(
1168 "{} Mux loop budget exhausted while frontend flush pending: {:?}",
1169 log_context!(self),
1170 self.frontend
1171 );
1172 self.frontend.readiness_mut().event.remove(Ready::WRITABLE);
1173 self.frontend.timeout_container().set(self.frontend_token);
1174 break;
1175 }
1176 return SessionResult::Close;
1177 }
1178 }
1179
1180 let context = &mut self.context;
1181 let answers_rc = context.listener.borrow().get_answers().clone();
1182 let mut dirty = false;
1183 while let Some(stream_id) = context.pending_links.pop_front() {
1184 let Some(stream) = context.streams.get(stream_id) else {
1185 continue;
1186 };
1187 if stream.state != StreamState::Link {
1188 continue;
1189 }
1190 // Before the first request triggers a stream Link, the frontend timeout is set
1191 // to a shorter request_timeout, here we switch to the longer nominal timeout
1192 self.frontend
1193 .timeout_container()
1194 .set_duration(self.configured_frontend_timeout);
1195 let front_readiness = self.frontend.readiness_mut();
1196 dirty = true;
1197 match self.router.connect(
1198 stream_id,
1199 context,
1200 session.clone(),
1201 proxy.clone(),
1202 self.frontend_token,
1203 ) {
1204 Ok(_) => {
1205 let state = context.streams[stream_id].state;
1206 context.debug.push(DebugEvent::CC(stream_id, state));
1207 }
1208 Err(error) => {
1209 trace!("{} Connection error: {}", log_module_context!(), error);
1210 let stream = &mut context.streams[stream_id];
1211 let answers = answers_rc.borrow();
1212 use BackendConnectionError as BE;
1213 match error {
1214 BE::MaxConnectionRetries(_)
1215 | BE::MaxSessionsMemory
1216 | BE::MaxBuffers => {
1217 warn!(
1218 "{} backend retry budget exhausted: {}",
1219 log_module_context!(stream.context),
1220 error
1221 );
1222 set_default_answer(stream, front_readiness, 503, &answers);
1223 }
1224 BE::Backend(BackendError::NoBackendForCluster(_)) => {
1225 set_default_answer(stream, front_readiness, 503, &answers);
1226 }
1227 BE::RetrieveClusterError(RetrieveClusterError::RetrieveFrontend(
1228 ref err,
1229 )) => {
1230 // RFC 9110 §15.5.1: a malformed authority is a
1231 // 400. A syntactically valid authority that
1232 // simply has no matching frontend stays on the
1233 // historical 404 path.
1234 let code = match err {
1235 FrontendFromRequestError::HostParse { .. }
1236 | FrontendFromRequestError::InvalidCharsAfterHost(_) => 400,
1237 FrontendFromRequestError::NoClusterFound(_) => 404,
1238 };
1239 set_default_answer(stream, front_readiness, code, &answers);
1240 }
1241 BE::RetrieveClusterError(RetrieveClusterError::UnauthorizedRoute) => {
1242 set_default_answer(stream, front_readiness, 401, &answers);
1243 }
1244 BE::RetrieveClusterError(
1245 RetrieveClusterError::SniAuthorityMismatch { .. },
1246 ) => {
1247 // RFC 9110 §15.5.20: 421 Misdirected Request is the
1248 // semantically correct status for an authority that
1249 // does not belong to this TLS connection. The
1250 // http.sni_authority_mismatch metric emitted in
1251 // `route_from_request` remains the durable signal;
1252 // the 421 body here is what a client sees and may
1253 // retry on a fresh TLS connection with a matching SNI.
1254 set_default_answer(stream, front_readiness, 421, &answers);
1255 }
1256 BE::RetrieveClusterError(RetrieveClusterError::HttpsRedirect) => {
1257 // Use the redirect status stashed by `Router::route_from_request`
1258 // (#1009). Falls back to 301 for the legacy
1259 // `cluster.https_redirect = true` path that does
1260 // not set the field.
1261 let code = stream.context.redirect_status.unwrap_or(301);
1262 set_default_answer(stream, front_readiness, code, &answers);
1263 }
1264
1265 BE::Backend(ref e) => {
1266 error!("{} backend connection error: {}", log_module_context!(), e);
1267 set_default_answer(stream, front_readiness, 503, &answers);
1268 }
1269 BE::RetrieveClusterError(ref other) => {
1270 error!(
1271 "{} unexpected RetrieveClusterError variant: {:?}",
1272 log_module_context!(),
1273 other
1274 );
1275 set_default_answer(stream, front_readiness, 503, &answers);
1276 }
1277 // TCP specific error
1278 BE::NotFound(ref msg) => {
1279 error!(
1280 "{} NotFound is TCP-specific, not reachable in mux: {:?}",
1281 log_module_context!(),
1282 msg
1283 );
1284 set_default_answer(stream, front_readiness, 503, &answers);
1285 }
1286 // Per-(cluster, source-IP) connection limit reached.
1287 // Emit HTTP 429 with the resolved `Retry-After`. The
1288 // value is computed in `Router::connect` (where the
1289 // SessionManager + cluster override are reachable)
1290 // and stashed on the stream context just before the
1291 // error is returned, so the answer engine can render
1292 // (or elide) the header without re-deriving the
1293 // resolution chain here.
1294 BE::TooManyConnectionsPerIp { ref cluster_id } => {
1295 debug!(
1296 "{} per-(cluster, source-IP) limit hit for cluster {:?}",
1297 log_module_context!(),
1298 cluster_id
1299 );
1300 let retry_after = stream.context.retry_after_seconds;
1301 set_default_answer_with_retry_after(
1302 stream,
1303 front_readiness,
1304 429,
1305 &answers,
1306 retry_after,
1307 );
1308 }
1309 }
1310 context.debug.push(DebugEvent::CCF(stream_id, error));
1311 }
1312 }
1313 // All routing error arms now set a default answer, transitioning
1314 // the stream out of Link state. No re-enqueue needed.
1315 }
1316 if !dirty {
1317 break;
1318 }
1319 }
1320
1321 // Stop service timers before yielding to epoll, so idle wait time is excluded
1322 // from the service_time metric. For Close/Upgrade returns, close() handles cleanup.
1323 for stream in &mut self.context.streams {
1324 if stream.state.is_open() {
1325 stream.metrics.service_stop();
1326 }
1327 }
1328
1329 #[cfg(debug_assertions)]
1330 {
1331 // Verify backend_streams index matches actual stream states.
1332 let mut expected: HashMap<Token, Vec<GlobalStreamId>> = HashMap::new();
1333 for (id, stream) in self.context.streams.iter().enumerate() {
1334 if let StreamState::Linked(token) = stream.state {
1335 expected.entry(token).or_default().push(id);
1336 }
1337 }
1338 assert_eq!(
1339 expected.len(),
1340 self.context.backend_streams.len(),
1341 "backend_streams index key count mismatch: expected={:?}, actual={:?}",
1342 expected,
1343 self.context.backend_streams
1344 );
1345 for (token, mut expected_ids) in expected {
1346 let mut actual_ids = self
1347 .context
1348 .backend_streams
1349 .get(&token)
1350 .cloned()
1351 .unwrap_or_default();
1352 expected_ids.sort();
1353 actual_ids.sort();
1354 assert_eq!(
1355 expected_ids, actual_ids,
1356 "backend_streams index mismatch for token {token:?}",
1357 );
1358 }
1359 }
1360
1361 SessionResult::Continue
1362 }
1363
1364 fn update_readiness(&mut self, token: Token, events: Ready) {
1365 trace!("{} EVENTS: {:?} on {:?}", log_context!(self), events, token);
1366 self.context.debug.push(DebugEvent::EV(token, events));
1367 if token == self.frontend_token {
1368 self.frontend.readiness_mut().event |= events;
1369 } else if let Some(c) = self.router.backends.get_mut(&token) {
1370 c.readiness_mut().event |= events;
1371 }
1372 }
1373
1374 fn timeout(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult {
1375 trace!("{} MuxState::timeout({:?})", log_context!(self), token);
1376 let front_is_h2 = match self.frontend {
1377 Connection::H1(_) => false,
1378 Connection::H2(_) => true,
1379 };
1380 let answers_rc = self.context.listener.borrow().get_answers().clone();
1381 let mut should_close = true;
1382 let mut should_write = false;
1383 if self.frontend_token == token {
1384 trace!(
1385 "{} MuxState::timeout_frontend({:#?})",
1386 log_context!(self),
1387 self.frontend
1388 );
1389 self.frontend.timeout_container().triggered();
1390 // The per-stream reaper (bidirectional-idle + outbound
1391 // flow-control-stall guards) normally runs only from `readable()`,
1392 // but a fully-silent peer never triggers a read event. Run it on the
1393 // connection-timeout path too so a window-stalled stream — a buffered
1394 // response the peer refuses to drain by holding its receive window
1395 // shut — is reaped and its MAX_CONCURRENT_STREAMS slot freed, instead
1396 // of lingering until the 30-minute zombie checker. The reaper queues
1397 // an `RST_STREAM(CANCEL)`; because `has_pending_write()` does NOT
1398 // observe `pending_rst_streams` (it gates connection close, so a
1399 // queued RST must not read as "keep open"), set `should_write` via
1400 // the dedicated `has_pending_control_write()` probe so the reset is
1401 // actually flushed to the peer before the connection closes — without
1402 // it, a fully-silent peer's stalled stream is freed but the peer sees
1403 // only EOF, never the RST(CANCEL).
1404 if let Connection::H2(h2) = &mut self.frontend {
1405 h2.cancel_timed_out_streams(
1406 &mut self.context,
1407 &mut EndpointClient(&mut self.router),
1408 );
1409 if h2.has_pending_control_write() {
1410 should_write = true;
1411 }
1412 }
1413 if self.frontend.has_pending_write() {
1414 should_write = true;
1415 }
1416 let front_readiness = self.frontend.readiness_mut();
1417 for stream_id in 0..self.context.streams.len() {
1418 match self.context.streams[stream_id].state {
1419 StreamState::Idle => {
1420 // In h1 an Idle stream is always the first request, so we can send a 408
1421 // In h2 an Idle stream doesn't necessarily hold a request yet,
1422 // in most cases it was just reserved, so we can just ignore them.
1423 if !front_is_h2 {
1424 let answers = answers_rc.borrow();
1425 let stream = &mut self.context.streams[stream_id];
1426 stream.context.access_log_message = Some("client_timeout");
1427 set_default_answer(stream, front_readiness, 408, &answers);
1428 should_write = true;
1429 }
1430 }
1431 StreamState::Link => {
1432 // This is an unusual case, as we have both a complete request and no
1433 // available backend yet. For now, we answer with 503.
1434 // Not a timeout-driven outcome from the operator's
1435 // perspective — leave access_log_message as None.
1436 let answers = answers_rc.borrow();
1437 let stream = &mut self.context.streams[stream_id];
1438 set_default_answer(stream, front_readiness, 503, &answers);
1439 should_write = true;
1440 }
1441 StreamState::Linked(_) => {
1442 // The frontend timed out while a stream is linked to a backend.
1443 // The backend timeout should handle this, but in case the backend
1444 // is also stalled, send a 504 and terminate the stream.
1445 if !self.context.streams[stream_id].back.consumed {
1446 self.context.unlink_stream(stream_id);
1447 let answers = answers_rc.borrow();
1448 let stream = &mut self.context.streams[stream_id];
1449 stream.context.access_log_message =
1450 Some("client_timeout_during_response");
1451 set_default_answer(stream, front_readiness, 504, &answers);
1452 should_write = true;
1453 } else if self.context.streams[stream_id].back.is_completed() {
1454 // Response fully proxied, stream can be closed
1455 } else if self.context.streams[stream_id].back.is_terminated()
1456 || self.context.streams[stream_id].back.is_error()
1457 {
1458 // Response is terminated/error but not fully written to frontend.
1459 // Keep the session alive briefly to flush remaining data.
1460 should_close = false;
1461 } else {
1462 // Partial response in progress — forcefully terminate
1463 self.context.unlink_stream(stream_id);
1464 let stream = &mut self.context.streams[stream_id];
1465 stream.context.access_log_message =
1466 Some("client_timeout_during_response");
1467 forcefully_terminate_answer(
1468 stream,
1469 front_readiness,
1470 H2Error::InternalError,
1471 );
1472 should_write = true;
1473 }
1474 // end_stream is called in a second pass below to avoid
1475 // borrow conflicts on context.streams.
1476 }
1477 StreamState::Unlinked => {
1478 // A stream Unlinked already has a response and its backend closed.
1479 // In case it hasn't finished proxying we wait. Otherwise it is a stream
1480 // kept alive for a new request, which can be killed.
1481 if !self.context.streams[stream_id].back.is_completed() {
1482 should_close = false;
1483 }
1484 }
1485 StreamState::Recycle => {
1486 // A recycled stream is an h2 stream which doesn't hold a request anymore.
1487 // We can ignore it.
1488 }
1489 }
1490 }
1491 // Second pass: end streams that were linked to backends.
1492 // This is done separately to avoid borrow conflicts on context.streams.
1493 let linked_streams: Vec<(GlobalStreamId, Token)> = self
1494 .context
1495 .streams
1496 .iter()
1497 .enumerate()
1498 .filter_map(|(id, stream)| {
1499 if let StreamState::Linked(back_token) = stream.state {
1500 Some((id, back_token))
1501 } else {
1502 None
1503 }
1504 })
1505 .collect();
1506 for (stream_id, back_token) in linked_streams {
1507 if let Some(backend) = self.router.backends.get_mut(&back_token) {
1508 backend.end_stream(stream_id, &mut self.context);
1509 }
1510 }
1511 } else if let Some(backend) = self.router.backends.get_mut(&token) {
1512 trace!(
1513 "{} MuxState::timeout_backend({:#?})",
1514 log_context_lite!(self),
1515 backend
1516 );
1517 backend.timeout_container().triggered();
1518 let front_readiness = self.frontend.readiness_mut();
1519 let linked_ids: Vec<GlobalStreamId> = self
1520 .context
1521 .backend_streams
1522 .get(&token)
1523 .map_or_else(Vec::new, |ids| ids.to_owned());
1524 for stream_id in linked_ids {
1525 // This stream is linked to the backend that timedout
1526 if self.context.streams[stream_id].back.is_terminated()
1527 || self.context.streams[stream_id].back.is_error()
1528 {
1529 trace!(
1530 "{} Stream terminated or in error, do nothing, just wait a bit more",
1531 log_module_context!()
1532 );
1533 // Nothing to do, simply wait for the remaining bytes to be proxied
1534 if !self.context.streams[stream_id].back.is_completed() {
1535 should_close = false;
1536 }
1537 } else if !self.context.streams[stream_id].back.consumed {
1538 // The response has not started yet
1539 trace!(
1540 "{} Stream still waiting for response, send 504",
1541 log_module_context!()
1542 );
1543 self.context.unlink_stream(stream_id);
1544 let answers = answers_rc.borrow();
1545 let stream = &mut self.context.streams[stream_id];
1546 stream.context.access_log_message = Some("backend_timeout");
1547 set_default_answer(stream, front_readiness, 504, &answers);
1548 should_write = true;
1549 } else {
1550 trace!(
1551 "{} Stream waiting for end of response, forcefully terminate it",
1552 log_module_context!()
1553 );
1554 self.context.unlink_stream(stream_id);
1555 let stream = &mut self.context.streams[stream_id];
1556 stream.context.access_log_message = Some("backend_response_timeout");
1557 forcefully_terminate_answer(stream, front_readiness, H2Error::InternalError);
1558 should_write = true;
1559 }
1560 backend.end_stream(stream_id, &mut self.context);
1561 }
1562 // Re-arm the backend timeout if the session stays alive (draining streams).
1563 // Without this, the timeout is consumed and the session becomes immortal
1564 // until the zombie checker runs.
1565 if !should_close {
1566 backend.timeout_container().set(token);
1567 }
1568 } else {
1569 // Session received a timeout for an unknown token, ignore it
1570 return StateResult::Continue;
1571 }
1572 if should_write {
1573 // Drain as much pending data as possible before closing.
1574 // A single writable() call is insufficient for large responses —
1575 // the TLS buffer may need multiple flushes. Without this loop,
1576 // the session is killed with unflushed TLS data, causing the
1577 // client to receive a truncated TLS record ("decode error").
1578 //
1579 // The constant 16 is empirical: it papers over a missing
1580 // invariant-15 hop in the H2 mux state machine where the
1581 // writable readiness signal is not always re-armed after a
1582 // partial flush. Long-term plan: reach invariant-15 closure
1583 // and remove this loop. See `lib/src/protocol/mux/LIFECYCLE.md`.
1584 let mut result = StateResult::Continue;
1585 for _ in 0..16 {
1586 result = match self
1587 .frontend
1588 .writable(&mut self.context, EndpointClient(&mut self.router))
1589 {
1590 MuxResult::Continue => StateResult::Continue,
1591 MuxResult::Upgrade => StateResult::Upgrade,
1592 MuxResult::CloseSession => StateResult::CloseSession,
1593 };
1594 if result != StateResult::Continue
1595 || !self.frontend.readiness_mut().interest.is_writable()
1596 {
1597 break;
1598 }
1599 }
1600 // Re-arm the frontend timeout so the session doesn't become immortal.
1601 // The writable call may have partially flushed the response — we need
1602 // the timeout to fire again if the flush stalls.
1603 if result == StateResult::Continue {
1604 self.frontend.timeout_container().set(self.frontend_token);
1605 }
1606 return result;
1607 }
1608 if should_close {
1609 if self.delay_close_for_frontend_flush("timeout") {
1610 debug!(
1611 "{} Mux timeout delaying close for frontend flush: token={:?}, frontend={:?}",
1612 log_context!(self),
1613 token,
1614 self.frontend
1615 );
1616 self.frontend.timeout_container().set(self.frontend_token);
1617 return StateResult::Continue;
1618 }
1619 if front_is_h2 {
1620 debug!(
1621 "{} Mux timeout returning CloseSession: token={:?}, frontend={:?}",
1622 log_context!(self),
1623 token,
1624 self.frontend
1625 );
1626 for (idx, stream) in self.context.streams.iter().enumerate() {
1627 if stream.state != StreamState::Recycle {
1628 debug!(
1629 "{} timeout stream[{}]: state={:?}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
1630 log_context!(self),
1631 idx,
1632 stream.state,
1633 stream.front.parsing_phase,
1634 stream.back.parsing_phase,
1635 stream.front.is_completed(),
1636 stream.back.is_completed()
1637 );
1638 }
1639 }
1640 }
1641 StateResult::CloseSession
1642 } else {
1643 // Re-arm the frontend timeout. Without this, the timeout is consumed
1644 // by triggered() and the session stays alive indefinitely until the
1645 // zombie checker runs (default: 30 minutes).
1646 self.frontend.timeout_container().set(self.frontend_token);
1647 StateResult::Continue
1648 }
1649 }
1650
1651 fn cancel_timeouts(&mut self) {
1652 trace!("{} MuxState::cancel_timeouts", log_context!(self));
1653 self.frontend.timeout_container().cancel();
1654 for backend in self.router.backends.values_mut() {
1655 backend.timeout_container().cancel();
1656 }
1657 }
1658
1659 fn print_state(&self, _context: &str) {
1660 // The trait-required `context: &str` parameter (protocol tag like
1661 // "HTTPS"/"HTTP" passed by callers) predates the unified
1662 // `log_context!(self)` envelope. The canonical `MUX` tag lives
1663 // inside `log_context!`, so we ignore the parameter here and emit
1664 // the bracketed Session(...) block instead, mirroring the second
1665 // `error!` in this function.
1666 error!(
1667 "\
1668{} Session(Mux)
1669\tFrontend:
1670\t\ttoken: {:?}\treadiness: {:?}
1671\tBackend(s):",
1672 log_context!(self),
1673 self.frontend_token,
1674 self.frontend.readiness()
1675 );
1676 for (backend_token, backend) in &self.router.backends {
1677 error!(
1678 "{} \t\ttoken: {:?}\treadiness: {:?}",
1679 log_context!(self),
1680 backend_token,
1681 backend.readiness()
1682 )
1683 }
1684 }
1685
1686 fn close(&mut self, proxy: Rc<RefCell<dyn L7Proxy>>, _metrics: &mut SessionMetrics) {
1687 if self.context.debug.is_interesting() {
1688 warn!("{} {:?}", log_context!(self), self.context.debug.events);
1689 }
1690 debug!("{} MUX CLOSE", log_context!(self));
1691 trace!("{} FRONTEND: {:#?}", log_context!(self), self.frontend);
1692 trace!(
1693 "{} BACKENDS: {:#?}",
1694 log_context!(self),
1695 self.router.backends
1696 );
1697
1698 // Log active streams at session teardown for timeout diagnosis
1699 let active_count = self
1700 .context
1701 .streams
1702 .iter()
1703 .filter(|s| s.state.is_open() && s.metrics.start.is_some())
1704 .count();
1705 if active_count > 0 {
1706 debug!(
1707 "{} Session close with {} active stream(s)",
1708 log_context!(self),
1709 active_count
1710 );
1711 for (idx, stream) in self
1712 .context
1713 .streams
1714 .iter()
1715 .enumerate()
1716 .filter(|(_, s)| s.state.is_open() && s.metrics.start.is_some())
1717 {
1718 let elapsed = stream.metrics.service_time();
1719 debug!(
1720 "{} active stream[{}]: state={:?} service_time={:?} method={:?} path={:?} status={:?}",
1721 log_context!(self),
1722 idx,
1723 stream.state,
1724 elapsed,
1725 stream.context.method,
1726 stream.context.path,
1727 stream.context.status,
1728 );
1729 }
1730 incr!(names::h2::CLOSE_WITH_ACTIVE_STREAMS);
1731 }
1732
1733 // Distribute H2 connection-level overhead (control frames) across in-flight
1734 // streams so that access log bytes_in/bytes_out reflect actual wire cost.
1735 // Integer division may lose up to (active_count - 1) bytes, which is acceptable.
1736 let active_count = active_count.max(1);
1737 let (total_overhead_in, total_overhead_out) = self.frontend.overhead_bytes();
1738 let share_in = total_overhead_in / active_count;
1739 let share_out = total_overhead_out / active_count;
1740
1741 // Generate access logs for in-flight streams on session teardown.
1742 // Skip streams that already had their access log emitted (metrics.start is
1743 // set to None by metrics.reset() after generate_access_log in the happy path).
1744 // Frontend RTT is the same for every stream on this session — snapshot
1745 // it once outside the loop instead of paying one TCP_INFO syscall per
1746 // open stream.
1747 let client_rtt = socket_rtt(self.frontend.socket());
1748 for stream in &mut self.context.streams {
1749 if stream.state.is_open() && stream.metrics.start.is_some() {
1750 stream.metrics.bin += share_in;
1751 stream.metrics.bout += share_out;
1752 stream.metrics.service_stop();
1753 if stream.metrics.backend_stop.is_none() {
1754 stream.metrics.backend_stop();
1755 }
1756 // Only mark as error if the stream had an actual protocol/processing failure
1757 // (kawa parse error, backend error). Normal timeouts, client disconnects,
1758 // and graceful connection closures are not errors.
1759 let is_error = stream.front.is_error() || stream.back.is_error();
1760 let server_rtt = stream.linked_token().and_then(|token| {
1761 self.router
1762 .backends
1763 .get(&token)
1764 .and_then(|c| socket_rtt(c.socket()))
1765 });
1766 stream.generate_access_log(
1767 is_error,
1768 Some("session close"),
1769 self.context.listener.clone(),
1770 client_rtt,
1771 server_rtt,
1772 );
1773 stream.state = StreamState::Recycle;
1774 }
1775 }
1776
1777 self.frontend
1778 .close(&mut self.context, EndpointClient(&mut self.router));
1779
1780 for (token, client) in &mut self.router.backends {
1781 let proxy_borrow = proxy.borrow();
1782 client.timeout_container().cancel();
1783 let socket = client.socket_mut();
1784 if let Err(e) = proxy_borrow.deregister_socket(socket) {
1785 error!(
1786 "{} error deregistering back socket({:?}): {:?}",
1787 log_context_lite!(self),
1788 socket,
1789 e
1790 );
1791 }
1792 // invariant: write-only shutdown — Shutdown::Both on a TLS frontend
1793 // discards the receive buffer and elicits TCP RST, truncating the
1794 // already-queued response. Canonical write-up: `lib/src/https.rs:650-655`.
1795 // Backend sockets follow the same discipline for symmetry.
1796 if let Err(e) = socket.shutdown(Shutdown::Write)
1797 && e.kind() != ErrorKind::NotConnected
1798 {
1799 error!(
1800 "{} error shutting down back socket({:?}): {:?}",
1801 log_context_lite!(self),
1802 socket,
1803 e
1804 );
1805 }
1806 if !proxy_borrow.remove_session(*token) {
1807 error!(
1808 "{} session {:?} was already removed!",
1809 log_context_lite!(self),
1810 token
1811 );
1812 }
1813
1814 match client.position() {
1815 Position::Client(cluster_id, backend, _) => {
1816 let mut backend_borrow = backend.borrow_mut();
1817 backend_borrow.dec_connections();
1818 gauge_add!(names::backend::CONNECTIONS, -1);
1819 // Second `-1` site for `backend.pool.size` (the first is
1820 // in `connection.rs::pre_close_client_bookkeeping`). This
1821 // path runs during session teardown when the frontend
1822 // session iterates the backends map directly without
1823 // routing through `Connection::close`. Both `-1` sites
1824 // mirror the single `+1` in router.rs::connect and the
1825 // matching `backend.connections, -1` calls already
1826 // present here, so symmetry follows from
1827 // `backend.connections` correctness.
1828 gauge_add!(names::backend::POOL_SIZE, -1);
1829 gauge_add!(
1830 names::backend::CONNECTIONS_PER_BACKEND,
1831 -1,
1832 Some(cluster_id),
1833 Some(&backend_borrow.backend_id)
1834 );
1835 let count = self
1836 .context
1837 .backend_streams
1838 .get(token)
1839 .map_or(0, |ids| ids.len());
1840 backend_borrow.active_requests =
1841 backend_borrow.active_requests.saturating_sub(count);
1842 trace!(
1843 "{} connection (session) closed: {:#?}",
1844 log_context_lite!(self),
1845 backend_borrow
1846 );
1847 }
1848 Position::Server => {
1849 error!(
1850 "{} close_backend called on Server position",
1851 log_context_lite!(self)
1852 );
1853 }
1854 }
1855 }
1856 // Clear the reverse index after all backends have decremented their
1857 // active_requests counters (which depend on the index for stream counts).
1858 self.context.backend_streams.clear();
1859 }
1860
1861 fn shutting_down(&mut self) -> SessionIsToBeClosed {
1862 // RFC 9113 §6.8: initiate graceful shutdown with double-GOAWAY pattern.
1863 // Only send the initial GOAWAY once. The final GOAWAY (with the real
1864 // last_stream_id) is handled by finalize_write() when all streams drain.
1865 // Calling graceful_goaway() again would send the final GOAWAY
1866 // prematurely and force-disconnect before in-flight streams complete.
1867 if !self.frontend.is_draining() {
1868 match self.frontend.graceful_goaway() {
1869 MuxResult::CloseSession => return true,
1870 MuxResult::Continue => {
1871 // graceful_goaway() queued a GOAWAY frame. Flush it directly
1872 // since the event loop uses edge-triggered epoll and won't
1873 // deliver a new WRITABLE event for an already-writable socket.
1874 self.frontend.flush_zero_buffer();
1875 }
1876 _ => {}
1877 }
1878 } else {
1879 trace!(
1880 "{} shutting_down: already draining, skipping duplicate GOAWAY",
1881 log_context!(self)
1882 );
1883 // shut_down_sessions() runs outside ready(), so retry flushing any
1884 // previously-buffered GOAWAY/TLS records on each pass.
1885 self.frontend.flush_zero_buffer();
1886 }
1887 if self.drive_frontend_shutdown_io() {
1888 return true;
1889 }
1890 // Forced-close deadline: once the H2 listener's
1891 // `h2_graceful_shutdown_deadline_seconds` budget has elapsed from
1892 // the moment `graceful_goaway` armed `drain.started_at`, stop
1893 // waiting for streams and tear the session down. `drive_frontend_
1894 // shutdown_io` above already had a chance to flush any pending
1895 // TLS/GOAWAY records; this branch accepts that some bytes may be
1896 // lost in exchange for honoring the operator-configured SLA.
1897 // Listeners that disable the knob (`= 0` → `None`) short-circuit
1898 // the check inside `graceful_shutdown_deadline_elapsed`.
1899 if self.frontend.graceful_shutdown_deadline_elapsed() {
1900 debug!(
1901 "{} Mux shutting_down: graceful-shutdown deadline elapsed, forcing close",
1902 log_context!(self)
1903 );
1904 return true;
1905 }
1906 if matches!(self.frontend, Connection::H2(_)) && self.frontend.is_draining() {
1907 for stream in &mut self.context.streams {
1908 if stream.front_received_end_of_stream {
1909 continue;
1910 }
1911 if !matches!(stream.state, StreamState::Linked(_) | StreamState::Unlinked) {
1912 continue;
1913 }
1914 if stream.front.consumed
1915 && stream.front.storage.is_empty()
1916 && stream.front.is_completed()
1917 {
1918 stream.front_received_end_of_stream = true;
1919 self.frontend
1920 .readiness_mut()
1921 .interest
1922 .insert(Ready::WRITABLE);
1923 self.frontend.readiness_mut().signal_pending_write();
1924 }
1925 }
1926 }
1927 let mut can_stop = true;
1928 for stream in &mut self.context.streams {
1929 match stream.state {
1930 StreamState::Linked(_) => {
1931 can_stop = false;
1932 }
1933 StreamState::Unlinked => {
1934 kawa::debug_kawa(&stream.front);
1935 kawa::debug_kawa(&stream.back);
1936 if stream.is_quiesced() {
1937 continue;
1938 }
1939 stream.context.closing = true;
1940 can_stop = false;
1941 }
1942 _ => {}
1943 }
1944 }
1945 if self.frontend.has_pending_write() {
1946 return false;
1947 }
1948 if can_stop {
1949 let active_h2_streams = self
1950 .context
1951 .streams
1952 .iter()
1953 .enumerate()
1954 .filter(|(_, s)| {
1955 if s.state == StreamState::Recycle {
1956 return false;
1957 }
1958 if s.state == StreamState::Unlinked && s.is_quiesced() {
1959 return false;
1960 }
1961 true
1962 })
1963 .collect::<Vec<_>>();
1964 if matches!(self.frontend, Connection::H2(_)) && !active_h2_streams.is_empty() {
1965 debug!(
1966 "{} Mux shutting_down returning true with active H2 streams: {:?}",
1967 log_context!(self),
1968 self.frontend
1969 );
1970 for (idx, stream) in active_h2_streams {
1971 debug!(
1972 "{} shutdown stream[{}]: state={:?}, front_phase={:?}, back_phase={:?}, front_completed={}, back_completed={}",
1973 log_context!(self),
1974 idx,
1975 stream.state,
1976 stream.front.parsing_phase,
1977 stream.back.parsing_phase,
1978 stream.front.is_completed(),
1979 stream.back.is_completed()
1980 );
1981 }
1982 }
1983 }
1984 if can_stop {
1985 return true;
1986 }
1987
1988 false
1989 }
1990}
1991
1992#[cfg(test)]
1993mod tests {
1994 use super::*;
1995
1996 #[test]
1997 fn update_readiness_after_read_closed_keeps_writable() {
1998 let mut readiness = Readiness {
1999 event: Ready::READABLE | Ready::WRITABLE | Ready::HUP,
2000 interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP,
2001 };
2002
2003 let should_yield = update_readiness_after_read(17, SocketResult::Closed, &mut readiness);
2004
2005 assert!(!should_yield);
2006 assert!(!readiness.event.is_readable());
2007 assert!(readiness.event.is_writable());
2008 assert!(readiness.event.is_hup());
2009 }
2010}