Skip to main content

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}