sozu_lib/protocol/mux/connection.rs
1//! Protocol-agnostic frontend/backend connection wrapper.
2//!
3//! [`Connection`] is the H1/H2 dispatch enum used everywhere in the mux
4//! layer. Most of the methods are trivial pass-through forwarders to the
5//! underlying [`ConnectionH1`] or [`h2::ConnectionH2`] implementation — the
6//! local `forward!` macro removes the boilerplate.
7//!
8//! The two `Endpoint` adaptors (`EndpointServer`, `EndpointClient`) are
9//! also defined here: they let a connection call back into either the
10//! frontend connection or the backend [`Router`] map without knowing which
11//! direction it faces.
12//!
13//! Edge-trigger discipline lives in `mux/h2.rs` (`writable`) — the canonical
14//! home for the `signal_pending_write` / `arm_writable` invariant. This
15//! module's abstractions delegate to that discipline through the
16//! protocol-specific writers.
17
18use std::{
19 cell::Cell,
20 fmt::Debug,
21 net::SocketAddr,
22 time::{Duration, Instant},
23};
24
25use mio::{Token, net::TcpStream};
26use rusty_ulid::Ulid;
27use sozu_command::{logging::ansi_palette, ready::Ready, state::ClusterId};
28
29use super::{
30 BackendChange, BackendId, BackendStatus, ConnectionH1, Context, Endpoint, GlobalStreamId,
31 MuxResult, Position, Router,
32 h2::{self, H2Shell, H2StreamId},
33 h2_flood_detector, memoized_rtt,
34};
35use crate::metrics::names;
36use crate::{L7ListenerHandler, ListenerHandler, Readiness, socket::SocketHandler};
37
38/// Module-level prefix used on every log line emitted from this module.
39/// Produces a bold bright-white `MUX-CONN` label (uniform across every
40/// protocol) when the logger is in colored mode. Session-specific context
41/// cannot be derived here because most log sites are inside `Endpoint` adapter
42/// methods that only see the backend/frontend maps, not the wrapping
43/// [`Connection`].
44macro_rules! log_module_context {
45 () => {{
46 let (open, reset, _, _, _) = ansi_palette();
47 format!("{open}MUX-CONN{reset}\t >>>", open = open, reset = reset)
48 }};
49}
50
51#[derive(Debug)]
52#[allow(clippy::large_enum_variant)]
53pub enum Connection<Front: SocketHandler> {
54 H1(ConnectionH1<Front>),
55 H2(H2Shell<Front>),
56}
57
58// Dispatches a method call or field access to the inner H1/H2 connection.
59// Used by trivial pass-through methods on Connection<Front> to avoid
60// repeating the two-arm match.
61macro_rules! forward {
62 ($self:expr, $method:ident ( $($args:tt)* )) => {
63 match $self {
64 Connection::H1(c) => c.$method($($args)*),
65 Connection::H2(c) => c.$method($($args)*),
66 }
67 };
68 // The H2 arm reaches one level further in. `Connection::H2` holds an
69 // `H2Shell`, whose state machine is `H2Shell::core`; `ConnectionH1` still
70 // carries its own fields directly. The asymmetry is the split, not an
71 // oversight, and it is confined to these two arms — every METHOD forwarded
72 // above is one `H2Shell` answers itself.
73 (&$self:expr, $field:ident) => {
74 match $self {
75 Connection::H1(c) => &c.$field,
76 Connection::H2(c) => &c.core.$field,
77 }
78 };
79 (&mut $self:expr, $field:ident) => {
80 match $self {
81 Connection::H1(c) => &mut c.$field,
82 Connection::H2(c) => &mut c.core.$field,
83 }
84 };
85}
86
87impl<Front: SocketHandler> Connection<Front> {
88 pub fn new_h1_server(
89 session_ulid: Ulid,
90 front_stream: Front,
91 timeout_duration: Duration,
92 ) -> Connection<Front> {
93 // The one read of `peer_addr()` on this connection's whole lifetime,
94 // taken before `front_stream` is moved into `socket`. `ConnectionH1::
95 // peer_address` documents why the answer is snapshotted, not re-read.
96 let peer_address = front_stream.peer_addr();
97 Connection::H1(ConnectionH1 {
98 socket: front_stream,
99 peer_address,
100 position: Position::Server,
101 readiness: Readiness {
102 interest: Ready::READABLE | Ready::HUP | Ready::ERROR,
103 event: Ready::EMPTY,
104 },
105 requests: 0,
106 stream: Some(0),
107 timeout_duration,
108 timeout_deadline: Instant::now().checked_add(timeout_duration),
109 parked_on_buffer_pressure: false,
110 write_failed: false,
111 close_notify_sent: false,
112 session_ulid,
113 reused_from_pool: false,
114 io_slices: Vec::new(),
115 rtt: Cell::new(None),
116 // A frontend is built with the listener's request timeout.
117 linger_timeout: timeout_duration,
118 linger: super::h1::Linger::Off,
119 })
120 }
121 pub fn new_h1_client(
122 session_ulid: Ulid,
123 front_stream: Front,
124 cluster_id: ClusterId,
125 backend: BackendId,
126 timeout_duration: Duration,
127 ) -> Connection<Front> {
128 // Same single read as the server constructor. On the dial path the
129 // handler already carries the cluster-configured backend address, so
130 // this answers it even while the async `connect()` is still in flight
131 // and `getpeername(2)` would refuse.
132 let peer_address = front_stream.peer_addr();
133 Connection::H1(ConnectionH1 {
134 socket: front_stream,
135 peer_address,
136 position: Position::Client(
137 cluster_id,
138 backend,
139 BackendStatus::Connecting(Instant::now()),
140 ),
141 readiness: Readiness {
142 interest: Ready::WRITABLE | Ready::READABLE | Ready::HUP | Ready::ERROR,
143 event: Ready::EMPTY,
144 },
145 stream: None,
146 requests: 0,
147 timeout_duration,
148 timeout_deadline: Instant::now().checked_add(timeout_duration),
149 parked_on_buffer_pressure: false,
150 write_failed: false,
151 close_notify_sent: false,
152 session_ulid,
153 reused_from_pool: false,
154 io_slices: Vec::new(),
155 rtt: Cell::new(None),
156 linger_timeout: Duration::ZERO,
157 linger: super::h1::Linger::Off,
158 })
159 }
160
161 #[allow(clippy::too_many_arguments)]
162 pub fn new_h2_server(
163 session_ulid: Ulid,
164 front_stream: Front,
165 buffers: &mut dyn super::buffer_source::BufferSource,
166 timeout_duration: Duration,
167 flood_config: h2_flood_detector::H2FloodConfig,
168 connection_config: h2::H2ConnectionConfig,
169 stream_idle_timeout: std::time::Duration,
170 graceful_shutdown_deadline: Option<std::time::Duration>,
171 ) -> Option<Connection<Front>> {
172 Some(Connection::H2(H2Shell::new(
173 session_ulid,
174 front_stream,
175 Position::Server,
176 buffers,
177 flood_config,
178 connection_config,
179 stream_idle_timeout,
180 graceful_shutdown_deadline,
181 timeout_duration,
182 Some((H2StreamId::Zero, h2::CLIENT_PREFACE_SIZE)),
183 Ready::READABLE | Ready::HUP | Ready::ERROR,
184 )?))
185 }
186
187 #[allow(clippy::too_many_arguments)]
188 pub fn new_h2_client(
189 session_ulid: Ulid,
190 front_stream: Front,
191 cluster_id: ClusterId,
192 backend: BackendId,
193 buffers: &mut dyn super::buffer_source::BufferSource,
194 timeout_duration: Duration,
195 flood_config: h2_flood_detector::H2FloodConfig,
196 connection_config: h2::H2ConnectionConfig,
197 stream_idle_timeout: std::time::Duration,
198 graceful_shutdown_deadline: Option<std::time::Duration>,
199 ) -> Option<Connection<Front>> {
200 // Test-only injection point: when set via
201 // [`__test_force_h2_client_failure`], pretend the pool was exhausted
202 // and return `None`. This mirrors the buffer-pool-exhaustion branch
203 // inside [`ConnectionH2::new`] deterministically so E2E tests can
204 // exercise `Mux::attach_dialed`'s rollback path (FIX-18) without having
205 // to starve the pool in-process.
206 #[cfg(any(test, feature = "e2e-hooks"))]
207 if test_hooks::FORCE_NEW_H2_CLIENT_FAILURE.swap(false, std::sync::atomic::Ordering::SeqCst)
208 {
209 return None;
210 }
211 Some(Connection::H2(H2Shell::new(
212 session_ulid,
213 front_stream,
214 Position::Client(
215 cluster_id,
216 backend,
217 BackendStatus::Connecting(Instant::now()),
218 ),
219 buffers,
220 flood_config,
221 connection_config,
222 stream_idle_timeout,
223 graceful_shutdown_deadline,
224 timeout_duration,
225 None,
226 Ready::WRITABLE | Ready::HUP | Ready::ERROR,
227 )?))
228 }
229
230 pub fn readiness(&self) -> &Readiness {
231 forward!(&self, readiness)
232 }
233 pub fn readiness_mut(&mut self) -> &mut Readiness {
234 forward!(&mut self, readiness)
235 }
236 pub fn position(&self) -> &Position {
237 forward!(&self, position)
238 }
239 /// Whether the peer has closed its side of this connection: its readiness
240 /// carries `Ready::HUP`, recorded by epoll (`EPOLLRDHUP`/`EPOLLHUP`, see
241 /// `Ready::from(&mio::event::Event)`) or, on an H1 backend, by a read that
242 /// met the EOF or a reset (`ConnectionH1::readable`).
243 ///
244 /// A backend that sozu itself is dropping does not count:
245 /// `force_disconnect` writes HUP into the readiness of a live backend
246 /// and marks it `BackendStatus::Disconnecting`, so that state is excluded
247 /// and its peer keeps its `shutdown(SHUT_WR)` (see `mux::shutdown_write`).
248 pub fn peer_closed(&self) -> bool {
249 self.readiness().event.is_hup()
250 && !matches!(
251 self.position(),
252 Position::Client(_, _, BackendStatus::Disconnecting)
253 )
254 }
255 pub fn position_mut(&mut self) -> &mut Position {
256 forward!(&mut self, position)
257 }
258 pub fn socket(&self) -> &TcpStream {
259 match self {
260 Connection::H1(c) => c.socket.socket_ref(),
261 Connection::H2(c) => c.socket.socket_ref(),
262 }
263 }
264 /// Smoothed RTT of this connection's own socket, sampled once for the
265 /// connection's whole life by [`memoized_rtt`] and reused by every
266 /// access log that reports it: `client_rtt` for a frontend, `server_rtt`
267 /// for a backend.
268 pub(super) fn rtt(&self) -> Option<Duration> {
269 match self {
270 Connection::H1(c) => memoized_rtt(&c.rtt, c.socket.socket_ref()),
271 Connection::H2(c) => memoized_rtt(&c.local_rtt, c.socket.socket_ref()),
272 }
273 }
274 pub fn socket_mut(&mut self) -> &mut TcpStream {
275 match self {
276 Connection::H1(c) => c.socket.socket_mut(),
277 Connection::H2(c) => c.socket.socket_mut(),
278 }
279 }
280 /// The socket handler itself, for what only it knows how to record:
281 /// `FrontRustls::readiness_delivered` (`lib/src/https.rs`).
282 pub fn socket_handler_mut(&mut self) -> &mut Front {
283 match self {
284 Connection::H1(c) => &mut c.socket,
285 Connection::H2(c) => &mut c.socket,
286 }
287 }
288 /// The peer address this connection snapshotted at construction —
289 /// `ConnectionH1::peer_address` / `ConnectionH2::peer_address`, the single
290 /// [`SocketHandler::peer_addr`] read taken on its whole lifetime.
291 ///
292 /// The one accessor `Mux`'s own log macros were missing. Without it the
293 /// only route from a [`Connection`] to a peer address was [`socket`], which
294 /// hands back a concrete `&mio::net::TcpStream` whose *inherent*
295 /// `peer_addr` wins method resolution over the trait — a live
296 /// `getpeername(2)` that answers `ENOTCONN` once the peer resets and
297 /// reports the transport source rather than a PROXY-advertised client. So
298 /// a `MUX` line disagreed with the `MUX-H1` / `MUX-H2` / `SOCKET` / `HTTPS`
299 /// lines of the very same session.
300 ///
301 /// Reads the field rather than calling the trait again, so rendering a
302 /// `MUX` line costs no socket access at all and cannot drift from what the
303 /// per-protocol envelopes print for the same session.
304 ///
305 /// This is the sole reader of `ConnectionH2::peer_address` outside
306 /// `lib/src/protocol/mux/h2.rs`, and the only reason that field is
307 /// `pub(super)` rather than private — the visibility
308 /// `ConnectionH1::peer_address` already carried.
309 ///
310 /// [`socket`]: Connection::socket
311 pub fn peer_address(&self) -> Option<SocketAddr> {
312 *forward!(&self, peer_address)
313 }
314 /// The next instant this connection wants `timeout()` called at, or
315 /// `None` for "no timer".
316 ///
317 /// This replaced `timeout_container()`, which handed the caller the wheel
318 /// handle itself. The cores no longer own one: they publish a deadline and
319 /// the `Mux` adapter owns every `TimeoutContainer`, so there is exactly one
320 /// place that talks to `crate::timer` and exactly one place that can get
321 /// the arm / re-arm discipline wrong. See `LIFECYCLE.md` §7.7.
322 pub fn poll_timeout(&self) -> Option<Instant> {
323 match self {
324 Connection::H1(c) => c.poll_timeout(),
325 Connection::H2(c) => c.core.poll_timeout(),
326 }
327 }
328
329 /// The configured idle timeout, carried so the adapter's container keeps a
330 /// current `duration()` for whoever inherits it (the WebSocket upgrade
331 /// hands it to `Pipe`, which re-arms from it).
332 pub fn timeout_duration(&self) -> Duration {
333 match self {
334 Connection::H1(c) => c.timeout_duration,
335 Connection::H2(c) => c.core.timeout_duration,
336 }
337 }
338
339 /// Push the deadline one full duration out from `now`. Replaces
340 /// `timeout_container().set(token)` / `.reset()` at the adapter's sites.
341 pub fn arm_timeout(&mut self, now: Instant) {
342 match self {
343 Connection::H1(c) => c.arm_timeout(now),
344 // `ConnectionH2::arm_timeout` reads the connection's own snapshot,
345 // which the adapter has not necessarily mirrored yet at the sites
346 // that call this; re-arm against the caller's `now` instead.
347 Connection::H2(c) => {
348 let duration = c.core.timeout_duration;
349 c.core.set_timeout_duration(duration, now)
350 }
351 }
352 }
353
354 /// Ask for no timer at all. Replaces `timeout_container().cancel()`.
355 pub fn clear_timeout(&mut self) {
356 match self {
357 Connection::H1(c) => c.clear_timeout(),
358 Connection::H2(c) => c.core.clear_timeout(),
359 }
360 }
361
362 /// Adopt a new configured duration and re-arm from `now`. Replaces
363 /// `timeout_container().set_duration(d)`.
364 pub fn set_timeout_duration(&mut self, duration: Duration, now: Instant) {
365 match self {
366 Connection::H1(c) => c.set_timeout_duration(duration, now),
367 Connection::H2(c) => c.core.set_timeout_duration(duration, now),
368 }
369 }
370
371 /// Returns connection-level byte overhead (bin, bout) for H2, (0, 0) for H1.
372 pub fn overhead_bytes(&self) -> (usize, usize) {
373 match self {
374 Connection::H1(_) => (0, 0),
375 Connection::H2(c) => (c.core.bytes.overhead_bin, c.core.bytes.overhead_bout),
376 }
377 }
378
379 pub(super) fn readable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
380 where
381 E: Endpoint,
382 L: ListenerHandler + L7ListenerHandler,
383 {
384 forward!(self, readable(context, endpoint))
385 }
386 pub(super) fn writable<E, L>(&mut self, context: &mut Context<L>, endpoint: E) -> MuxResult
387 where
388 E: Endpoint,
389 L: ListenerHandler + L7ListenerHandler,
390 {
391 forward!(self, writable(context, endpoint))
392 }
393
394 /// Returns true if this connection could not read because its stream's
395 /// kawa buffer was full. Used to prevent the dead-backend check from
396 /// closing a backend that still has data in the OS socket buffer.
397 pub(super) fn has_buffer_pressure<L>(&self, context: &Context<L>) -> bool
398 where
399 L: ListenerHandler + L7ListenerHandler,
400 {
401 match self {
402 Connection::H1(c) => {
403 let Some(stream_id) = c.stream else {
404 // No stream assigned — no buffer pressure.
405 return false;
406 };
407 let kawa = match c.position {
408 Position::Client(..) => &context.streams[stream_id].back,
409 Position::Server => &context.streams[stream_id].front,
410 };
411 kawa.storage.available_space() == 0
412 }
413 // H2 connections manage their own flow control via expect_read
414 Connection::H2(_) => false,
415 }
416 }
417
418 /// Whether this backend connection is work for the inner loop of
419 /// `Mux::ready_inner`: an event it asked for that a `readable` or
420 /// `writable` pass can act on, or a HUP or ERROR the dead-backend check
421 /// has yet to act on. That check runs at the top of each iteration, so a
422 /// HUP sozu raises itself during the iteration (`force_disconnect`, after
423 /// a protocol error) must bring the loop back to close the connection:
424 /// no socket event will.
425 ///
426 /// A dead backend the check keeps for the bytes it has left to deliver
427 /// ([`Self::has_buffer_pressure`]) is not work: it waits for the client
428 /// to drain the stream buffer, which [`Self::try_resume_reading`]
429 /// signals with READABLE. `HUP` and `ERROR` are always in a backend's
430 /// interest, and counting them there spun the loop to
431 /// `MAX_LOOP_ITERATIONS` with nothing to progress (sozu-proxy/sozu#1819).
432 pub(super) fn has_loop_work<L>(&self, context: &Context<L>) -> bool
433 where
434 L: ListenerHandler + L7ListenerHandler,
435 {
436 let readiness = self.readiness();
437 let work = readiness.filter_interest();
438 work.is_readable()
439 || work.is_writable()
440 || ((readiness.event.is_hup() || readiness.event.is_error())
441 && !self.has_buffer_pressure(context))
442 }
443
444 /// Re-enable READABLE if this connection is parked waiting for buffer space
445 /// and the target stream's buffer now has enough room.
446 ///
447 /// For H1: checks the `parked_on_buffer_pressure` flag set when `readable`
448 /// exits early because the kawa buffer was full. Edge-triggered epoll will
449 /// not re-fire READABLE for data already in the kernel socket buffer, so
450 /// this is the only path that re-arms it after the peer drains space.
451 ///
452 /// For H2: checks the `expect_read` field tracking which stream and how
453 /// many bytes are needed.
454 pub(super) fn try_resume_reading<L>(&mut self, context: &Context<L>) -> bool
455 where
456 L: ListenerHandler + L7ListenerHandler,
457 {
458 match self {
459 Connection::H1(c) => {
460 if !c.parked_on_buffer_pressure {
461 return false;
462 }
463 let Some(stream_id) = c.stream else {
464 return false;
465 };
466 let kawa = match c.position {
467 Position::Client(..) => &context.streams[stream_id].back,
468 Position::Server => &context.streams[stream_id].front,
469 };
470 if kawa.storage.available_space() > 0 {
471 trace!(
472 "{} H1 try_resume_reading: re-arming READABLE",
473 log_module_context!()
474 );
475 // Pre: we only reach here because the connection parked on
476 // buffer pressure AND the kawa now has room. Pair the
477 // synthetic READABLE event with that drained-space fact:
478 // edge-triggered epoll won't re-fire on its own, so this is
479 // the sole re-arm path.
480 debug_assert!(
481 c.parked_on_buffer_pressure,
482 "re-arm only fires for a connection parked on buffer pressure"
483 );
484 c.readiness.signal_pending_read();
485 debug_assert!(
486 c.readiness.event.is_readable(),
487 "signal_pending_read must leave a READABLE event queued"
488 );
489 true
490 } else {
491 false
492 }
493 }
494 Connection::H2(c) => c.core.try_resume_reading(context),
495 }
496 }
497
498 /// The backend this connection was dialled to, when it is a pooled
499 /// backend connection carrying no stream: an H1 keep-alive, or a
500 /// connected H2 connection whose stream table is empty. `None` for a
501 /// frontend, a dial in progress, a connection being dropped, and any
502 /// connection that still carries a stream.
503 pub(super) fn idle_pooled_backend(&self) -> Option<&BackendId> {
504 match self {
505 Connection::H1(c) => match &c.position {
506 Position::Client(_, backend, BackendStatus::KeepAlive) if c.stream.is_none() => {
507 Some(backend)
508 }
509 _ => None,
510 },
511 Connection::H2(c) => match &c.core.position {
512 Position::Client(_, backend, BackendStatus::Connected)
513 if c.core.stream_count() == 0 =>
514 {
515 Some(backend)
516 }
517 _ => None,
518 },
519 }
520 }
521
522 /// Drop this connection: its status becomes `Disconnecting` and its
523 /// readiness carries `HUP`, which `Mux::ready_inner`'s dead-backend
524 /// sweep closes on its next pass.
525 pub(super) fn force_disconnect(&mut self) -> MuxResult {
526 forward!(self, force_disconnect())
527 }
528
529 /// `now` is the caller's clock snapshot; H2 arms the graceful-shutdown
530 /// budget from it. H1 has no multiplex to drain and ignores it.
531 pub(super) fn graceful_goaway(&mut self, now: Instant) -> MuxResult {
532 match self {
533 Connection::H1(_) => MuxResult::Continue,
534 Connection::H2(c) => c.graceful_goaway(now),
535 }
536 }
537
538 /// RFC 9113 §6.8 final GOAWAY ahead of a drain's forced close. H1 has
539 /// no GOAWAY and ignores it. See
540 /// [`h2::ConnectionH2::goaway_before_forced_close`].
541 pub(super) fn goaway_before_forced_close<L>(&mut self, context: &Context<L>) -> MuxResult
542 where
543 L: ListenerHandler + L7ListenerHandler,
544 {
545 match self {
546 Connection::H1(_) => MuxResult::Continue,
547 Connection::H2(c) => c.goaway_before_forced_close(context),
548 }
549 }
550
551 /// True while an H2 peer's HEADERS block is still being reassembled.
552 /// H1 has no such state. See
553 /// [`h2::ConnectionH2::peer_header_block_in_progress`].
554 pub(super) fn peer_header_block_in_progress(&self) -> bool {
555 match self {
556 Connection::H1(_) => false,
557 Connection::H2(c) => c.core.peer_header_block_in_progress(),
558 }
559 }
560
561 /// True while a frontend drains what its client still sends before it
562 /// closes: an H1 connection after a response that completed before its
563 /// request, an H2 connection after its final GOAWAY. See
564 /// [`super::shared::Linger`].
565 pub(super) fn is_lingering(&self) -> bool {
566 match self {
567 Connection::H1(c) => c.is_lingering(),
568 Connection::H2(c) => c.core.is_lingering(),
569 }
570 }
571
572 /// True once an H2 frontend has decided to linger after a final GOAWAY
573 /// that carried an error code: the linger is pending (its TLS
574 /// `close_notify` still to flush) or draining. Its streams get no answer,
575 /// so `Mux` releases them and their backends in the pass that decides
576 /// the linger instead of when it ends
577 /// (`Mux::release_streams_and_backends_for_error_linger`). Pending
578 /// counts: when the kernel refuses the alert's record, nothing re-arms
579 /// the write and that pass would otherwise reach the `pending_links`
580 /// pass of `Mux::ready_inner` with the streams still open.
581 pub(super) fn lingers_after_error_goaway(&self) -> bool {
582 match self {
583 Connection::H1(_) => false,
584 Connection::H2(c) => {
585 !matches!(c.core.linger, super::shared::Linger::Off) && !c.core.graceful_goaway
586 }
587 }
588 }
589
590 /// Whether the client may have sent bytes this connection has not read:
591 /// a read is due, or the read stopped on buffer pressure with the rest
592 /// still in the kernel. An H1 connection parks with its READABLE event
593 /// cleared (`ConnectionH1::readable`), an H2 one with its READABLE
594 /// interest cleared and the event kept (`ConnectionH2::try_resume_reading`).
595 pub(super) fn has_unread_input(&self) -> bool {
596 match self {
597 Connection::H1(c) => {
598 c.readiness.filter_interest().is_readable() || c.parked_on_buffer_pressure
599 }
600 Connection::H2(_) => self.readiness().event.is_readable(),
601 }
602 }
603
604 pub(super) fn is_draining(&self) -> bool {
605 match self {
606 Connection::H1(_) => false,
607 Connection::H2(c) => c.core.drain.draining(),
608 }
609 }
610
611 /// Proxy-side graceful-shutdown budget exhaustion check. Only H2
612 /// connections carry the timer — H1 has no multiplex to drain, so its
613 /// answer is always `false` and the H1 path continues to fall through
614 /// to the ordinary single-response close. See
615 /// [`h2::ConnectionH2::graceful_shutdown_deadline_elapsed`].
616 pub(super) fn graceful_shutdown_deadline_elapsed(&self) -> bool {
617 match self {
618 Connection::H1(_) => false,
619 Connection::H2(c) => c.core.graceful_shutdown_deadline_elapsed(),
620 }
621 }
622
623 /// Arm the H2 graceful-shutdown budget on a connection already draining
624 /// when the proxy soft-stops. H1 has no budget and ignores it. See
625 /// [`h2::ConnectionH2::arm_graceful_shutdown_deadline`].
626 pub(super) fn arm_graceful_shutdown_deadline(&mut self, now: Instant) {
627 match self {
628 Connection::H1(_) => {}
629 Connection::H2(c) => c.core.arm_graceful_shutdown_deadline(now),
630 }
631 }
632
633 pub(super) fn has_pending_write(&self) -> bool {
634 forward!(self, has_pending_write())
635 }
636
637 /// Connection-level [`Self::has_pending_write`] extended with a per-stream
638 /// back-buffer probe (LIFECYCLE §9 invariant 16), for H2 only. H1 falls
639 /// back to [`Self::has_pending_write`], which checks the socket alone
640 /// (TLS records not yet flushed) and NOT the bytes still queued in its
641 /// stream's response buffer; `Mux::wait_for_client_at_loop_limit` probes
642 /// those itself.
643 pub(super) fn has_pending_write_including_streams<L>(&self, context: &super::Context<L>) -> bool
644 where
645 L: ListenerHandler + L7ListenerHandler,
646 {
647 match self {
648 Connection::H1(c) => c.has_pending_write(),
649 Connection::H2(c) => c.has_pending_write_full(context),
650 }
651 }
652
653 pub(super) fn initiate_close_notify(&mut self) -> bool {
654 forward!(self, initiate_close_notify())
655 }
656
657 pub(super) fn flush_output_buffer(&mut self) {
658 if let Connection::H2(c) = self {
659 c.flush_output_buffer();
660 }
661 }
662
663 fn pre_close_client_bookkeeping<L>(&self, context: &mut Context<L>)
664 where
665 L: ListenerHandler + L7ListenerHandler,
666 {
667 if let Position::Client(cluster_id, backend, _) = self.position() {
668 // The close path releases exactly one slot. The embedder performs
669 // it and holds the "decreased by one, unless already at zero"
670 // relation — see `BackendRegistry::apply`.
671 context.record_backend_delta(backend, BackendChange::ConnectionClosed);
672 gauge_add!(names::backend::CONNECTIONS, -1);
673 // Pair with the `+1` at `router.rs::connect` (new-dial path).
674 // This is the graceful-close decrement, used both by the dead
675 // backend path in `mod.rs::back_readable` (which routes through
676 // `client.close()`) and by any explicit Connection::close.
677 gauge_add!(names::backend::POOL_SIZE, -1);
678 gauge_add!(
679 names::backend::CONNECTIONS_PER_BACKEND,
680 -1,
681 Some(cluster_id),
682 Some(&backend.backend_id),
683 backend.metric_incarnation()
684 );
685 trace!("{} connection close: {:?}", log_module_context!(), backend);
686 }
687 }
688
689 fn pre_end_stream_client_bookkeeping<L>(&self, context: &mut Context<L>)
690 where
691 L: ListenerHandler + L7ListenerHandler,
692 {
693 if let Position::Client(_, backend, BackendStatus::Connected) = self.position() {
694 // Pairs with the `StreamsStarted(1)` in
695 // `post_start_stream_client_bookkeeping`.
696 context.record_backend_delta(backend, BackendChange::StreamsEnded(1));
697 trace!(
698 "{} connection end stream: {:?}",
699 log_module_context!(),
700 backend
701 );
702 }
703 }
704
705 fn post_start_stream_client_bookkeeping<L>(&self, context: &mut Context<L>)
706 where
707 L: ListenerHandler + L7ListenerHandler,
708 {
709 if let Position::Client(_, backend, BackendStatus::Connected) = self.position() {
710 // Pairs with the `StreamsEnded(1)` in the end path.
711 context.record_backend_delta(backend, BackendChange::StreamsStarted(1));
712 trace!(
713 "{} connection start stream: {:?}",
714 log_module_context!(),
715 backend
716 );
717 }
718 }
719
720 pub(super) fn close<E, L>(&mut self, context: &mut Context<L>, endpoint: E)
721 where
722 E: Endpoint,
723 L: ListenerHandler + L7ListenerHandler,
724 {
725 self.pre_close_client_bookkeeping(context);
726 forward!(self, close(context, endpoint))
727 }
728
729 pub(super) fn end_stream<L>(&mut self, stream: GlobalStreamId, context: &mut Context<L>)
730 where
731 L: ListenerHandler + L7ListenerHandler,
732 {
733 self.pre_end_stream_client_bookkeeping(context);
734 forward!(self, end_stream(stream, context))
735 }
736
737 /// Release the `active_requests` charge [`Self::start_stream`] took, for
738 /// a caller that has to abandon a connection it already started a stream
739 /// on.
740 ///
741 /// Its one caller is `Mux::attach_dialed`'s `register_socket` rollback,
742 /// which drops a connection whose `start_stream` already succeeded. Same
743 /// guard as the charge, so the pair holds whatever status the connection
744 /// is in: a freshly-dialled one is `BackendStatus::Connecting` and was
745 /// never charged, so this correctly emits nothing for it.
746 pub(super) fn release_start_stream_charge<L>(&self, context: &mut Context<L>)
747 where
748 L: ListenerHandler + L7ListenerHandler,
749 {
750 self.pre_end_stream_client_bookkeeping(context);
751 }
752
753 pub(super) fn start_stream<L>(
754 &mut self,
755 stream: GlobalStreamId,
756 context: &mut Context<L>,
757 ) -> bool
758 where
759 L: ListenerHandler + L7ListenerHandler,
760 {
761 // The charge is emitted AFTER the start is known to have succeeded,
762 // never before with a rollback behind it. A refused `start_stream`
763 // must not leak an `active_requests` charge onto the backend — it
764 // would skew least-loaded balancing forever — and the way to hold
765 // that is for the refusal path to have no charge to undo rather than
766 // to undo one correctly. Ordering the emit this way is free: neither
767 // `ConnectionH1::start_stream` nor `ConnectionH2::start_stream` reads
768 // backend load state, so nothing about the decision changes.
769 let started = forward!(self, start_stream(stream, context));
770 if started {
771 self.post_start_stream_client_bookkeeping(context);
772 }
773 started
774 }
775}
776
777#[derive(Debug)]
778pub(super) struct EndpointServer<'a, Front: SocketHandler>(pub &'a mut Connection<Front>);
779#[derive(Debug)]
780pub(super) struct EndpointClient<'a>(pub &'a mut Router);
781
782// note: EndpointServer are used by client Connection, they do not know the frontend Token
783// they will use the Stream's Token which is their backend token
784impl<Front: SocketHandler + Debug> Endpoint for EndpointServer<'_, Front> {
785 fn readiness(&self, _token: Token) -> &Readiness {
786 self.0.readiness()
787 }
788 fn readiness_mut(&mut self, _token: Token) -> &mut Readiness {
789 self.0.readiness_mut()
790 }
791 fn peer_rtt(&self, _token: Token) -> Option<Duration> {
792 self.0.rtt()
793 }
794 fn local_rtt(&self) -> Option<Duration> {
795 None
796 }
797
798 fn end_stream<L>(&mut self, _token: Token, stream: GlobalStreamId, context: &mut Context<L>)
799 where
800 L: ListenerHandler + L7ListenerHandler,
801 {
802 // this may be used to forward H2<->H2 RstStream
803 // or to handle backend hup
804 self.0.end_stream(stream, context);
805 }
806
807 fn start_stream<L>(
808 &mut self,
809 _token: Token,
810 stream: GlobalStreamId,
811 context: &mut Context<L>,
812 ) -> bool
813 where
814 L: ListenerHandler + L7ListenerHandler,
815 {
816 // Forward stream start to the frontend connection.
817 // This is used when a backend H2 connection starts a new stream
818 // (e.g. for H2<->H2 proxying or PUSH_PROMISE forwarding).
819 self.0.start_stream(stream, context)
820 }
821}
822impl Endpoint for EndpointClient<'_> {
823 fn readiness(&self, token: Token) -> &Readiness {
824 match self.0.backends.get(&token) {
825 Some(backend) => backend.readiness(),
826 None => {
827 error!(
828 "{} backend token {:?} missing from backends map (readiness)",
829 log_module_context!(),
830 token
831 );
832 &self.0.fallback_readiness
833 }
834 }
835 }
836 fn readiness_mut(&mut self, token: Token) -> &mut Readiness {
837 match self.0.backends.get_mut(&token) {
838 Some(backend) => backend.readiness_mut(),
839 None => {
840 error!(
841 "{} backend token {:?} missing from backends map (readiness_mut)",
842 log_module_context!(),
843 token
844 );
845 &mut self.0.fallback_readiness
846 }
847 }
848 }
849 fn peer_rtt(&self, token: Token) -> Option<Duration> {
850 self.0.backends.get(&token).and_then(Connection::rtt)
851 }
852 fn local_rtt(&self) -> Option<Duration> {
853 None
854 }
855
856 fn end_stream<L>(&mut self, token: Token, stream: GlobalStreamId, context: &mut Context<L>)
857 where
858 L: ListenerHandler + L7ListenerHandler,
859 {
860 match self.0.backends.get_mut(&token) {
861 Some(backend) => backend.end_stream(stream, context),
862 None => {
863 error!(
864 "{} backend token {:?} missing from backends map (end_stream)",
865 log_module_context!(),
866 token
867 );
868 }
869 }
870 }
871
872 fn start_stream<L>(
873 &mut self,
874 token: Token,
875 stream: GlobalStreamId,
876 context: &mut Context<L>,
877 ) -> bool
878 where
879 L: ListenerHandler + L7ListenerHandler,
880 {
881 match self.0.backends.get_mut(&token) {
882 Some(backend) => backend.start_stream(stream, context),
883 None => {
884 error!(
885 "{} backend token {:?} missing from backends map (start_stream)",
886 log_module_context!(),
887 token
888 );
889 false
890 }
891 }
892 }
893}
894
895/// Test-only injection hooks for the mux layer.
896///
897/// These are compiled **only** when running `cargo test` (or with
898/// `cfg(test)` enabled); downstream code must not rely on them. They exist
899/// so end-to-end tests can drive hard-to-reach code paths — buffer-pool
900/// exhaustion during backend attach, stream-ID exhaustion — without
901/// having to reproduce the underlying resource starvation in-process.
902#[cfg(any(test, feature = "e2e-hooks"))]
903pub mod test_hooks {
904 use std::sync::atomic::AtomicBool;
905
906 /// When `true`, the next call to [`super::Connection::new_h2_client`]
907 /// returns `None` as if the buffer pool were exhausted. The flag is
908 /// consumed (reset to `false`) by that call so each opt-in is scoped
909 /// to exactly one attempted backend attach.
910 pub static FORCE_NEW_H2_CLIENT_FAILURE: AtomicBool = AtomicBool::new(false);
911
912 /// Arm or disarm the `new_h2_client` failure injection. Returns the
913 /// previous value so tests can stack-save/restore if they run in
914 /// parallel (`cargo test` defaults to serial for this crate because
915 /// of global registries, but keep the API honest).
916 pub fn __test_force_h2_client_failure(on: bool) -> bool {
917 FORCE_NEW_H2_CLIENT_FAILURE.swap(on, std::sync::atomic::Ordering::SeqCst)
918 }
919}