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