Skip to main content

sozu_lib/protocol/
pipe.rs

1//! Transparent byte-stream forwarder (TCP + WebSocket post-upgrade).
2//!
3//! Forwards bytes between front and back through fixed-size buffers without
4//! payload inspection. When bytes enter a sozu-owned buffer, the opposite
5//! endpoint is armed via `Readiness::arm_writable()` so edge-triggered epoll
6//! cannot park buffered data behind a missing writable edge. Used as the
7//! post-handshake state for raw TCP listeners and after a successful WebSocket
8//! upgrade on the H1 path.
9
10use std::{cell::RefCell, net::SocketAddr, rc::Rc};
11
12use mio::{Token, net::TcpStream};
13use rusty_ulid::Ulid;
14use sozu_command::{
15    config::MAX_LOOP_ITERATIONS,
16    logging::{EndpointRecord, LogContext, ansi_palette},
17    state::ClusterId,
18};
19
20use crate::metrics::names;
21use crate::{
22    L7Proxy, ListenerHandler, Protocol, Readiness, SessionMetrics, SessionResult, StateResult,
23    backends::Backend,
24    pool::Checkout,
25    protocol::{SessionState, http::parser::Method},
26    socket::{SocketHandler, SocketResult, TransportProtocol, stats::socket_rtt},
27    sozu_command::ready::Ready,
28    timer::TimeoutContainer,
29};
30
31#[cfg(all(target_os = "linux", feature = "splice"))]
32use crate::splice::{self, SplicePipe};
33
34/// This macro is defined uniquely in this module to help the tracking of
35/// pipelining issues inside Sōzu. Colored output uses bold bright-white
36/// (uniform across every protocol) for the protocol label, light grey for the
37/// `Session` keyword, gray for keys and bright white for values. The
38/// `[ulid - - -]` context comes first to stay aligned with `MUX-*` and
39/// `SOCKET` log lines.
40macro_rules! log_context {
41    ($self:expr) => {{
42        let (open, reset, grey, gray, white) = ansi_palette();
43        format!(
44            "{gray}{ctx}{reset}\t{open}PIPE{reset}\t{grey}Session{reset}({gray}address{reset}={white}{address}{reset}, {gray}frontend{reset}={white}{frontend}{reset}, {gray}frontend_readiness{reset}={white}{frontend_readiness}{reset}, {gray}frontend_status{reset}={white}{frontend_status:?}{reset}, {gray}backend{reset}={white}{backend}{reset}, {gray}backend_status{reset}={white}{backend_status:?}{reset}, {gray}backend_readiness{reset}={white}{backend_readiness}{reset})\t >>>",
45            open = open,
46            reset = reset,
47            grey = grey,
48            gray = gray,
49            white = white,
50            ctx = $self.log_context(),
51            address = $self.session_address.map(|addr| addr.to_string()).unwrap_or_else(|| "<none>".to_string()),
52            frontend = $self.frontend_token.0,
53            frontend_readiness = $self.frontend_readiness,
54            frontend_status = $self.frontend_status,
55            backend = $self.backend_token.map(|token| token.0.to_string()).unwrap_or_else(|| "<none>".to_string()),
56            backend_status = $self.backend_status,
57            backend_readiness = $self.backend_readiness,
58        )
59    }};
60}
61
62#[derive(PartialEq, Eq)]
63pub enum SessionStatus {
64    Normal,
65    DefaultAnswer,
66}
67
68#[derive(Copy, Clone, Debug)]
69enum ConnectionStatus {
70    Normal,
71    ReadOpen,
72    WriteOpen,
73    Closed,
74}
75
76/// matches sozu_command_lib::logging::access_logs::EndpointRecords
77pub enum WebSocketContext {
78    Http {
79        method: Option<Method>,
80        authority: Option<String>,
81        path: Option<String>,
82        status: Option<u16>,
83        reason: Option<String>,
84    },
85    Tcp,
86}
87
88pub struct Pipe<Front: SocketHandler, L: ListenerHandler> {
89    backend_buffer: Checkout,
90    backend_id: Option<String>,
91    /// Routed cluster lifetime inherited from an HTTP request or TCP session
92    /// when that owner upgrades into this long-lived pipe.
93    cluster_metrics_incarnation: crate::metrics::ClusterMetricsCapture,
94    /// Address of the backend this pipe forwards to, recorded when Sōzu
95    /// picked it: from `backend` in `Pipe::new` (WebSocket upgrades), or
96    /// through `set_backend_address` by the TCP proxy when it dials. The
97    /// access log reads it from here instead of asking the socket with
98    /// `getpeername(2)` once per session.
99    backend_address: Option<SocketAddr>,
100    pub backend_readiness: Readiness,
101    backend_socket: Option<TcpStream>,
102    backend_status: ConnectionStatus,
103    backend_token: Option<Token>,
104    pub backend: Option<Rc<RefCell<Backend>>>,
105    cluster_id: Option<ClusterId>,
106    pub container_backend_timeout: Option<TimeoutContainer>,
107    pub container_frontend_timeout: Option<TimeoutContainer>,
108    frontend_buffer: Checkout,
109    pub frontend_readiness: Readiness,
110    frontend_status: ConnectionStatus,
111    frontend_token: Token,
112    frontend: Front,
113    listener: Rc<RefCell<L>>,
114    protocol: Protocol,
115    /// Connection/session ULID inherited from the parent mux or handshake.
116    /// Emitted in the first slot of the legacy log-context bracket.
117    session_id: Ulid,
118    request_id: Ulid,
119    session_address: Option<SocketAddr>,
120    websocket_context: WebSocketContext,
121    /// Connection-scoped TLS metadata captured at handshake completion,
122    /// inherited from the upstream mux `HttpContext` when `Pipe` is created
123    /// via WSS upgrade. `None` on plaintext paths (plain TCP, plain WS,
124    /// proxy-protocol) where no TLS was terminated by Sōzu.
125    tls_version: Option<&'static str>,
126    tls_cipher: Option<&'static str>,
127    /// Negotiated SNI hostname, pre-lowercased, no port. `None` on plaintext
128    /// paths or when the client omitted the SNI extension.
129    tls_sni: Option<String>,
130    tls_alpn: Option<&'static str>,
131    /// Override for the access-log tags lookup key. `None` (every path but
132    /// TCP SNI-preread) falls back to the historical bare listener-address
133    /// key; SNI-routed TCP sessions carry the matched frontend's composed
134    /// key (`sni_tags_key` in `lib/src/tcp.rs`) so one listener's many
135    /// SNI/ALPN fronts each log their own tags.
136    tags_key: Option<String>,
137    /// Kernel-pipe pair used for zero-copy `splice(2)` forwarding on
138    /// `Protocol::TCP` listeners. Allocated lazily in `new()` and
139    /// `None` for WebSocket-after-upgrade paths or when allocation
140    /// failed (caller falls back to the buffered path).
141    #[cfg(all(target_os = "linux", feature = "splice"))]
142    splice_pipe: Option<SplicePipe>,
143}
144
145impl<Front: SocketHandler, L: ListenerHandler> Pipe<Front, L> {
146    /// Instantiate a new Pipe SessionState with:
147    ///
148    /// - frontend_interest: READABLE | WRITABLE | HUP | ERROR
149    /// - frontend_event: EMPTY
150    /// - backend_interest: READABLE | WRITABLE | HUP | ERROR
151    /// - backend_event: EMPTY
152    ///
153    /// Remember to set the events from the previous State!
154    #[allow(clippy::too_many_arguments)]
155    pub fn new(
156        backend_buffer: Checkout,
157        backend_id: Option<String>,
158        backend_socket: Option<TcpStream>,
159        backend: Option<Rc<RefCell<Backend>>>,
160        container_backend_timeout: Option<TimeoutContainer>,
161        container_frontend_timeout: Option<TimeoutContainer>,
162        cluster_id: Option<ClusterId>,
163        frontend_buffer: Checkout,
164        frontend_token: Token,
165        frontend: Front,
166        listener: Rc<RefCell<L>>,
167        protocol: Protocol,
168        session_id: Ulid,
169        request_id: Ulid,
170        session_address: Option<SocketAddr>,
171        websocket_context: WebSocketContext,
172    ) -> Pipe<Front, L> {
173        let frontend_status = ConnectionStatus::Normal;
174        let backend_status = if backend_socket.is_none() {
175            ConnectionStatus::Closed
176        } else {
177            ConnectionStatus::Normal
178        };
179
180        let backend_address = backend.as_ref().map(|backend| backend.borrow().address);
181        let mut session = Pipe {
182            backend_buffer,
183            backend_id,
184            cluster_metrics_incarnation: Default::default(),
185            backend_address,
186            backend_readiness: Readiness {
187                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
188                event: Ready::EMPTY,
189            },
190            backend_socket,
191            backend_status,
192            backend_token: None,
193            backend,
194            cluster_id,
195            container_backend_timeout,
196            container_frontend_timeout,
197            frontend_buffer,
198            frontend_readiness: Readiness {
199                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
200                event: Ready::EMPTY,
201            },
202            frontend_status,
203            frontend_token,
204            frontend,
205            listener,
206            protocol,
207            session_id,
208            request_id,
209            session_address,
210            websocket_context,
211            tls_version: None,
212            tls_cipher: None,
213            tls_sni: None,
214            tls_alpn: None,
215            tags_key: None,
216            #[cfg(all(target_os = "linux", feature = "splice"))]
217            splice_pipe: if protocol == Protocol::TCP {
218                SplicePipe::new()
219            } else {
220                None
221            },
222        };
223
224        session.arm_inherited_buffer_writes();
225
226        trace!("{} created pipe", log_context!(session));
227        session
228    }
229
230    fn arm_inherited_buffer_writes(&mut self) {
231        if self.backend_buffer.available_data() > 0 {
232            self.frontend_readiness.arm_writable();
233        }
234        if self.frontend_buffer.available_data() > 0 && self.backend_socket.is_some() {
235            self.backend_readiness.arm_writable();
236        }
237    }
238
239    pub fn restore_readiness_events(&mut self, frontend_event: Ready, backend_event: Ready) {
240        self.frontend_readiness.event = frontend_event;
241        self.backend_readiness.event = backend_event;
242        self.arm_inherited_buffer_writes();
243    }
244
245    pub(crate) fn set_cluster_metrics_incarnation(
246        &mut self,
247        incarnation: crate::metrics::ClusterMetricsCapture,
248    ) {
249        self.cluster_metrics_incarnation = incarnation;
250    }
251
252    /// Stamp connection-scoped TLS metadata onto the pipe for access-log
253    /// emission. Two caller classes exist:
254    ///
255    /// - the HTTPS→WSS upgrade path (`https.rs::upgrade_mux`), which stamps
256    ///   all four fields captured at handshake time from the prior mux
257    ///   `HttpContext` (version/cipher/SNI/ALPN);
258    /// - the TCP SNI-preread upgrade paths (`tcp.rs::upgrade_send` and
259    ///   `tcp.rs::build_pipe_from_preread`), which stamp the routed SNI and
260    ///   the offered-ALPN label with `version`/`cipher` deliberately `None`:
261    ///   Sōzu never terminates that TLS session — it only parses the
262    ///   ClientHello in passthrough — so it never learns the negotiated
263    ///   parameters.
264    ///
265    /// Plain (non-SNI-routed) TCP, plain WS, and proxy-protocol paths never
266    /// call this, so their access logs continue to emit `None` for all TLS
267    /// fields.
268    pub fn set_tls_metadata(
269        &mut self,
270        version: Option<&'static str>,
271        cipher: Option<&'static str>,
272        sni: Option<String>,
273        alpn: Option<&'static str>,
274    ) {
275        self.tls_version = version;
276        self.tls_cipher = cipher;
277        self.tls_sni = sni;
278        self.tls_alpn = alpn;
279    }
280
281    pub fn front_socket(&self) -> &TcpStream {
282        self.frontend.socket_ref()
283    }
284
285    /// The frontend socket handler itself, for what only it knows how to
286    /// record: `FrontRustls::readiness_delivered` (`lib/src/https.rs`).
287    pub fn front_socket_handler_mut(&mut self) -> &mut Front {
288        &mut self.frontend
289    }
290
291    pub fn front_socket_mut(&mut self) -> &mut TcpStream {
292        self.frontend.socket_mut()
293    }
294
295    pub fn back_socket(&self) -> Option<&TcpStream> {
296        self.backend_socket.as_ref()
297    }
298
299    pub fn back_socket_mut(&mut self) -> Option<&mut TcpStream> {
300        self.backend_socket.as_mut()
301    }
302
303    pub fn set_back_socket(&mut self, socket: TcpStream) {
304        self.backend_socket = Some(socket);
305        self.backend_status = ConnectionStatus::Normal;
306    }
307
308    pub fn back_token(&self) -> Vec<Token> {
309        self.backend_token.iter().cloned().collect()
310    }
311
312    fn reset_timeouts(&mut self) {
313        if let Some(t) = self.container_frontend_timeout.as_mut()
314            && !t.reset()
315        {
316            error!(
317                "{} Could not reset front timeout (pipe)",
318                log_context!(self)
319            );
320        }
321
322        if let Some(t) = self.container_backend_timeout.as_mut()
323            && !t.reset()
324        {
325            error!("{} Could not reset back timeout (pipe)", log_context!(self));
326        }
327    }
328
329    pub fn set_cluster_id(&mut self, cluster_id: Option<ClusterId>) {
330        self.cluster_id = cluster_id;
331    }
332
333    /// Override the access-log tags lookup key (see the `tags_key` field
334    /// doc). Called from `lib/src/tcp.rs`'s pipe-building upgrade paths for
335    /// SNI-routed sessions; `None` (the default) keeps the historical
336    /// bare-address lookup.
337    pub fn set_tags_key(&mut self, tags_key: Option<String>) {
338        self.tags_key = tags_key;
339    }
340
341    pub fn set_backend_id(&mut self, backend_id: Option<String>) {
342        self.backend_id = backend_id;
343    }
344
345    pub fn set_back_token(&mut self, token: Token) {
346        self.backend_token = Some(token);
347    }
348
349    /// Record the address of the backend Sōzu dialed for this pipe (see the
350    /// `backend_address` field doc). The TCP proxy calls it on every dial,
351    /// so a retry onto another backend overwrites the previous address.
352    pub fn set_backend_address(&mut self, backend_address: Option<SocketAddr>) {
353        self.backend_address = backend_address;
354    }
355
356    pub fn get_session_address(&self) -> Option<SocketAddr> {
357        self.session_address
358            .or_else(|| self.frontend.socket_ref().peer_addr().ok())
359    }
360
361    /// The address recorded when the backend was chosen, without a
362    /// `getpeername(2)`: it stays reported whatever the socket's state, where
363    /// `peer_addr` failed with `ENOTCONN` on a backend still connecting or
364    /// already reset.
365    pub fn get_backend_address(&self) -> Option<SocketAddr> {
366        self.backend_address
367    }
368
369    fn protocol_string(&self) -> &'static str {
370        match self.protocol {
371            Protocol::TCP => "TCP",
372            Protocol::HTTP => "WS",
373            Protocol::HTTPS => match self.frontend.protocol() {
374                TransportProtocol::Ssl2 => "WSS-SSL2",
375                TransportProtocol::Ssl3 => "WSS-SSL3",
376                TransportProtocol::Tls1_0 => "WSS-TLS1.0",
377                TransportProtocol::Tls1_1 => "WSS-TLS1.1",
378                TransportProtocol::Tls1_2 => "WSS-TLS1.2",
379                TransportProtocol::Tls1_3 => "WSS-TLS1.3",
380                _ => unreachable!(),
381            },
382            _ => unreachable!(),
383        }
384    }
385
386    pub fn log_request(&self, metrics: &SessionMetrics, error: bool, message: Option<&str>) {
387        let listener = self.listener.borrow();
388        let context = self.log_context();
389        let endpoint = self.log_endpoint();
390        metrics.register_end_of_session_for_incarnation(&context, self.cluster_metrics_incarnation);
391        // TCP SNI-preread sessions carry the matched frontend's own tags
392        // key (`set_tags_key`); every other path keeps the historical
393        // bare-address lookup.
394        let address_key = listener.get_addr().to_string();
395        let tags_key = self.tags_key.as_deref().unwrap_or(&address_key);
396        log_access!(
397            error,
398            on_failure: { incr!(names::access_logs::UNSENT) },
399            message,
400            context,
401            session_address: self.get_session_address(),
402            backend_address: self.get_backend_address(),
403            protocol: self.protocol_string(),
404            endpoint,
405            tags: listener.get_tags(tags_key),
406            client_rtt: socket_rtt(self.front_socket()),
407            server_rtt: self.backend_socket.as_ref().and_then(socket_rtt),
408            service_time: metrics.service_time(),
409            response_time: metrics.backend_response_time(),
410            request_time: metrics.request_time(),
411            start_time_ns: metrics.start_wall_ns(),
412            bytes_in: metrics.bin,
413            bytes_out: metrics.bout,
414            user_agent: None,
415            x_request_id: None,
416            // Pipe is post-upgrade; the TLS metadata was captured once at
417            // handshake in `https.rs::upgrade_handshake` and plumbed through
418            // via `set_tls_metadata`. Plaintext paths leave these fields as
419            // `None` — matching the TCP log shape.
420            tls_version: self.tls_version,
421            tls_cipher: self.tls_cipher,
422            tls_sni: self.tls_sni.as_deref(),
423            tls_alpn: self.tls_alpn,
424            xff_chain: None,
425            otel: None,
426        );
427    }
428
429    pub fn log_request_success(&self, metrics: &SessionMetrics) {
430        self.log_request(metrics, false, None);
431    }
432
433    pub fn log_request_error(&self, metrics: &SessionMetrics, message: &str) {
434        incr!(names::pipe::ERRORS);
435        error!(
436            "{} Could not process request properly got: {}",
437            log_context!(self),
438            message
439        );
440        self.print_state(self.protocol_string());
441        self.log_request(metrics, true, Some(message));
442    }
443
444    /// Access-log wrapper for benign idle-timeout tear-downs.
445    ///
446    /// Unlike `log_request_error`, this path logs at `debug!` and skips the
447    /// state dump — an idle pipe hitting its front/back_timeout is expected
448    /// behaviour (e.g. a WebSocket with no keepalive) and should not pollute
449    /// the error stream.
450    pub fn log_request_timeout(&self, metrics: &SessionMetrics, message: &str) {
451        debug!("{} pipe timeout: {}", log_context!(self), message);
452        self.log_request(metrics, true, Some(message));
453    }
454
455    /// Bytes currently sitting inside the `splice` frontend→backend
456    /// kernel pipe (`0` if splice is disabled or the pipe was not
457    /// allocated). Counted as "request in flight" by `check_connections`
458    /// so a half-closed session stays alive until the kernel drains.
459    #[cfg(all(target_os = "linux", feature = "splice"))]
460    fn splice_in_pending(&self) -> usize {
461        self.splice_pipe
462            .as_ref()
463            .map(|p| p.in_pipe_pending)
464            .unwrap_or(0)
465    }
466    #[cfg(not(all(target_os = "linux", feature = "splice")))]
467    fn splice_in_pending(&self) -> usize {
468        0
469    }
470
471    /// Bytes currently sitting inside the `splice` backend→frontend
472    /// kernel pipe. Counterpart to `splice_in_pending` for the response
473    /// direction.
474    #[cfg(all(target_os = "linux", feature = "splice"))]
475    fn splice_out_pending(&self) -> usize {
476        self.splice_pipe
477            .as_ref()
478            .map(|p| p.out_pipe_pending)
479            .unwrap_or(0)
480    }
481    #[cfg(not(all(target_os = "linux", feature = "splice")))]
482    fn splice_out_pending(&self) -> usize {
483        0
484    }
485
486    /// Realised kernel-pipe capacity per direction (`0` if splice is
487    /// disabled). Drives the "pipe is full" backpressure check in the
488    /// splice readable methods and the per-call `len` for `splice_in`.
489    #[cfg(all(target_os = "linux", feature = "splice"))]
490    fn splice_capacity(&self) -> usize {
491        self.splice_pipe.as_ref().map(|p| p.capacity).unwrap_or(0)
492    }
493
494    /// Tear down both readiness trackers ahead of a `SessionResult::Close`.
495    ///
496    /// This is the *write-only-shutdown discipline* (CLAUDE.md gotcha: never
497    /// `shutdown(Shutdown::Both)` on a TLS frontend — it emits a TCP RST that
498    /// truncates the already-queued response). `Pipe` never issues an explicit
499    /// `shutdown`; it closes purely by clearing interest+event so the event
500    /// loop stops driving I/O and lets the kernel flush queued bytes, with the
501    /// peer close arriving via the normal read path. The post-condition
502    /// asserts both trackers are fully cleared.
503    fn reset_readiness_for_close(&mut self) {
504        self.frontend_readiness.reset();
505        self.backend_readiness.reset();
506        debug_assert!(
507            self.frontend_readiness.interest.is_empty() && self.frontend_readiness.event.is_empty(),
508            "frontend readiness must be fully cleared on close (write-only-shutdown discipline)"
509        );
510        debug_assert!(
511            self.backend_readiness.interest.is_empty() && self.backend_readiness.event.is_empty(),
512            "backend readiness must be fully cleared on close (write-only-shutdown discipline)"
513        );
514    }
515
516    /// Wether the session should be kept open, depending on endpoints status
517    /// and buffer usage (both in memory and in kernel)
518    pub fn check_connections(&self) -> bool {
519        // In-flight accounting must never see more *buffered* bytes than the
520        // backing Checkout buffer can hold. We intentionally do NOT bound the
521        // splice-pending counters by the pipe `capacity`: a kernel pipe buffers
522        // well beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves
523        // skb-backed GRO segments, so `splice_*_pending` legitimately exceeds it
524        // (see `splice_readable`). A violation here means a `fill`/`consume`
525        // elsewhere desynced the counters, corrupting the keep-alive decision.
526        debug_assert!(
527            self.frontend_buffer.available_data() <= self.frontend_buffer.capacity(),
528            "frontend buffered data exceeds its capacity"
529        );
530        debug_assert!(
531            self.backend_buffer.available_data() <= self.backend_buffer.capacity(),
532            "backend buffered data exceeds its capacity"
533        );
534
535        let request_is_inflight = self.frontend_buffer.available_data() > 0
536            || self.frontend_readiness.event.is_readable()
537            || self.splice_in_pending() > 0;
538        let response_is_inflight = self.backend_buffer.available_data() > 0
539            || self.backend_readiness.event.is_readable()
540            || self.splice_out_pending() > 0;
541        match (self.frontend_status, self.backend_status) {
542            (ConnectionStatus::Normal, ConnectionStatus::Normal) => true,
543            (ConnectionStatus::Normal, ConnectionStatus::ReadOpen) => true,
544            (ConnectionStatus::Normal, ConnectionStatus::WriteOpen) => {
545                // technically we should keep it open, but we'll assume that if the front
546                // is not readable and there is no in flight data front -> back or back -> front,
547                // we'll close the session, otherwise it interacts badly with HTTP connections
548                // with Connection: close header and no Content-length
549                request_is_inflight || response_is_inflight
550            }
551            (ConnectionStatus::Normal, ConnectionStatus::Closed) => response_is_inflight,
552
553            (ConnectionStatus::WriteOpen, ConnectionStatus::Normal) => {
554                // technically we should keep it open, but we'll assume that if the back
555                // is not readable and there is no in flight data back -> front or front -> back, we'll close the session
556                request_is_inflight || response_is_inflight
557            }
558            (ConnectionStatus::WriteOpen, ConnectionStatus::ReadOpen) => true,
559            (ConnectionStatus::WriteOpen, ConnectionStatus::WriteOpen) => {
560                request_is_inflight || response_is_inflight
561            }
562            (ConnectionStatus::WriteOpen, ConnectionStatus::Closed) => response_is_inflight,
563
564            (ConnectionStatus::ReadOpen, ConnectionStatus::Normal) => true,
565            (ConnectionStatus::ReadOpen, ConnectionStatus::ReadOpen) => false,
566            (ConnectionStatus::ReadOpen, ConnectionStatus::WriteOpen) => true,
567            (ConnectionStatus::ReadOpen, ConnectionStatus::Closed) => false,
568
569            (ConnectionStatus::Closed, ConnectionStatus::Normal) => request_is_inflight,
570            (ConnectionStatus::Closed, ConnectionStatus::ReadOpen) => false,
571            (ConnectionStatus::Closed, ConnectionStatus::WriteOpen) => request_is_inflight,
572            (ConnectionStatus::Closed, ConnectionStatus::Closed) => false,
573        }
574    }
575
576    pub fn frontend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
577        self.frontend_status = ConnectionStatus::Closed;
578        // The frontend hung up: its status is now terminal regardless of
579        // which branch we take below (mirrors `backend_hup`).
580        debug_assert!(
581            matches!(self.frontend_status, ConnectionStatus::Closed),
582            "frontend_hup must mark the frontend Closed"
583        );
584        // EPOLLRDHUP only means the client sent FIN; on a loaded event loop it
585        // can coalesce with the payload tail into the same epoll batch, so
586        // bytes may still sit in `frontend_buffer` (already read), in the
587        // kernel receive buffer (not read yet, signalled by a pending
588        // READABLE event), or in the splice `in_pipe` (`splice_in_pending`)
589        // — see the sibling `SocketResult::Closed` drains in `readable` and
590        // `splice_readable` below and `check_connections`'s
591        // `request_is_inflight` (sozu-proxy/sozu#1290, the HUP-path variant
592        // of the same close-before-flush truncation).
593        let request_is_inflight = self.frontend_buffer.available_data() > 0
594            || self.frontend_readiness.event.is_readable()
595            || self.splice_in_pending() > 0;
596        if request_is_inflight && self.backend_socket.is_some() {
597            // Positive space: the branch that keeps the session alive must
598            // never be entered without something left to drain.
599            debug_assert!(
600                self.frontend_buffer.available_data() > 0
601                    || self.frontend_readiness.event.is_readable()
602                    || self.splice_in_pending() > 0,
603                "drain branch entered without any observable in-flight request bytes"
604            );
605            if self.frontend_readiness.event.is_readable() {
606                // Keep reading: the kernel still has the tail of the payload
607                // queued behind the FIN. `readable` drains it, possibly in one
608                // short read that stops before the EOF (`plain_socket_read`);
609                // `frontend_status` is already `Closed`, so the
610                // `check_connections` that follows the flush finishes the
611                // lifecycle either way, as does a `SocketResult::Closed` read.
612                self.frontend_readiness.interest.insert(Ready::READABLE);
613            }
614            self.backend_readiness.arm_writable();
615            debug!(
616                "{} Pipe::frontend_hup: frontend connection closed, keeping alive due to inflight request data.",
617                log_context!(self)
618            );
619            SessionResult::Continue
620        } else {
621            // Negative space: closing outright must never drop bytes still
622            // queued for a live backend.
623            debug_assert!(
624                self.backend_socket.is_none() || self.frontend_buffer.available_data() == 0,
625                "close branch entered with backend present but request bytes still queued"
626            );
627            self.log_request_success(metrics);
628            SessionResult::Close
629        }
630    }
631
632    pub fn backend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
633        self.backend_status = ConnectionStatus::Closed;
634        // The backend hung up: its status is now terminal regardless of which
635        // keep-alive branch we take below.
636        debug_assert!(
637            matches!(self.backend_status, ConnectionStatus::Closed),
638            "backend_hup must mark the backend Closed"
639        );
640        let pipe_has_data = self.splice_out_pending() > 0;
641        if self.backend_buffer.available_data() == 0 && !pipe_has_data {
642            // No buffered or in-kernel response data: there is nothing left to
643            // drain toward the frontend on this no-data branch.
644            debug_assert_eq!(
645                self.backend_buffer.available_data(),
646                0,
647                "no-data branch entered with response bytes still buffered"
648            );
649            if self.backend_readiness.event.is_readable() {
650                self.backend_readiness.interest.insert(Ready::READABLE);
651                debug!(
652                    "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in kernel.",
653                    log_context!(self)
654                );
655                SessionResult::Continue
656            } else {
657                self.log_request_success(metrics);
658                SessionResult::Close
659            }
660        } else {
661            debug!(
662                "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in buffers.",
663                log_context!(self)
664            );
665            self.frontend_readiness.arm_writable();
666            if self.backend_readiness.event.is_readable() {
667                self.backend_readiness.interest.insert(Ready::READABLE);
668            }
669            SessionResult::Continue
670        }
671    }
672
673    /// Shared tail of `readable`'s and `splice_readable`'s
674    /// `SocketResult::Closed` arms: the frontend read side just observed EOF.
675    /// Transition `frontend_status` to the half-closed state, clear the
676    /// READABLE interest/event (the read side is done), arm the backend
677    /// writable when bytes are still queued for it (`has_pending` —
678    /// `frontend_buffer.available_data() > 0` for the buffered path,
679    /// `splice_in_pending() > 0` for the splice path), and defer teardown to
680    /// `check_connections`, which closes only once nothing is left in flight
681    /// (mirrors `backend_hup`'s drain branch).
682    fn close_frontend_read_side(
683        &mut self,
684        metrics: &mut SessionMetrics,
685        has_pending: bool,
686    ) -> SessionResult {
687        self.frontend_status = match self.frontend_status {
688            ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
689            ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
690            s => s,
691        };
692        self.frontend_readiness.event.remove(Ready::READABLE);
693        self.frontend_readiness.interest.remove(Ready::READABLE);
694        if has_pending && self.backend_socket.is_some() {
695            self.backend_readiness.arm_writable();
696        }
697        if !self.check_connections() {
698            self.reset_readiness_for_close();
699            self.log_request_success(metrics);
700            return SessionResult::Close;
701        }
702        SessionResult::Continue
703    }
704
705    // Read content from the session
706    pub fn readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
707        // Inherited preread bytes (e.g. SNI ClientHello replay) sit in
708        // `frontend_buffer`; splice never drains that userspace buffer, so it
709        // must be empty before the fast path takes over.
710        #[cfg(all(target_os = "linux", feature = "splice"))]
711        if self.protocol == Protocol::TCP
712            && self.splice_pipe.is_some()
713            && self.frontend_buffer.available_data() == 0
714        {
715            return self.splice_readable(metrics);
716        }
717
718        self.reset_timeouts();
719
720        trace!("{} pipe readable", log_context!(self));
721        if self.frontend_buffer.available_space() == 0 {
722            self.frontend_readiness.interest.remove(Ready::READABLE);
723            self.backend_readiness.arm_writable();
724            return SessionResult::Continue;
725        }
726
727        let space_before = self.frontend_buffer.available_space();
728        let data_before = self.frontend_buffer.available_data();
729        let bin_before = metrics.bin;
730        let (sz, res) = self.frontend.socket_read(self.frontend_buffer.space());
731        // `socket_read` fills `buf[..]` and returns `min(read, buf.len())`; it
732        // can never report more bytes than the space slice it was handed.
733        debug_assert!(
734            sz <= space_before,
735            "frontend socket_read reported more bytes ({sz}) than the buffer space offered ({space_before})"
736        );
737        debug!("{} Read {} bytes", log_context!(self), sz);
738
739        if sz > 0 {
740            //FIXME: replace with copy()
741            self.frontend_buffer.fill(sz);
742            // `fill(sz)` with `sz <= available_space` moves exactly `sz` bytes
743            // from free space into readable data — no truncation, no growth.
744            debug_assert_eq!(
745                self.frontend_buffer.available_data(),
746                data_before + sz,
747                "fill must grow readable data by exactly the bytes read"
748            );
749
750            count!(names::backend::BYTES_IN, sz as i64);
751            metrics.bin += sz;
752            // Front→proxy ingress metric advances by exactly the bytes read.
753            debug_assert_eq!(
754                metrics.bin,
755                bin_before + sz,
756                "metrics.bin must advance by exactly the bytes read"
757            );
758
759            if self.frontend_buffer.available_space() == 0 {
760                self.frontend_readiness.interest.remove(Ready::READABLE);
761            }
762            self.backend_readiness.arm_writable();
763        } else {
764            self.frontend_readiness.event.remove(Ready::READABLE);
765
766            if res == SocketResult::Continue {
767                self.frontend_status = match self.frontend_status {
768                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
769                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
770                    s => s,
771                };
772            }
773        }
774
775        if !self.check_connections() {
776            self.reset_readiness_for_close();
777            self.log_request_success(metrics);
778            return SessionResult::Close;
779        }
780
781        match res {
782            SocketResult::Error => {
783                self.reset_readiness_for_close();
784                self.log_request_error(metrics, "front socket read error");
785                return SessionResult::Close;
786            }
787            SocketResult::Closed => {
788                // The frontend read side closed (EOF). Bytes it already
789                // delivered may still be queued in `frontend_buffer` for the
790                // backend; returning `Close` here unconditionally dropped
791                // them, silently truncating the stream whenever a front->back
792                // tail was still in flight (sozu-proxy/sozu#1279: a
793                // payload coalesced with the SNI ClientHello is the
794                // reproducer, but the drop hit any plain-TCP upload racing a
795                // frontend close). Transition to the half-closed `WriteOpen`
796                // status (same mapping as the zero-byte `Continue` read above),
797                // arm the backend writable to flush the queue, and defer the
798                // teardown to `check_connections`, which closes only once
799                // nothing is in flight (mirrors `backend_hup`'s drain branch).
800                // `frontend_hup` (EPOLLRDHUP, above) has the same drain
801                // requirement for the same reason: FIN can coalesce with the
802                // payload tail into one epoll batch on a loaded event loop.
803                // So does `splice_readable`'s own `Closed` arm, for bytes
804                // sitting in the kernel `in_pipe` instead of `frontend_buffer`.
805                let has_pending = self.frontend_buffer.available_data() > 0;
806                return self.close_frontend_read_side(metrics, has_pending);
807            }
808            SocketResult::WouldBlock => {
809                self.frontend_readiness.event.remove(Ready::READABLE);
810            }
811            SocketResult::Continue => {}
812        };
813
814        self.backend_readiness.arm_writable();
815        SessionResult::Continue
816    }
817
818    // Forward content to session
819    pub fn writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
820        // Mirror of the front-side preread gate: splice never drains the
821        // userspace `backend_buffer`, so it must be empty before the fast
822        // path takes over.
823        #[cfg(all(target_os = "linux", feature = "splice"))]
824        if self.protocol == Protocol::TCP
825            && self.splice_pipe.is_some()
826            && self.backend_buffer.available_data() == 0
827        {
828            return self.splice_writable(metrics);
829        }
830
831        trace!("{} Pipe writable", log_context!(self));
832        if self.backend_buffer.available_data() == 0 {
833            self.backend_readiness.interest.insert(Ready::READABLE);
834            self.frontend_readiness.interest.remove(Ready::WRITABLE);
835            return SessionResult::Continue;
836        }
837
838        let queued_total = self.backend_buffer.available_data();
839        let mut sz = 0usize;
840        let mut res = SocketResult::Continue;
841        while res == SocketResult::Continue {
842            // no more data in buffer, stop here
843            if self.backend_buffer.available_data() == 0 {
844                count!(names::backend::BYTES_OUT, sz as i64);
845                metrics.bout += sz;
846                self.backend_readiness.interest.insert(Ready::READABLE);
847                self.frontend_readiness.interest.remove(Ready::WRITABLE);
848                return SessionResult::Continue;
849            }
850            let queued = self.backend_buffer.available_data();
851            let (current_sz, current_res) = self.frontend.socket_write(self.backend_buffer.data());
852            // A partial write can never report more than was queued: the
853            // socket writes from `data()` and returns `min(written, data.len())`.
854            debug_assert!(
855                current_sz <= queued,
856                "frontend socket_write reported {current_sz} bytes but only {queued} were queued"
857            );
858            res = current_res;
859            let consumed = self.backend_buffer.consume(current_sz);
860            // `consume` drops exactly the written bytes (we already proved
861            // `current_sz <= available_data`, so no clamping occurs).
862            debug_assert_eq!(
863                consumed, current_sz,
864                "consume must drop exactly the bytes written to the frontend"
865            );
866            sz += current_sz;
867            // Cumulative transfer never overruns what was queued at entry.
868            debug_assert!(
869                sz <= queued_total,
870                "cumulative frontend write ({sz}) exceeded the queued backend data ({queued_total})"
871            );
872
873            if current_sz == 0 && res == SocketResult::Continue {
874                self.frontend_status = match self.frontend_status {
875                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
876                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
877                    s => s,
878                };
879            }
880
881            if !self.check_connections() {
882                metrics.bout += sz;
883                count!(names::backend::BYTES_OUT, sz as i64);
884                self.reset_readiness_for_close();
885                self.log_request_success(metrics);
886                return SessionResult::Close;
887            }
888        }
889
890        if sz > 0 {
891            count!(names::backend::BYTES_OUT, sz as i64);
892            self.backend_readiness.interest.insert(Ready::READABLE);
893            metrics.bout += sz;
894        }
895
896        debug!(
897            "{} Wrote {} bytes of {}",
898            log_context!(self),
899            sz,
900            self.backend_buffer.available_data()
901        );
902
903        match res {
904            SocketResult::Error => {
905                self.reset_readiness_for_close();
906                self.log_request_error(metrics, "front socket write error");
907                return SessionResult::Close;
908            }
909            SocketResult::Closed => {
910                self.reset_readiness_for_close();
911                self.log_request_success(metrics);
912                return SessionResult::Close;
913            }
914            SocketResult::WouldBlock => {
915                self.frontend_readiness.event.remove(Ready::WRITABLE);
916            }
917            SocketResult::Continue => {}
918        }
919
920        SessionResult::Continue
921    }
922
923    // Forward content to cluster
924    pub fn backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
925        // Inherited preread bytes (e.g. SNI ClientHello replay) sit in
926        // `frontend_buffer`; splice never drains that userspace buffer, so it
927        // must be empty before the fast path takes over.
928        #[cfg(all(target_os = "linux", feature = "splice"))]
929        if self.protocol == Protocol::TCP
930            && self.splice_pipe.is_some()
931            && self.frontend_buffer.available_data() == 0
932        {
933            return self.splice_backend_writable(metrics);
934        }
935
936        trace!("{} pipe back_writable", log_context!(self));
937
938        if self.frontend_buffer.available_data() == 0 {
939            self.frontend_readiness.interest.insert(Ready::READABLE);
940            self.backend_readiness.interest.remove(Ready::WRITABLE);
941            return SessionResult::Continue;
942        }
943
944        let output_size = self.frontend_buffer.available_data();
945
946        let mut sz = 0usize;
947        let mut socket_res = SocketResult::Continue;
948        // Set when the loop below exits because `frontend_buffer` just ran
949        // dry (as opposed to exiting because of a socket error/close/would-
950        // block). `backend` (below) borrows `self.backend_socket` for the
951        // whole loop, so the drained-buffer close gate has to live outside
952        // this `if let` — it needs `&mut self` for `check_connections` and
953        // friends, which would conflict with `backend` if run inline.
954        let mut drained = false;
955
956        if let Some(ref mut backend) = self.backend_socket {
957            while socket_res == SocketResult::Continue {
958                // no more data in buffer, stop here
959                if self.frontend_buffer.available_data() == 0 {
960                    self.frontend_readiness.interest.insert(Ready::READABLE);
961                    self.backend_readiness.interest.remove(Ready::WRITABLE);
962                    drained = true;
963                    break;
964                }
965
966                let queued = self.frontend_buffer.available_data();
967                let (current_sz, current_res) = backend.socket_write(self.frontend_buffer.data());
968                // A partial write can never report more than was queued.
969                debug_assert!(
970                    current_sz <= queued,
971                    "backend socket_write reported {current_sz} bytes but only {queued} were queued"
972                );
973                socket_res = current_res;
974                let consumed = self.frontend_buffer.consume(current_sz);
975                debug_assert_eq!(
976                    consumed, current_sz,
977                    "consume must drop exactly the bytes written to the backend"
978                );
979                sz += current_sz;
980                // Cumulative transfer never overruns the data queued at entry.
981                debug_assert!(
982                    sz <= output_size,
983                    "cumulative backend write ({sz}) exceeded the queued frontend data ({output_size})"
984                );
985
986                if current_sz == 0 && current_res == SocketResult::Continue {
987                    self.backend_status = match self.backend_status {
988                        ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
989                        ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
990                        s => s,
991                    };
992                }
993            }
994        }
995
996        if drained {
997            count!(names::backend::BACK_BYTES_OUT, sz as i64);
998            metrics.backend_bout += sz;
999            // The queued request bytes just fully drained toward the
1000            // backend. This early return used to always report `Continue`,
1001            // even when the frontend read side had already hit EOF
1002            // (`WriteOpen`/`Closed`, set by `readable`'s/`splice_readable`'s
1003            // `SocketResult::Closed` arm — sozu-proxy/sozu#1290): the
1004            // READABLE edge that would have driven a follow-up `readable()`
1005            // call was already consumed by that arm, so edge-triggered epoll
1006            // offers no other event to hang the teardown on and the session
1007            // sat resident until an unrelated event or timeout. Run the same
1008            // `check_connections` gate every other close goes through so an
1009            // already-half-closed session tears down as soon as the final
1010            // queued bytes drain. Healthy sessions (frontend still
1011            // `Normal`/`ReadOpen`) are unaffected: the gate is skipped
1012            // entirely and this keeps returning `Continue`, matching the
1013            // pre-fix behavior for normal keepalive flow.
1014            if matches!(
1015                self.frontend_status,
1016                ConnectionStatus::WriteOpen | ConnectionStatus::Closed
1017            ) && !self.check_connections()
1018            {
1019                self.reset_readiness_for_close();
1020                self.log_request_success(metrics);
1021                return SessionResult::Close;
1022            }
1023            return SessionResult::Continue;
1024        }
1025
1026        let backend_bout_before = metrics.backend_bout;
1027        count!(names::backend::BACK_BYTES_OUT, sz as i64);
1028        metrics.backend_bout += sz;
1029        // Proxy→backend egress metric advances by exactly the bytes written.
1030        debug_assert_eq!(
1031            metrics.backend_bout,
1032            backend_bout_before + sz,
1033            "metrics.backend_bout must advance by exactly the bytes written"
1034        );
1035
1036        if !self.check_connections() {
1037            self.reset_readiness_for_close();
1038            self.log_request_success(metrics);
1039            return SessionResult::Close;
1040        }
1041
1042        debug!(
1043            "{} Wrote {} bytes of {}",
1044            log_context!(self),
1045            sz,
1046            output_size
1047        );
1048
1049        match socket_res {
1050            SocketResult::Error => {
1051                self.reset_readiness_for_close();
1052                self.log_request_error(metrics, "back socket write error");
1053                return SessionResult::Close;
1054            }
1055            SocketResult::Closed => {
1056                self.reset_readiness_for_close();
1057                self.log_request_success(metrics);
1058                return SessionResult::Close;
1059            }
1060            SocketResult::WouldBlock => {
1061                self.backend_readiness.event.remove(Ready::WRITABLE);
1062            }
1063            SocketResult::Continue => {}
1064        }
1065        SessionResult::Continue
1066    }
1067
1068    // Read content from cluster
1069    pub fn backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1070        // Mirror of the front-side preread gate: splice never drains the
1071        // userspace `backend_buffer`, so it must be empty before the fast
1072        // path takes over.
1073        #[cfg(all(target_os = "linux", feature = "splice"))]
1074        if self.protocol == Protocol::TCP
1075            && self.splice_pipe.is_some()
1076            && self.backend_buffer.available_data() == 0
1077        {
1078            return self.splice_backend_readable(metrics);
1079        }
1080
1081        self.reset_timeouts();
1082
1083        trace!("{} Pipe backend_readable", log_context!(self));
1084        if self.backend_buffer.available_space() == 0 {
1085            self.backend_readiness.interest.remove(Ready::READABLE);
1086            return SessionResult::Continue;
1087        }
1088
1089        let space_before = self.backend_buffer.available_space();
1090        let data_before = self.backend_buffer.available_data();
1091        let backend_bin_before = metrics.backend_bin;
1092        if let Some(ref mut backend) = self.backend_socket {
1093            let (size, remaining) = backend.socket_read(self.backend_buffer.space());
1094            // `socket_read` reports at most the space slice it was handed.
1095            debug_assert!(
1096                size <= space_before,
1097                "backend socket_read reported more bytes ({size}) than the buffer space offered ({space_before})"
1098            );
1099            self.backend_buffer.fill(size);
1100            // `fill(size)` with `size <= available_space` moves exactly `size`
1101            // bytes from free space into readable data.
1102            debug_assert_eq!(
1103                self.backend_buffer.available_data(),
1104                data_before + size,
1105                "fill must grow readable data by exactly the bytes read"
1106            );
1107
1108            debug!("{} Read {} bytes", log_context!(self), size);
1109
1110            if remaining != SocketResult::Continue || size == 0 {
1111                self.backend_readiness.event.remove(Ready::READABLE);
1112            }
1113            if size > 0 {
1114                self.frontend_readiness.arm_writable();
1115                count!(names::backend::BACK_BYTES_IN, size as i64);
1116                metrics.backend_bin += size;
1117                // Backend→proxy ingress metric advances by exactly bytes read.
1118                debug_assert_eq!(
1119                    metrics.backend_bin,
1120                    backend_bin_before + size,
1121                    "metrics.backend_bin must advance by exactly the bytes read"
1122                );
1123            }
1124
1125            if size == 0 && remaining == SocketResult::Closed {
1126                self.backend_status = match self.backend_status {
1127                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1128                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1129                    s => s,
1130                };
1131
1132                if !self.check_connections() {
1133                    self.reset_readiness_for_close();
1134                    self.log_request_success(metrics);
1135                    return SessionResult::Close;
1136                }
1137            }
1138
1139            match remaining {
1140                SocketResult::Error => {
1141                    self.reset_readiness_for_close();
1142                    self.log_request_error(metrics, "back socket read error");
1143                    return SessionResult::Close;
1144                }
1145                SocketResult::Closed => {
1146                    if !self.check_connections() {
1147                        self.reset_readiness_for_close();
1148                        self.log_request_success(metrics);
1149                        return SessionResult::Close;
1150                    }
1151                }
1152                SocketResult::WouldBlock => {
1153                    self.backend_readiness.event.remove(Ready::READABLE);
1154                }
1155                SocketResult::Continue => {}
1156            }
1157        }
1158
1159        SessionResult::Continue
1160    }
1161
1162    /// Zero-copy fast path of `readable`: pull bytes off the frontend
1163    /// socket into the kernel `in_pipe` via `splice(2)`, then mark the
1164    /// backend writable so the data drains in the next event loop tick.
1165    ///
1166    /// Mirrors `readable`'s `ConnectionStatus` transitions and metric
1167    /// emissions exactly so observability and the `check_connections`
1168    /// state machine behave the same with or without the feature flag.
1169    #[cfg(all(target_os = "linux", feature = "splice"))]
1170    fn splice_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1171        self.reset_timeouts();
1172
1173        trace!("{} pipe splice_readable", log_context!(self));
1174        let capacity = self.splice_capacity();
1175        if self.splice_in_pending() >= capacity {
1176            // Pipe is full — stop reading and let the backend drain it.
1177            self.frontend_readiness.interest.remove(Ready::READABLE);
1178            self.backend_readiness.arm_writable();
1179            return SessionResult::Continue;
1180        }
1181
1182        let pending_before = self.splice_in_pending();
1183        let bin_before = metrics.bin;
1184        let pipe_write_end = self.splice_pipe.as_ref().unwrap().in_pipe[1];
1185        let (sz, res) = splice::splice_in(self.frontend.socket_ref(), pipe_write_end, capacity);
1186        // `splice_in` is asked for at most `capacity` bytes, so the kernel can
1187        // never report moving more than that in one call. We deliberately do
1188        // NOT assert `in_pipe_pending <= capacity`: a kernel pipe buffers well
1189        // beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed
1190        // segments — a GRO super-packet on loopback hands a single ring slot far
1191        // more than a page — so byte-occupancy legitimately exceeds `capacity`.
1192        // `capacity` is the per-call `len` and a soft backpressure threshold,
1193        // not a hard occupancy bound.
1194        debug_assert!(
1195            sz <= capacity,
1196            "splice_in reported {sz} bytes but was capped at len {capacity}"
1197        );
1198        debug!("{} Spliced {} bytes from frontend", log_context!(self), sz);
1199
1200        // Same EAGAIN ambiguity as `splice_backend_readable`: with bytes
1201        // still pending in the in_pipe the kernel may be reporting a full
1202        // pipe rather than a drained frontend socket. Keep the READABLE
1203        // event, park the interest until `splice_backend_writable` drains
1204        // the pipe and re-arms it (mirrors the capacity guard above).
1205        // This exit skips `check_connections`: the pipe is non-empty so the
1206        // request is inflight by definition, and `splice_backend_writable`
1207        // (armed here) runs the check unconditionally on its drain.
1208        if res == SocketResult::WouldBlock && self.splice_in_pending() > 0 {
1209            debug_assert_eq!(sz, 0, "splice_in must report 0 bytes on EAGAIN");
1210            self.frontend_readiness.interest.remove(Ready::READABLE);
1211            self.backend_readiness.arm_writable();
1212            debug_assert!(
1213                self.frontend_readiness.event.is_readable()
1214                    && !self.frontend_readiness.interest.is_readable(),
1215                "a pipe-full EAGAIN must keep the READABLE event pending and park the interest"
1216            );
1217            debug_assert_eq!(
1218                self.splice_in_pending(),
1219                pending_before,
1220                "a pipe-full EAGAIN must not change the in_pipe accounting"
1221            );
1222            return SessionResult::Continue;
1223        }
1224
1225        if sz > 0 {
1226            self.splice_pipe.as_mut().unwrap().in_pipe_pending += sz;
1227            // Pending advanced by exactly the spliced bytes (tracks real
1228            // kernel-pipe occupancy; see the capacity note above).
1229            debug_assert_eq!(
1230                self.splice_in_pending(),
1231                pending_before + sz,
1232                "in_pipe_pending must grow by exactly the spliced bytes"
1233            );
1234            count!(names::backend::BYTES_IN, sz as i64);
1235            metrics.bin += sz;
1236            debug_assert_eq!(
1237                metrics.bin,
1238                bin_before + sz,
1239                "metrics.bin must advance by exactly the spliced bytes"
1240            );
1241            self.backend_readiness.arm_writable();
1242        } else {
1243            self.frontend_readiness.event.remove(Ready::READABLE);
1244
1245            if res == SocketResult::Continue {
1246                self.frontend_status = match self.frontend_status {
1247                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1248                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1249                    s => s,
1250                };
1251            }
1252        }
1253
1254        if !self.check_connections() {
1255            self.reset_readiness_for_close();
1256            self.log_request_success(metrics);
1257            return SessionResult::Close;
1258        }
1259
1260        match res {
1261            SocketResult::Error => {
1262                self.reset_readiness_for_close();
1263                self.log_request_error(metrics, "splice front socket read error");
1264                return SessionResult::Close;
1265            }
1266            SocketResult::Closed => {
1267                // The frontend read side closed (EOF). This is the SPLICE
1268                // sibling of `readable`'s `SocketResult::Closed` drain above
1269                // (sozu-proxy/sozu#1279) and of `frontend_hup`'s
1270                // drain branch: bytes already spliced into the kernel
1271                // `in_pipe` (`splice_in_pending()`) still belong to the
1272                // backend, and returning `Close` here unconditionally
1273                // dropped them. Transition to the half-closed `WriteOpen`
1274                // status, arm the backend writable to drain the kernel pipe
1275                // via `splice_backend_writable`, and defer teardown to
1276                // `check_connections`, whose `request_is_inflight` already
1277                // counts `splice_in_pending() > 0`; once the pipe drains,
1278                // the next `splice_readable` re-observes EOF with nothing
1279                // inflight and closes through the `check_connections` gate
1280                // above this match.
1281                let has_pending = self.splice_in_pending() > 0;
1282                return self.close_frontend_read_side(metrics, has_pending);
1283            }
1284            SocketResult::WouldBlock => {
1285                self.frontend_readiness.event.remove(Ready::READABLE);
1286            }
1287            SocketResult::Continue => {}
1288        }
1289
1290        self.backend_readiness.arm_writable();
1291        SessionResult::Continue
1292    }
1293
1294    /// Zero-copy fast path of `writable`: drain the backend→frontend
1295    /// kernel `out_pipe` toward the frontend socket via `splice(2)`.
1296    /// Mirrors `writable`'s loop, status transitions, and metric
1297    /// emissions.
1298    #[cfg(all(target_os = "linux", feature = "splice"))]
1299    fn splice_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1300        trace!("{} Pipe splice_writable", log_context!(self));
1301        if self.splice_out_pending() == 0 {
1302            self.backend_readiness.interest.insert(Ready::READABLE);
1303            self.frontend_readiness.interest.remove(Ready::WRITABLE);
1304            return SessionResult::Continue;
1305        }
1306
1307        let mut sz = 0usize;
1308        let mut res = SocketResult::Continue;
1309        while res == SocketResult::Continue {
1310            let pending = self.splice_out_pending();
1311            // no more data in pipe, stop here
1312            if pending == 0 {
1313                count!(names::backend::BYTES_OUT, sz as i64);
1314                metrics.bout += sz;
1315                self.backend_readiness.interest.insert(Ready::READABLE);
1316                self.frontend_readiness.interest.remove(Ready::WRITABLE);
1317                return SessionResult::Continue;
1318            }
1319
1320            let pipe_read_end = self.splice_pipe.as_ref().unwrap().out_pipe[0];
1321            let (current_sz, current_res) =
1322                splice::splice_out(pipe_read_end, self.frontend.socket_ref(), pending);
1323            // `splice_out` was asked for `pending` bytes and can drain no more
1324            // than the pipe holds; draining more than `pending` would underflow
1325            // `out_pipe_pending` below.
1326            debug_assert!(
1327                current_sz <= pending,
1328                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1329            );
1330            res = current_res;
1331            if current_sz > 0 {
1332                self.splice_pipe.as_mut().unwrap().out_pipe_pending -= current_sz;
1333                debug_assert_eq!(
1334                    self.splice_out_pending(),
1335                    pending - current_sz,
1336                    "out_pipe_pending must shrink by exactly the drained bytes"
1337                );
1338            }
1339            sz += current_sz;
1340
1341            if current_sz == 0 && res == SocketResult::Continue {
1342                self.frontend_status = match self.frontend_status {
1343                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1344                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1345                    s => s,
1346                };
1347            }
1348
1349            if !self.check_connections() {
1350                metrics.bout += sz;
1351                count!(names::backend::BYTES_OUT, sz as i64);
1352                self.reset_readiness_for_close();
1353                self.log_request_success(metrics);
1354                return SessionResult::Close;
1355            }
1356        }
1357
1358        if sz > 0 {
1359            count!(names::backend::BYTES_OUT, sz as i64);
1360            self.backend_readiness.interest.insert(Ready::READABLE);
1361            metrics.bout += sz;
1362        }
1363
1364        debug!(
1365            "{} Spliced {} bytes (out_pipe_pending={})",
1366            log_context!(self),
1367            sz,
1368            self.splice_out_pending()
1369        );
1370
1371        match res {
1372            SocketResult::Error => {
1373                self.reset_readiness_for_close();
1374                self.log_request_error(metrics, "splice front socket write error");
1375                return SessionResult::Close;
1376            }
1377            SocketResult::Closed => {
1378                self.reset_readiness_for_close();
1379                self.log_request_success(metrics);
1380                return SessionResult::Close;
1381            }
1382            SocketResult::WouldBlock => {
1383                self.frontend_readiness.event.remove(Ready::WRITABLE);
1384            }
1385            SocketResult::Continue => {}
1386        }
1387
1388        SessionResult::Continue
1389    }
1390
1391    /// Zero-copy fast path of `backend_writable`: drain the
1392    /// frontend→backend kernel `in_pipe` toward the backend socket via
1393    /// `splice(2)`. Mirrors `backend_writable`'s loop, status
1394    /// transitions, and metric emissions.
1395    #[cfg(all(target_os = "linux", feature = "splice"))]
1396    fn splice_backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1397        trace!("{} pipe splice_backend_writable", log_context!(self));
1398
1399        if self.splice_in_pending() == 0 {
1400            self.frontend_readiness.interest.insert(Ready::READABLE);
1401            self.backend_readiness.interest.remove(Ready::WRITABLE);
1402            return SessionResult::Continue;
1403        }
1404
1405        let output_size = self.splice_in_pending();
1406        let mut sz = 0usize;
1407        let mut socket_res = SocketResult::Continue;
1408
1409        while socket_res == SocketResult::Continue {
1410            let pending = self.splice_in_pending();
1411            // no more data in pipe, stop here
1412            if pending == 0 {
1413                self.frontend_readiness.interest.insert(Ready::READABLE);
1414                self.backend_readiness.interest.remove(Ready::WRITABLE);
1415                count!(names::backend::BACK_BYTES_OUT, sz as i64);
1416                metrics.backend_bout += sz;
1417                // The queued bytes just fully drained out of the kernel
1418                // `in_pipe`. This early return used to always report
1419                // `Continue`, even when the frontend read side had already
1420                // hit EOF (`WriteOpen`/`Closed`, set by `splice_readable`'s
1421                // `SocketResult::Closed` arm — sozu-proxy/sozu#1290): the
1422                // READABLE edge that would have driven a follow-up
1423                // `splice_readable` call was already consumed by that arm, so
1424                // edge-triggered epoll offers no other event to hang the
1425                // teardown on and the session sat resident until an
1426                // unrelated event or timeout. Run the same
1427                // `check_connections` gate every other close goes through so
1428                // an already-half-closed session tears down as soon as the
1429                // final queued bytes drain. Healthy sessions (frontend still
1430                // `Normal`/`ReadOpen`) are unaffected: the gate is skipped
1431                // entirely and this keeps returning `Continue`, matching the
1432                // pre-fix behavior for normal keepalive flow.
1433                if matches!(
1434                    self.frontend_status,
1435                    ConnectionStatus::WriteOpen | ConnectionStatus::Closed
1436                ) && !self.check_connections()
1437                {
1438                    self.reset_readiness_for_close();
1439                    self.log_request_success(metrics);
1440                    return SessionResult::Close;
1441                }
1442                return SessionResult::Continue;
1443            }
1444
1445            let pipe_read_end = self.splice_pipe.as_ref().unwrap().in_pipe[0];
1446            let (current_sz, current_res) = match self.backend_socket.as_ref() {
1447                Some(b) => splice::splice_out(pipe_read_end, b, pending),
1448                None => break,
1449            };
1450            // Draining more than `pending` would underflow `in_pipe_pending`.
1451            debug_assert!(
1452                current_sz <= pending,
1453                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1454            );
1455            socket_res = current_res;
1456            if current_sz > 0 {
1457                self.splice_pipe.as_mut().unwrap().in_pipe_pending -= current_sz;
1458                debug_assert_eq!(
1459                    self.splice_in_pending(),
1460                    pending - current_sz,
1461                    "in_pipe_pending must shrink by exactly the drained bytes"
1462                );
1463            }
1464            sz += current_sz;
1465            // Cumulative drain never exceeds what was pending at entry.
1466            debug_assert!(
1467                sz <= output_size,
1468                "cumulative splice drain ({sz}) exceeded the bytes pending at entry ({output_size})"
1469            );
1470
1471            if current_sz == 0 && current_res == SocketResult::Continue {
1472                self.backend_status = match self.backend_status {
1473                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1474                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1475                    s => s,
1476                };
1477            }
1478        }
1479
1480        count!(names::backend::BACK_BYTES_OUT, sz as i64);
1481        metrics.backend_bout += sz;
1482        if sz > 0 {
1483            // A partial drain freed pipe slots: resume the frontend read that
1484            // `splice_readable` parked on a pipe-full EAGAIN (mirrors
1485            // `splice_writable`'s re-arm of the backend read).
1486            self.frontend_readiness.interest.insert(Ready::READABLE);
1487        }
1488
1489        if !self.check_connections() {
1490            self.reset_readiness_for_close();
1491            self.log_request_success(metrics);
1492            return SessionResult::Close;
1493        }
1494
1495        debug!(
1496            "{} Spliced {} bytes of {}",
1497            log_context!(self),
1498            sz,
1499            output_size
1500        );
1501
1502        match socket_res {
1503            SocketResult::Error => {
1504                self.reset_readiness_for_close();
1505                self.log_request_error(metrics, "splice back socket write error");
1506                return SessionResult::Close;
1507            }
1508            SocketResult::Closed => {
1509                self.reset_readiness_for_close();
1510                self.log_request_success(metrics);
1511                return SessionResult::Close;
1512            }
1513            SocketResult::WouldBlock => {
1514                self.backend_readiness.event.remove(Ready::WRITABLE);
1515            }
1516            SocketResult::Continue => {}
1517        }
1518        SessionResult::Continue
1519    }
1520
1521    /// Zero-copy fast path of `backend_readable`: pull bytes off the
1522    /// backend socket into the kernel `out_pipe` via `splice(2)`, then
1523    /// mark the frontend writable so the data drains in the next event
1524    /// loop tick. Mirrors `backend_readable`'s status transitions and
1525    /// metric emissions.
1526    #[cfg(all(target_os = "linux", feature = "splice"))]
1527    fn splice_backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1528        self.reset_timeouts();
1529
1530        trace!("{} Pipe splice_backend_readable", log_context!(self));
1531        let capacity = self.splice_capacity();
1532        if self.splice_out_pending() >= capacity {
1533            // Pipe is full — stop reading and let the frontend drain it.
1534            self.backend_readiness.interest.remove(Ready::READABLE);
1535            self.frontend_readiness.arm_writable();
1536            return SessionResult::Continue;
1537        }
1538
1539        let pending_before = self.splice_out_pending();
1540        let backend_bin_before = metrics.backend_bin;
1541        let pipe_write_end = self.splice_pipe.as_ref().unwrap().out_pipe[1];
1542        let (size, remaining) = match self.backend_socket.as_ref() {
1543            Some(b) => splice::splice_in(b, pipe_write_end, capacity),
1544            None => return SessionResult::Continue,
1545        };
1546        // `splice_in` is capped at `len = capacity`, so the kernel never reports
1547        // moving more than that per call. As in `splice_readable`, we do NOT
1548        // assert `out_pipe_pending <= capacity`: a kernel pipe holds well beyond
1549        // its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed (GRO)
1550        // segments, so byte-occupancy legitimately exceeds `capacity` — it is
1551        // only the per-call `len` and a soft backpressure threshold.
1552        debug_assert!(
1553            size <= capacity,
1554            "splice_in reported {size} bytes but was capped at len {capacity}"
1555        );
1556
1557        debug!("{} Spliced {} bytes from backend", log_context!(self), size);
1558
1559        // `splice(2)` reports EAGAIN both when the socket has nothing left to
1560        // read and when the pipe has no free slot: a pipe holds
1561        // `capacity / PAGE_SIZE` slots, every spliced skb fragment takes one
1562        // whatever its length, and a partially drained slot stays occupied.
1563        // So the slots run out while `out_pipe_pending < capacity` (MSS-sized
1564        // segments from a remote backend fill 16 slots at ~23 KiB). An empty
1565        // pipe always has a free slot, so EAGAIN there is a drained socket;
1566        // with bytes pending it may be the pipe. Clearing the READABLE event
1567        // on that ambiguity loses bytes already queued in the socket for
1568        // good: edge-triggered epoll never re-signals them and the client
1569        // hangs on the tail of the response. Keep the event, park the
1570        // interest until `splice_writable` drains the pipe and re-arms it,
1571        // exactly like the `out_pending >= capacity` guard above.
1572        let pipe_may_be_full =
1573            remaining == SocketResult::WouldBlock && self.splice_out_pending() > 0;
1574        if pipe_may_be_full {
1575            debug_assert_eq!(size, 0, "splice_in must report 0 bytes on EAGAIN");
1576            self.backend_readiness.interest.remove(Ready::READABLE);
1577            self.frontend_readiness.arm_writable();
1578            debug_assert!(
1579                self.backend_readiness.event.is_readable()
1580                    && !self.backend_readiness.interest.is_readable(),
1581                "a pipe-full EAGAIN must keep the READABLE event pending and park the interest"
1582            );
1583            debug_assert_eq!(
1584                self.splice_out_pending(),
1585                pending_before,
1586                "a pipe-full EAGAIN must not change the out_pipe accounting"
1587            );
1588            return SessionResult::Continue;
1589        }
1590        if remaining != SocketResult::Continue || size == 0 {
1591            self.backend_readiness.event.remove(Ready::READABLE);
1592        }
1593        if size > 0 {
1594            self.splice_pipe.as_mut().unwrap().out_pipe_pending += size;
1595            debug_assert_eq!(
1596                self.splice_out_pending(),
1597                pending_before + size,
1598                "out_pipe_pending must grow by exactly the spliced bytes"
1599            );
1600            self.frontend_readiness.arm_writable();
1601            count!(names::backend::BACK_BYTES_IN, size as i64);
1602            metrics.backend_bin += size;
1603            debug_assert_eq!(
1604                metrics.backend_bin,
1605                backend_bin_before + size,
1606                "metrics.backend_bin must advance by exactly the spliced bytes"
1607            );
1608        }
1609
1610        if size == 0 && remaining == SocketResult::Closed {
1611            self.backend_status = match self.backend_status {
1612                ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1613                ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1614                s => s,
1615            };
1616
1617            if !self.check_connections() {
1618                self.reset_readiness_for_close();
1619                self.log_request_success(metrics);
1620                return SessionResult::Close;
1621            }
1622        }
1623
1624        match remaining {
1625            SocketResult::Error => {
1626                self.reset_readiness_for_close();
1627                self.log_request_error(metrics, "splice back socket read error");
1628                return SessionResult::Close;
1629            }
1630            SocketResult::Closed => {
1631                if !self.check_connections() {
1632                    self.reset_readiness_for_close();
1633                    self.log_request_success(metrics);
1634                    return SessionResult::Close;
1635                }
1636            }
1637            SocketResult::WouldBlock => {
1638                self.backend_readiness.event.remove(Ready::READABLE);
1639            }
1640            SocketResult::Continue => {}
1641        }
1642
1643        SessionResult::Continue
1644    }
1645
1646    pub fn log_context(&self) -> LogContext<'_> {
1647        LogContext {
1648            session_id: self.session_id,
1649            request_id: Some(self.request_id),
1650            cluster_id: self.cluster_id.as_deref(),
1651            backend_id: self.backend_id.as_deref(),
1652        }
1653    }
1654
1655    fn log_endpoint(&self) -> EndpointRecord<'_> {
1656        match &self.websocket_context {
1657            WebSocketContext::Http {
1658                method,
1659                authority,
1660                path,
1661                status,
1662                reason,
1663            } => EndpointRecord::Http {
1664                method: method.as_deref(),
1665                authority: authority.as_deref(),
1666                path: path.as_deref(),
1667                status: status.to_owned(),
1668                reason: reason.as_deref(),
1669            },
1670            WebSocketContext::Tcp => EndpointRecord::Tcp,
1671        }
1672    }
1673}
1674
1675impl<Front: SocketHandler, L: ListenerHandler> SessionState for Pipe<Front, L> {
1676    fn ready(
1677        &mut self,
1678        _session: Rc<RefCell<dyn crate::ProxySession>>,
1679        _proxy: Rc<RefCell<dyn crate::L7Proxy>>,
1680        metrics: &mut SessionMetrics,
1681    ) -> SessionResult {
1682        let mut counter = 0;
1683
1684        if self.frontend_readiness.event.is_hup() {
1685            return SessionResult::Close;
1686        }
1687
1688        while counter < MAX_LOOP_ITERATIONS {
1689            let frontend_interest = self.frontend_readiness.filter_interest();
1690            let backend_interest = self.backend_readiness.filter_interest();
1691
1692            trace!(
1693                "{} Frontend interest({:?}), backend interest({:?})",
1694                log_context!(self),
1695                frontend_interest,
1696                backend_interest
1697            );
1698            if frontend_interest.is_empty() && backend_interest.is_empty() {
1699                break;
1700            }
1701
1702            if self.backend_readiness.event.is_hup()
1703                && self.frontend_readiness.interest.is_writable()
1704                && !self.frontend_readiness.event.is_writable()
1705            {
1706                break;
1707            }
1708
1709            if frontend_interest.is_readable() && self.readable(metrics) == SessionResult::Close {
1710                return SessionResult::Close;
1711            }
1712
1713            if backend_interest.is_writable()
1714                && self.backend_writable(metrics) == SessionResult::Close
1715            {
1716                return SessionResult::Close;
1717            }
1718
1719            if backend_interest.is_readable()
1720                && self.backend_readable(metrics) == SessionResult::Close
1721            {
1722                return SessionResult::Close;
1723            }
1724
1725            if frontend_interest.is_writable() && self.writable(metrics) == SessionResult::Close {
1726                return SessionResult::Close;
1727            }
1728
1729            if backend_interest.is_hup() && self.backend_hup(metrics) == SessionResult::Close {
1730                return SessionResult::Close;
1731            }
1732
1733            if frontend_interest.is_error() {
1734                error!(
1735                    "{} Frontend socket error, disconnecting",
1736                    log_context!(self)
1737                );
1738
1739                self.frontend_readiness.interest = Ready::EMPTY;
1740                self.backend_readiness.interest = Ready::EMPTY;
1741
1742                return SessionResult::Close;
1743            }
1744
1745            if backend_interest.is_error() && self.backend_hup(metrics) == SessionResult::Close {
1746                self.frontend_readiness.interest = Ready::EMPTY;
1747                self.backend_readiness.interest = Ready::EMPTY;
1748
1749                error!("{} Backend socket error, disconnecting", log_context!(self));
1750                return SessionResult::Close;
1751            }
1752
1753            counter += 1;
1754        }
1755
1756        if counter >= MAX_LOOP_ITERATIONS {
1757            error!(
1758                "{}\tHandling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
1759                log_context!(self),
1760                MAX_LOOP_ITERATIONS
1761            );
1762
1763            incr!(names::http::INFINITE_LOOP_ERROR);
1764            self.print_state(self.protocol_string());
1765
1766            return SessionResult::Close;
1767        }
1768
1769        SessionResult::Continue
1770    }
1771
1772    fn update_readiness(&mut self, token: Token, events: Ready) {
1773        if self.frontend_token == token {
1774            self.frontend_readiness.event |= events;
1775        } else if self.backend_token == Some(token) {
1776            self.backend_readiness.event |= events;
1777        }
1778    }
1779
1780    fn timeout(&mut self, token: Token, metrics: &mut SessionMetrics) -> StateResult {
1781        //info!("got timeout for token: {:?}", token);
1782        if self.frontend_token == token {
1783            self.log_request_timeout(metrics, "frontend socket timeout");
1784            if let Some(timeout) = self.container_frontend_timeout.as_mut() {
1785                timeout.triggered()
1786            }
1787            return StateResult::CloseSession;
1788        }
1789
1790        if self.backend_token == Some(token) {
1791            //info!("backend timeout triggered for token {:?}", token);
1792            if let Some(timeout) = self.container_backend_timeout.as_mut() {
1793                timeout.triggered()
1794            }
1795
1796            self.log_request_timeout(metrics, "backend socket timeout");
1797            return StateResult::CloseSession;
1798        }
1799
1800        error!("{} Got timeout for an invalid token", log_context!(self));
1801        self.log_request_error(metrics, "invalid token timeout");
1802        StateResult::CloseSession
1803    }
1804
1805    fn cancel_timeouts(&mut self) {
1806        self.container_frontend_timeout.as_mut().map(|t| t.cancel());
1807        self.container_backend_timeout.as_mut().map(|t| t.cancel());
1808    }
1809
1810    fn close(&mut self, _proxy: Rc<RefCell<dyn L7Proxy>>, _metrics: &mut SessionMetrics) {
1811        if let Some(backend) = self.backend.as_mut() {
1812            let mut backend = backend.borrow_mut();
1813            backend.active_requests = backend.active_requests.saturating_sub(1);
1814        }
1815    }
1816
1817    fn print_state(&self, context: &str) {
1818        error!(
1819            "\
1820{} {} Session(Pipe)
1821\tFrontend:
1822\t\ttoken: {:?}\treadiness: {:?}
1823\tBackend:
1824\t\ttoken: {:?}\treadiness: {:?}",
1825            log_context!(self),
1826            context,
1827            self.frontend_token,
1828            self.frontend_readiness,
1829            self.backend_token,
1830            self.backend_readiness
1831        );
1832    }
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837    use std::{
1838        collections::BTreeMap,
1839        io::Write,
1840        net::{TcpListener as StdTcpListener, TcpStream as StdTcpStream},
1841        time::Duration,
1842    };
1843
1844    use super::*;
1845    use crate::pool::Pool;
1846
1847    struct TestListener {
1848        address: SocketAddr,
1849    }
1850
1851    impl ListenerHandler for TestListener {
1852        fn get_addr(&self) -> &SocketAddr {
1853            &self.address
1854        }
1855
1856        fn get_tags(&self, _key: &str) -> Option<&sozu_command::logging::CachedTags> {
1857            None
1858        }
1859
1860        fn set_tags(&mut self, _key: String, _tags: Option<BTreeMap<String, String>>) {}
1861
1862        fn protocol(&self) -> Protocol {
1863            Protocol::HTTP
1864        }
1865
1866        fn public_address(&self) -> SocketAddr {
1867            self.address
1868        }
1869    }
1870
1871    fn connected_pair() -> (StdTcpStream, StdTcpStream) {
1872        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
1873        let address = listener.local_addr().expect("listener local addr");
1874        let client = StdTcpStream::connect(address).expect("connect test client");
1875        let (server, _) = listener.accept().expect("accept test server");
1876        client.set_nonblocking(true).expect("client nonblocking");
1877        server.set_nonblocking(true).expect("server nonblocking");
1878        (client, server)
1879    }
1880
1881    #[test]
1882    fn backend_readable_arms_frontend_writable_event_when_buffering_response() {
1883        let (frontend_peer, frontend_socket) = connected_pair();
1884        let (mut backend_peer, backend_socket) = connected_pair();
1885
1886        let mut pool = Pool::with_capacity(2, 2, 4096);
1887        let backend_buffer = pool.checkout().expect("backend buffer");
1888        let frontend_buffer = pool.checkout().expect("frontend buffer");
1889        let address = "127.0.0.1:0".parse().expect("test address");
1890        let listener = Rc::new(RefCell::new(TestListener { address }));
1891
1892        let mut pipe = Pipe::new(
1893            backend_buffer,
1894            None,
1895            Some(TcpStream::from_std(backend_socket)),
1896            None,
1897            None,
1898            None,
1899            None,
1900            frontend_buffer,
1901            Token(0),
1902            TcpStream::from_std(frontend_socket),
1903            listener,
1904            Protocol::HTTP,
1905            Ulid::generate(),
1906            Ulid::generate(),
1907            None,
1908            WebSocketContext::Tcp,
1909        );
1910        pipe.set_back_token(Token(1));
1911        pipe.frontend_readiness.event = Ready::EMPTY;
1912        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1913        pipe.backend_readiness.event = Ready::READABLE;
1914        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1915
1916        backend_peer
1917            .write_all(b"server-speaks-first")
1918            .expect("write backend payload");
1919
1920        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1921        assert_eq!(pipe.backend_readable(&mut metrics), SessionResult::Continue);
1922
1923        assert!(
1924            pipe.backend_buffer.available_data() > 0,
1925            "backend_readable must buffer backend bytes"
1926        );
1927        assert!(
1928            pipe.frontend_readiness.interest.is_writable(),
1929            "buffered backend bytes must arm frontend WRITABLE interest"
1930        );
1931        assert!(
1932            pipe.frontend_readiness.event.is_writable(),
1933            "buffered backend bytes must queue a frontend WRITABLE event"
1934        );
1935
1936        drop(frontend_peer);
1937    }
1938
1939    #[test]
1940    fn frontend_readable_arms_backend_writable_event_when_buffering_request() {
1941        let (mut frontend_peer, frontend_socket) = connected_pair();
1942        let (backend_peer, backend_socket) = connected_pair();
1943
1944        let mut pool = Pool::with_capacity(2, 2, 4096);
1945        let backend_buffer = pool.checkout().expect("backend buffer");
1946        let frontend_buffer = pool.checkout().expect("frontend buffer");
1947        let address = "127.0.0.1:0".parse().expect("test address");
1948        let listener = Rc::new(RefCell::new(TestListener { address }));
1949
1950        let mut pipe = Pipe::new(
1951            backend_buffer,
1952            None,
1953            Some(TcpStream::from_std(backend_socket)),
1954            None,
1955            None,
1956            None,
1957            None,
1958            frontend_buffer,
1959            Token(0),
1960            TcpStream::from_std(frontend_socket),
1961            listener,
1962            Protocol::HTTP,
1963            Ulid::generate(),
1964            Ulid::generate(),
1965            None,
1966            WebSocketContext::Tcp,
1967        );
1968        pipe.set_back_token(Token(1));
1969        pipe.frontend_readiness.event = Ready::READABLE;
1970        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1971        pipe.backend_readiness.event = Ready::EMPTY;
1972        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1973
1974        frontend_peer
1975            .write_all(b"client-speaks-after-upgrade")
1976            .expect("write frontend payload");
1977
1978        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1979        assert_eq!(pipe.readable(&mut metrics), SessionResult::Continue);
1980
1981        assert!(
1982            pipe.frontend_buffer.available_data() > 0,
1983            "readable must buffer frontend bytes"
1984        );
1985        assert!(
1986            pipe.backend_readiness.interest.is_writable(),
1987            "buffered frontend bytes must arm backend WRITABLE interest"
1988        );
1989        assert!(
1990            pipe.backend_readiness.event.is_writable(),
1991            "buffered frontend bytes must queue a backend WRITABLE event"
1992        );
1993
1994        drop(backend_peer);
1995    }
1996
1997    #[test]
1998    fn restore_readiness_events_rearms_inherited_buffered_writes() {
1999        let (frontend_peer, frontend_socket) = connected_pair();
2000        let (backend_peer, backend_socket) = connected_pair();
2001
2002        let mut pool = Pool::with_capacity(2, 2, 4096);
2003        let mut backend_buffer = pool.checkout().expect("backend buffer");
2004        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2005        backend_buffer
2006            .write_all(b"backend bytes inherited from 101 read")
2007            .expect("write backend buffer");
2008        frontend_buffer
2009            .write_all(b"frontend bytes inherited from upgrade read")
2010            .expect("write frontend buffer");
2011        let address = "127.0.0.1:0".parse().expect("test address");
2012        let listener = Rc::new(RefCell::new(TestListener { address }));
2013
2014        let mut pipe = Pipe::new(
2015            backend_buffer,
2016            None,
2017            Some(TcpStream::from_std(backend_socket)),
2018            None,
2019            None,
2020            None,
2021            None,
2022            frontend_buffer,
2023            Token(0),
2024            TcpStream::from_std(frontend_socket),
2025            listener,
2026            Protocol::HTTP,
2027            Ulid::generate(),
2028            Ulid::generate(),
2029            None,
2030            WebSocketContext::Tcp,
2031        );
2032
2033        pipe.restore_readiness_events(Ready::EMPTY, Ready::EMPTY);
2034
2035        assert!(
2036            pipe.frontend_readiness.event.is_writable(),
2037            "restoring inherited frontend events must not park backend-buffered bytes"
2038        );
2039        assert!(
2040            pipe.backend_readiness.event.is_writable(),
2041            "restoring inherited backend events must not park frontend-buffered bytes"
2042        );
2043
2044        drop(frontend_peer);
2045        drop(backend_peer);
2046    }
2047
2048    /// Regression guard for the splice/preread interaction: when `Pipe` is
2049    /// constructed with the splice fast path available (`Protocol::TCP`)
2050    /// AND a non-empty inherited `frontend_buffer` (the SNI-preread
2051    /// ClientHello replay scenario), `backend_writable` must drain those
2052    /// buffered bytes to the backend socket through the normal buffered
2053    /// path first. Without the gate in `backend_writable`, this call would
2054    /// dispatch straight to `splice_backend_writable`, which only drains the
2055    /// kernel pipe (`splice_in_pending`) — 0 here — and returns immediately
2056    /// without ever touching `frontend_buffer`, silently dropping the
2057    /// preread bytes.
2058    #[cfg(all(target_os = "linux", feature = "splice"))]
2059    #[test]
2060    fn backend_writable_drains_inherited_frontend_buffer_before_splice_engages() {
2061        use std::io::Read;
2062
2063        let (frontend_peer, frontend_socket) = connected_pair();
2064        let (mut backend_peer, backend_socket) = connected_pair();
2065
2066        let mut pool = Pool::with_capacity(2, 2, 4096);
2067        let backend_buffer = pool.checkout().expect("backend buffer");
2068        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2069        frontend_buffer
2070            .write_all(b"inherited-preread-client-hello")
2071            .expect("write inherited frontend buffer");
2072        let address = "127.0.0.1:0".parse().expect("test address");
2073        let listener = Rc::new(RefCell::new(TestListener { address }));
2074
2075        let mut pipe = Pipe::new(
2076            backend_buffer,
2077            None,
2078            Some(TcpStream::from_std(backend_socket)),
2079            None,
2080            None,
2081            None,
2082            None,
2083            frontend_buffer,
2084            Token(0),
2085            TcpStream::from_std(frontend_socket),
2086            listener,
2087            Protocol::TCP,
2088            Ulid::generate(),
2089            Ulid::generate(),
2090            None,
2091            WebSocketContext::Tcp,
2092        );
2093        pipe.set_back_token(Token(1));
2094
2095        assert!(
2096            pipe.splice_pipe.is_some(),
2097            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
2098        );
2099        assert!(
2100            pipe.frontend_buffer.available_data() > 0,
2101            "test setup must inherit a non-empty frontend buffer"
2102        );
2103
2104        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2105        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Continue);
2106
2107        assert_eq!(
2108            pipe.frontend_buffer.available_data(),
2109            0,
2110            "inherited preread bytes must drain through the buffered path before splice engages"
2111        );
2112
2113        let mut received = [0u8; 64];
2114        let n = backend_peer
2115            .read(&mut received)
2116            .expect("backend socket must have received the drained preread bytes");
2117        assert_eq!(&received[..n], b"inherited-preread-client-hello");
2118
2119        drop(frontend_peer);
2120    }
2121
2122    /// Regression guard for the close-before-flush data loss
2123    /// (sozu-proxy/sozu#1279): when the frontend read side reaches
2124    /// EOF while `frontend_buffer` still holds bytes queued for the backend,
2125    /// `readable` must NOT return `Close` (which discarded them, silently
2126    /// truncating the stream -- the reproducer was a payload coalesced with an
2127    /// SNI ClientHello). It must keep the session alive to drain, then the
2128    /// queued bytes must reach the backend and only then may the session
2129    /// close.
2130    #[test]
2131    fn frontend_eof_flushes_queued_backend_bytes_before_closing() {
2132        use std::io::{Read, Write};
2133
2134        let (frontend_peer, frontend_socket) = connected_pair();
2135        let (mut backend_peer, backend_socket) = connected_pair();
2136
2137        let mut pool = Pool::with_capacity(2, 2, 4096);
2138        let backend_buffer = pool.checkout().expect("backend buffer");
2139        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2140        // Bytes the frontend already delivered, still queued for the backend.
2141        frontend_buffer
2142            .write_all(b"front-to-back-tail-still-queued")
2143            .expect("seed queued frontend bytes");
2144        let address = "127.0.0.1:0".parse().expect("test address");
2145        let listener = Rc::new(RefCell::new(TestListener { address }));
2146
2147        let mut pipe = Pipe::new(
2148            backend_buffer,
2149            None,
2150            Some(TcpStream::from_std(backend_socket)),
2151            None,
2152            None,
2153            None,
2154            None,
2155            frontend_buffer,
2156            Token(0),
2157            TcpStream::from_std(frontend_socket),
2158            listener,
2159            Protocol::TCP,
2160            Ulid::generate(),
2161            Ulid::generate(),
2162            None,
2163            WebSocketContext::Tcp,
2164        );
2165        pipe.set_back_token(Token(1));
2166        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2167        pipe.frontend_readiness.event = Ready::READABLE;
2168
2169        // The frontend closes right after delivering its bytes (no gap): the
2170        // pipe reads EOF while `frontend_buffer` is still non-empty.
2171        drop(frontend_peer);
2172
2173        // Nonblocking EOF is not observable on the very first read on every
2174        // platform, so retry `readable` (bounded, no sleep) until it observes
2175        // the close. `readable` must never report `Close` while bytes remain
2176        // queued.
2177        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2178        let mut saw_eof = false;
2179        for _ in 0..100_000 {
2180            let result = pipe.readable(&mut metrics);
2181            assert_ne!(
2182                result,
2183                SessionResult::Close,
2184                "readable must not close while {} queued backend bytes remain",
2185                pipe.frontend_buffer.available_data()
2186            );
2187            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2188                saw_eof = true;
2189                break;
2190            }
2191        }
2192        assert!(
2193            saw_eof,
2194            "frontend EOF was never observed within the retry budget"
2195        );
2196        assert!(
2197            pipe.frontend_buffer.available_data() > 0,
2198            "the queued backend bytes must survive the frontend EOF, not be dropped"
2199        );
2200
2201        // Draining now delivers every queued byte to the backend. Nothing
2202        // else is in flight (backend still `Normal`) and the frontend read
2203        // side already hit EOF (`WriteOpen`), so `backend_writable` closes
2204        // the session itself as soon as the drain completes -- it does not
2205        // wait for a follow-up event that edge-triggered epoll would never
2206        // deliver (sozu-proxy/sozu#1290).
2207        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Close);
2208        assert_eq!(
2209            pipe.frontend_buffer.available_data(),
2210            0,
2211            "the queued bytes must all drain to the backend after EOF"
2212        );
2213
2214        let mut received = Vec::new();
2215        // Read until we have the full payload (a single read may segment).
2216        for _ in 0..100_000 {
2217            let mut buf = [0u8; 64];
2218            match backend_peer.read(&mut buf) {
2219                Ok(0) => break,
2220                Ok(n) => {
2221                    received.extend_from_slice(&buf[..n]);
2222                    if received.len() >= b"front-to-back-tail-still-queued".len() {
2223                        break;
2224                    }
2225                }
2226                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2227                Err(_) => break,
2228            }
2229        }
2230        assert_eq!(
2231            received, b"front-to-back-tail-still-queued",
2232            "the backend must receive the queued tail byte-for-byte, not a truncation"
2233        );
2234    }
2235
2236    /// Regression guard for the missing completion edge on the deferred
2237    /// close (sozu-proxy/sozu#1290): the test above proves the queued bytes
2238    /// survive the frontend's EOF; this proves the other half of the fix --
2239    /// that draining those bytes closes the session immediately, from that
2240    /// single `backend_writable` event, with no extra event injected by the
2241    /// test. Before the fix, `backend_writable`'s drained-buffer early
2242    /// return always reported `Continue`, even with the frontend already at
2243    /// EOF and nothing else in flight: edge-triggered epoll had already
2244    /// consumed the READABLE/HUP edge that signalled the EOF, so no other
2245    /// event existed to hang a follow-up close decision on and the session
2246    /// sat resident until an unrelated event or timeout.
2247    #[test]
2248    fn frontend_eof_with_queued_bytes_closes_on_the_draining_backend_writable_call() {
2249        use std::io::Read;
2250
2251        let (mut frontend_peer, frontend_socket) = connected_pair();
2252        let (mut backend_peer, backend_socket) = connected_pair();
2253
2254        let mut pool = Pool::with_capacity(2, 2, 4096);
2255        let backend_buffer = pool.checkout().expect("backend buffer");
2256        let frontend_buffer = pool.checkout().expect("frontend buffer");
2257        let address = "127.0.0.1:0".parse().expect("test address");
2258        let listener = Rc::new(RefCell::new(TestListener { address }));
2259
2260        let mut pipe = Pipe::new(
2261            backend_buffer,
2262            None,
2263            Some(TcpStream::from_std(backend_socket)),
2264            None,
2265            None,
2266            None,
2267            None,
2268            frontend_buffer,
2269            Token(0),
2270            TcpStream::from_std(frontend_socket),
2271            listener,
2272            Protocol::HTTP,
2273            Ulid::generate(),
2274            Ulid::generate(),
2275            None,
2276            WebSocketContext::Tcp,
2277        );
2278        pipe.set_back_token(Token(1));
2279        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2280        pipe.frontend_readiness.event = Ready::READABLE;
2281
2282        // The frontend delivers its bytes then closes right away -- no gap
2283        // -- so the real `readable()` call below observes EOF while bytes
2284        // are still queued in `frontend_buffer`.
2285        frontend_peer
2286            .write_all(b"queued-request-tail")
2287            .expect("write frontend payload");
2288        drop(frontend_peer);
2289
2290        // Drive the REAL ready pass: keep calling `readable` (bounded, no
2291        // sleep) until it observes the close, exactly like the event loop
2292        // would after mio reports the frontend's HUP/EOF.
2293        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2294        let mut saw_eof = false;
2295        for _ in 0..100_000 {
2296            let result = pipe.readable(&mut metrics);
2297            assert_ne!(
2298                result,
2299                SessionResult::Close,
2300                "readable must not close while queued backend bytes remain"
2301            );
2302            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2303                saw_eof = true;
2304                break;
2305            }
2306        }
2307        assert!(
2308            saw_eof,
2309            "frontend EOF was never observed within the retry budget"
2310        );
2311        assert!(
2312            pipe.frontend_buffer.available_data() > 0,
2313            "the queued bytes must survive the frontend EOF"
2314        );
2315        assert!(
2316            pipe.backend_readiness.interest.is_writable()
2317                && pipe.backend_readiness.event.is_writable(),
2318            "observing EOF with queued bytes must arm backend WRITABLE to drain them"
2319        );
2320
2321        // The backend becomes writable exactly once -- the real event the
2322        // arming above promises -- and drains everything in that single
2323        // call. No extra readable()/frontend_hup()/check_connections() call
2324        // is injected here: the session must close from THIS event alone.
2325        let result = pipe.backend_writable(&mut metrics);
2326
2327        assert_eq!(
2328            result,
2329            SessionResult::Close,
2330            "the single draining backend_writable call must close the session by itself -- \
2331             edge-triggered epoll already consumed the EOF edge and offers no other event to \
2332             hang the teardown on"
2333        );
2334        assert_eq!(
2335            pipe.frontend_buffer.available_data(),
2336            0,
2337            "the queued bytes must have fully drained before the session closed"
2338        );
2339
2340        let mut received = Vec::new();
2341        for _ in 0..100_000 {
2342            let mut buf = [0u8; 64];
2343            match backend_peer.read(&mut buf) {
2344                Ok(0) => break,
2345                Ok(n) => {
2346                    received.extend_from_slice(&buf[..n]);
2347                    if received.len() >= b"queued-request-tail".len() {
2348                        break;
2349                    }
2350                }
2351                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2352                Err(_) => break,
2353            }
2354        }
2355        assert_eq!(
2356            received, b"queued-request-tail",
2357            "the backend must still receive the queued bytes byte-for-byte before the close"
2358        );
2359    }
2360
2361    /// Regression guard for the HUP-path sibling of the close-before-flush
2362    /// data loss (sozu-proxy/sozu#1290): `command/src/ready.rs`'s
2363    /// `From<&mio::event::Event>` sets `Ready::HUP` whenever
2364    /// `is_read_closed()`/`is_write_closed()` fires, independently of
2365    /// `Ready::READABLE` -- so a client FIN that coalesces with the payload
2366    /// tail on a loaded event loop delivers a SINGLE epoll batch carrying
2367    /// BOTH bits. `frontend_hup` must not drop bytes still queued in
2368    /// `frontend_buffer` (already read) or still pending in the kernel
2369    /// receive buffer (signalled by the retained READABLE event) just
2370    /// because HUP also fired.
2371    #[test]
2372    fn frontend_hup_drains_inflight_request_bytes_before_closing() {
2373        let (frontend_peer, frontend_socket) = connected_pair();
2374        let (_backend_peer, backend_socket) = connected_pair();
2375
2376        let mut pool = Pool::with_capacity(2, 2, 4096);
2377        let backend_buffer = pool.checkout().expect("backend buffer");
2378        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2379        frontend_buffer
2380            .write_all(b"front-to-back-tail-still-queued")
2381            .expect("seed queued frontend bytes");
2382        let address = "127.0.0.1:0".parse().expect("test address");
2383        let listener = Rc::new(RefCell::new(TestListener { address }));
2384
2385        let mut pipe = Pipe::new(
2386            backend_buffer,
2387            None,
2388            Some(TcpStream::from_std(backend_socket)),
2389            None,
2390            None,
2391            None,
2392            None,
2393            frontend_buffer,
2394            Token(0),
2395            TcpStream::from_std(frontend_socket),
2396            listener,
2397            Protocol::TCP,
2398            Ulid::generate(),
2399            Ulid::generate(),
2400            None,
2401            WebSocketContext::Tcp,
2402        );
2403        pipe.set_back_token(Token(1));
2404        // The kernel receive buffer still has a tail behind the FIN: both
2405        // bits set in the same batch, exactly as `ready.rs` would produce.
2406        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2407        pipe.frontend_readiness.event = Ready::READABLE | Ready::HUP;
2408        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2409        pipe.backend_readiness.event = Ready::EMPTY;
2410
2411        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2412        let result = pipe.frontend_hup(&mut metrics);
2413
2414        assert_eq!(
2415            result,
2416            SessionResult::Continue,
2417            "frontend_hup must keep the session alive while request bytes are still in flight"
2418        );
2419        assert!(
2420            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2421            "frontend_hup must still mark the frontend Closed even on the drain branch"
2422        );
2423        assert!(
2424            pipe.backend_readiness.interest.is_writable()
2425                && pipe.backend_readiness.event.is_writable(),
2426            "the drain branch must arm backend WRITABLE to flush the queued frontend bytes"
2427        );
2428        assert!(
2429            pipe.frontend_readiness.interest.is_readable(),
2430            "the drain branch must retain frontend READABLE interest to read the kernel tail to EOF"
2431        );
2432
2433        drop(frontend_peer);
2434    }
2435
2436    /// Sibling of the above: when nothing is in flight (no buffered bytes,
2437    /// no pending READABLE event), `frontend_hup` keeps its pre-existing
2438    /// behavior of closing immediately -- there is nothing left to drain.
2439    #[test]
2440    fn frontend_hup_closes_immediately_when_nothing_is_inflight() {
2441        let (frontend_peer, frontend_socket) = connected_pair();
2442        let (_backend_peer, backend_socket) = connected_pair();
2443
2444        let mut pool = Pool::with_capacity(2, 2, 4096);
2445        let backend_buffer = pool.checkout().expect("backend buffer");
2446        let frontend_buffer = pool.checkout().expect("frontend buffer");
2447        let address = "127.0.0.1:0".parse().expect("test address");
2448        let listener = Rc::new(RefCell::new(TestListener { address }));
2449
2450        let mut pipe = Pipe::new(
2451            backend_buffer,
2452            None,
2453            Some(TcpStream::from_std(backend_socket)),
2454            None,
2455            None,
2456            None,
2457            None,
2458            frontend_buffer,
2459            Token(0),
2460            TcpStream::from_std(frontend_socket),
2461            listener,
2462            Protocol::TCP,
2463            Ulid::generate(),
2464            Ulid::generate(),
2465            None,
2466            WebSocketContext::Tcp,
2467        );
2468        pipe.set_back_token(Token(1));
2469        // HUP only, no readable event, no queued bytes: nothing to drain.
2470        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2471        pipe.frontend_readiness.event = Ready::HUP;
2472        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2473        pipe.backend_readiness.event = Ready::EMPTY;
2474
2475        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2476        let result = pipe.frontend_hup(&mut metrics);
2477
2478        assert_eq!(
2479            result,
2480            SessionResult::Close,
2481            "frontend_hup must close immediately when nothing is in flight (unchanged legacy behavior)"
2482        );
2483        assert!(
2484            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2485            "frontend_hup must mark the frontend Closed"
2486        );
2487        assert!(
2488            !pipe.backend_readiness.event.is_writable(),
2489            "the close branch must not arm backend WRITABLE when there is nothing queued"
2490        );
2491
2492        drop(frontend_peer);
2493    }
2494
2495    /// Regression guard for the SPLICE sibling of the close-before-flush
2496    /// data loss (sozu-proxy/sozu#1279 / #1290): `splice_readable`'s
2497    /// `SocketResult::Closed` arm used to return `Close` unconditionally,
2498    /// dropping whatever `splice_in_pending()` bytes sat in the kernel
2499    /// `in_pipe` when the frontend's FIN was observed. It must instead keep
2500    /// the session alive until `splice_backend_writable` drains the kernel
2501    /// pipe, then close on the next EOF re-observation once nothing is
2502    /// inflight.
2503    #[cfg(all(target_os = "linux", feature = "splice"))]
2504    #[test]
2505    fn splice_readable_eof_drains_kernel_pipe_bytes_before_closing() {
2506        use std::io::Read;
2507
2508        let (mut frontend_peer, frontend_socket) = connected_pair();
2509        let (mut backend_peer, backend_socket) = connected_pair();
2510
2511        let mut pool = Pool::with_capacity(2, 2, 4096);
2512        let backend_buffer = pool.checkout().expect("backend buffer");
2513        // The frontend buffer stays EMPTY: the splice fast path only engages
2514        // when no inherited userspace bytes remain (see `readable`'s gate).
2515        let frontend_buffer = pool.checkout().expect("frontend buffer");
2516        let address = "127.0.0.1:0".parse().expect("test address");
2517        let listener = Rc::new(RefCell::new(TestListener { address }));
2518
2519        let mut pipe = Pipe::new(
2520            backend_buffer,
2521            None,
2522            Some(TcpStream::from_std(backend_socket)),
2523            None,
2524            None,
2525            None,
2526            None,
2527            frontend_buffer,
2528            Token(0),
2529            TcpStream::from_std(frontend_socket),
2530            listener,
2531            Protocol::TCP,
2532            Ulid::generate(),
2533            Ulid::generate(),
2534            None,
2535            WebSocketContext::Tcp,
2536        );
2537        pipe.set_back_token(Token(1));
2538        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2539        pipe.frontend_readiness.event = Ready::READABLE;
2540
2541        assert!(
2542            pipe.splice_pipe.is_some(),
2543            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
2544        );
2545
2546        // 32 KiB: below the 64 KiB default pipe capacity so the whole
2547        // payload fits the kernel in_pipe without backpressure pauses.
2548        let payload = vec![0x5a_u8; 32 * 1024];
2549        // Environment precondition: once this user's pipe buffers exceed
2550        // `fs.pipe-user-pages-soft`, Linux creates every new pipe with 2 pages
2551        // (8 KiB) and refuses `F_SETPIPE_SZ` with EPERM, so `splice_readable`
2552        // parks at its capacity guard and the payload can never sit whole in
2553        // the in_pipe. Fail loudly and name the host limit rather than report
2554        // a false regression.
2555        let capacity = pipe.splice_capacity();
2556        assert!(
2557            capacity >= payload.len(),
2558            "environment precondition not met: splice pipe capacity is {capacity} bytes, \
2559             below the {}-byte payload; this user's pipe buffers likely exceed \
2560             fs.pipe-user-pages-soft ({}). Close pipe-heavy processes or raise the limit, \
2561             then rerun: this is a host limit, not a Sozu regression",
2562            payload.len(),
2563            std::fs::read_to_string("/proc/sys/fs/pipe-user-pages-soft")
2564                .map(|limit| limit.trim().to_owned())
2565                .unwrap_or_else(|error| format!("unreadable: {error}")),
2566        );
2567        frontend_peer
2568            .write_all(&payload)
2569            .expect("write frontend payload");
2570
2571        // Splice the payload into the kernel in_pipe (bounded retries:
2572        // loopback delivers the payload in several chunks, and an early
2573        // call can observe WouldBlock).
2574        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2575        for _ in 0..100_000 {
2576            if pipe.splice_in_pending() >= payload.len() {
2577                break;
2578            }
2579            let result = pipe.splice_readable(&mut metrics);
2580            assert_ne!(
2581                result,
2582                SessionResult::Close,
2583                "splice_readable must not close while splicing the payload in"
2584            );
2585        }
2586        assert_eq!(
2587            pipe.splice_in_pending(),
2588            payload.len(),
2589            "the whole payload must sit in the kernel in_pipe before EOF"
2590        );
2591
2592        // The frontend closes right after its payload: FIN behind the
2593        // spliced bytes.
2594        drop(frontend_peer);
2595
2596        // Keep calling until EOF is observed (WriteOpen transition). The
2597        // buggy arm returned `Close` here, dropping the kernel-pipe bytes.
2598        let mut saw_eof = false;
2599        for _ in 0..100_000 {
2600            let result = pipe.splice_readable(&mut metrics);
2601            assert_ne!(
2602                result,
2603                SessionResult::Close,
2604                "splice_readable must not close while {} kernel-pipe bytes remain",
2605                pipe.splice_in_pending()
2606            );
2607            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2608                saw_eof = true;
2609                break;
2610            }
2611        }
2612        assert!(
2613            saw_eof,
2614            "frontend EOF was never observed within the retry budget"
2615        );
2616        assert_eq!(
2617            pipe.splice_in_pending(),
2618            payload.len(),
2619            "the kernel-pipe bytes must survive the frontend EOF, not be dropped"
2620        );
2621        assert!(
2622            pipe.backend_readiness.interest.is_writable()
2623                && pipe.backend_readiness.event.is_writable(),
2624            "EOF with kernel-pipe bytes pending must arm backend WRITABLE to drain them"
2625        );
2626
2627        // Drain the kernel pipe to the backend and read the peer: the bytes
2628        // must arrive byte-identical (interleave drain + read so a full
2629        // backend socket buffer cannot deadlock the loop). Once the kernel
2630        // pipe is fully drained with the frontend already at EOF, the fix
2631        // under test (sozu-proxy/sozu#1290) closes the session as part of
2632        // that same drain call -- so `drain == Close` is now expected, but
2633        // ONLY once nothing is left pending; closing any earlier would be
2634        // the mid-flight truncation this test guards against. Keep reading
2635        // from `backend_peer` regardless of the session's reported status:
2636        // the bytes were already handed off to the kernel by `splice_out`,
2637        // independently of whether `Pipe` still considers itself open.
2638        let mut received = Vec::with_capacity(payload.len());
2639        let mut closed_during_drain = false;
2640        for _ in 0..100_000 {
2641            let drain = pipe.splice_backend_writable(&mut metrics);
2642            if drain == SessionResult::Close {
2643                assert_eq!(
2644                    pipe.splice_in_pending(),
2645                    0,
2646                    "the session must only close once the kernel pipe is fully drained, \
2647                     never mid-flight"
2648                );
2649                closed_during_drain = true;
2650            }
2651            let mut buf = [0u8; 16384];
2652            match backend_peer.read(&mut buf) {
2653                Ok(0) => break,
2654                Ok(n) => received.extend_from_slice(&buf[..n]),
2655                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
2656                Err(_) => break,
2657            }
2658            if received.len() >= payload.len() {
2659                break;
2660            }
2661        }
2662        assert_eq!(
2663            received, payload,
2664            "the backend must receive the spliced tail byte-for-byte, not a truncation"
2665        );
2666        assert_eq!(
2667            pipe.splice_in_pending(),
2668            0,
2669            "the kernel in_pipe must be fully drained"
2670        );
2671        assert!(
2672            closed_during_drain,
2673            "the fix under test (sozu-proxy/sozu#1290) must close the session as soon as \
2674             the kernel pipe drains, without waiting for a separate re-observation event"
2675        );
2676
2677        // Nothing inflight and the frontend is half-closed: the session was
2678        // already closed above, during the drain itself. Re-observing EOF
2679        // here proves that closed state is stable under a repeat call,
2680        // rather than being the sole mechanism that closes the session (as
2681        // it was before the fix).
2682        assert!(
2683            !pipe.check_connections(),
2684            "with nothing inflight and the frontend closed, the session must be closeable"
2685        );
2686        assert_eq!(
2687            pipe.splice_readable(&mut metrics),
2688            SessionResult::Close,
2689            "re-observing EOF with nothing inflight must (still) close the session"
2690        );
2691    }
2692
2693    /// Regression guard for the splice fast path: `splice(2)` from a socket
2694    /// into a pipe returns `EAGAIN` both when the *socket* has nothing left
2695    /// to read AND when the *pipe* has no free slot. A pipe holds
2696    /// `capacity / PAGE_SIZE` slots, every spliced skb fragment occupies one
2697    /// slot whatever its length, and a partially drained slot stays occupied.
2698    /// So the slots run out while `out_pipe_pending < capacity` (MSS-sized
2699    /// segments from a remote backend: 16 slots x ~1400 B, far below 64 KiB).
2700    /// Treating that `EAGAIN` as a drained socket clears the READABLE event;
2701    /// edge-triggered epoll never re-signals bytes already queued in the
2702    /// socket, so the tail of the response sits in the kernel recv queue
2703    /// forever and the client hangs at the end of the transfer.
2704    ///
2705    /// The test shrinks the pipe to a single 4 KiB slot and drains one byte
2706    /// of it so the slot is still held: the next `splice_in` hits `EAGAIN`
2707    /// with bytes still queued in the backend socket.
2708    #[cfg(all(target_os = "linux", feature = "splice"))]
2709    #[test]
2710    fn splice_backend_readable_keeps_readable_event_when_pipe_slots_are_exhausted() {
2711        use std::os::unix::io::AsRawFd;
2712
2713        fn unread_bytes(socket: &TcpStream) -> usize {
2714            let mut n: libc::c_int = 0;
2715            let ret = unsafe { libc::ioctl(socket.as_raw_fd(), libc::FIONREAD, &mut n) };
2716            assert_eq!(ret, 0, "FIONREAD must succeed");
2717            n as usize
2718        }
2719
2720        let (_frontend_peer, frontend_socket) = connected_pair();
2721        let (mut backend_peer, backend_socket) = connected_pair();
2722
2723        let mut pool = Pool::with_capacity(2, 2, 4096);
2724        let backend_buffer = pool.checkout().expect("backend buffer");
2725        let frontend_buffer = pool.checkout().expect("frontend buffer");
2726        let address = "127.0.0.1:0".parse().expect("test address");
2727        let listener = Rc::new(RefCell::new(TestListener { address }));
2728
2729        let mut pipe = Pipe::new(
2730            backend_buffer,
2731            None,
2732            Some(TcpStream::from_std(backend_socket)),
2733            None,
2734            None,
2735            None,
2736            None,
2737            frontend_buffer,
2738            Token(0),
2739            TcpStream::from_std(frontend_socket),
2740            listener,
2741            Protocol::TCP,
2742            Ulid::generate(),
2743            Ulid::generate(),
2744            None,
2745            WebSocketContext::Tcp,
2746        );
2747        pipe.set_back_token(Token(1));
2748        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2749        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2750        pipe.backend_readiness.event = Ready::READABLE;
2751
2752        // Shrink both kernel pipes to a single page: one slot each.
2753        let slot = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
2754        assert!(slot > 0, "sysconf(_SC_PAGESIZE) must succeed");
2755        {
2756            let splice_pipe = pipe
2757                .splice_pipe
2758                .as_mut()
2759                .expect("Protocol::TCP must allocate the splice kernel pipe");
2760            for fd in [splice_pipe.in_pipe[0], splice_pipe.out_pipe[0]] {
2761                let ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, slot as libc::c_int) };
2762                assert_eq!(ret, slot as libc::c_int, "F_SETPIPE_SZ(page) must succeed");
2763            }
2764            splice_pipe.capacity = slot;
2765        }
2766        let capacity = pipe.splice_capacity();
2767        assert_eq!(capacity, slot);
2768
2769        // Two slots' worth of response bytes queued in the backend socket.
2770        let payload = vec![0x5a_u8; 2 * slot];
2771        backend_peer
2772            .write_all(&payload)
2773            .expect("write backend payload");
2774        for _ in 0..1000 {
2775            if unread_bytes(pipe.backend_socket.as_ref().unwrap()) >= payload.len() {
2776                break;
2777            }
2778            std::thread::sleep(Duration::from_millis(1));
2779        }
2780        assert_eq!(
2781            unread_bytes(pipe.backend_socket.as_ref().unwrap()),
2782            payload.len(),
2783            "the whole payload must be queued in the backend socket"
2784        );
2785
2786        // First pass fills the single slot exactly: byte accounting agrees
2787        // with the kernel and the guard `out_pending >= capacity` engages.
2788        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2789        assert_ne!(
2790            pipe.splice_backend_readable(&mut metrics),
2791            SessionResult::Close
2792        );
2793        let first = pipe.splice_out_pending();
2794        assert!(
2795            first > 0 && first <= slot,
2796            "the single slot must absorb part of the payload (got {first})"
2797        );
2798        assert!(
2799            pipe.backend_readiness.event.is_readable(),
2800            "the READABLE event must survive a successful splice"
2801        );
2802
2803        // Partial drain toward the frontend (a short write): the slot is
2804        // still occupied but `out_pipe_pending` is now below `capacity`.
2805        {
2806            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().out_pipe[0];
2807            let (drained, res) = splice::splice_out(pipe_read_end, pipe.frontend.socket_ref(), 1);
2808            assert_eq!((drained, res), (1, SocketResult::Continue));
2809            pipe.splice_pipe.as_mut().unwrap().out_pipe_pending -= drained;
2810        }
2811        assert!(pipe.splice_out_pending() < capacity);
2812
2813        // Second pass: `splice_in` hits EAGAIN because the pipe has no free
2814        // slot, NOT because the socket is drained.
2815        pipe.backend_readiness.interest.insert(Ready::READABLE);
2816        assert_ne!(
2817            pipe.splice_backend_readable(&mut metrics),
2818            SessionResult::Close
2819        );
2820
2821        let still_queued = unread_bytes(pipe.backend_socket.as_ref().unwrap());
2822        assert_eq!(
2823            still_queued,
2824            payload.len() - first,
2825            "precondition: the rest of the payload must still sit in the socket"
2826        );
2827        assert!(
2828            pipe.backend_readiness.event.is_readable(),
2829            "READABLE event must survive a pipe-full EAGAIN: {still_queued} bytes are still \
2830             queued in the backend socket and edge-triggered epoll will never re-signal them"
2831        );
2832        assert!(
2833            !pipe.backend_readiness.interest.is_readable(),
2834            "READABLE interest must be parked while the pipe is full, or the ready loop spins"
2835        );
2836        assert!(
2837            pipe.frontend_readiness.event.is_writable()
2838                && pipe.frontend_readiness.interest.is_writable(),
2839            "the frontend must be armed writable to drain the pipe"
2840        );
2841
2842        // Recovery: draining the pipe toward the frontend re-arms the
2843        // backend READABLE interest; with the event still pending, the next
2844        // pass splices the second slot's worth of bytes out of the socket
2845        // without any new epoll edge.
2846        assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2847        assert_eq!(
2848            pipe.splice_out_pending(),
2849            0,
2850            "the pipe must be fully drained"
2851        );
2852        assert!(
2853            pipe.backend_readiness.interest.is_readable()
2854                && pipe.backend_readiness.event.is_readable(),
2855            "draining the pipe must re-arm the backend read"
2856        );
2857        assert_ne!(
2858            pipe.splice_backend_readable(&mut metrics),
2859            SessionResult::Close
2860        );
2861        assert!(
2862            unread_bytes(pipe.backend_socket.as_ref().unwrap()) < still_queued,
2863            "the remaining response bytes must start leaving the backend socket"
2864        );
2865        assert!(pipe.splice_out_pending() > 0);
2866
2867        // Drain socket -> pipe -> frontend until the socket is empty.
2868        for _ in 0..1000 {
2869            if unread_bytes(pipe.backend_socket.as_ref().unwrap()) == 0 {
2870                break;
2871            }
2872            assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2873            pipe.backend_readiness.interest.insert(Ready::READABLE);
2874            assert_ne!(
2875                pipe.splice_backend_readable(&mut metrics),
2876                SessionResult::Close
2877            );
2878        }
2879        assert_eq!(unread_bytes(pipe.backend_socket.as_ref().unwrap()), 0);
2880        if pipe.splice_out_pending() == 0 {
2881            // Re-create the ambiguous state: one slot's worth in the pipe.
2882            backend_peer.write_all(&payload[..slot]).expect("write");
2883            for _ in 0..1000 {
2884                if unread_bytes(pipe.backend_socket.as_ref().unwrap()) >= slot {
2885                    break;
2886                }
2887                std::thread::sleep(Duration::from_millis(1));
2888            }
2889            pipe.backend_readiness.interest.insert(Ready::READABLE);
2890            pipe.backend_readiness.event.insert(Ready::READABLE);
2891            assert_ne!(
2892                pipe.splice_backend_readable(&mut metrics),
2893                SessionResult::Close
2894            );
2895            assert_eq!(unread_bytes(pipe.backend_socket.as_ref().unwrap()), 0);
2896        }
2897        assert!(pipe.splice_out_pending() > 0);
2898
2899        // Socket empty and pipe non-empty: EAGAIN is now ambiguous again,
2900        // the event stays pending and the interest is parked until the
2901        // next drain, after which the genuine drained-socket EAGAIN (empty
2902        // pipe) clears the event.
2903        pipe.backend_readiness.interest.insert(Ready::READABLE);
2904        assert_ne!(
2905            pipe.splice_backend_readable(&mut metrics),
2906            SessionResult::Close
2907        );
2908        assert!(pipe.backend_readiness.event.is_readable());
2909        assert!(!pipe.backend_readiness.interest.is_readable());
2910        assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2911        assert_eq!(pipe.splice_out_pending(), 0);
2912        assert_ne!(
2913            pipe.splice_backend_readable(&mut metrics),
2914            SessionResult::Close
2915        );
2916        assert!(
2917            !pipe.backend_readiness.event.is_readable(),
2918            "a drained socket with an empty pipe must clear the READABLE event"
2919        );
2920    }
2921
2922    #[cfg(all(target_os = "linux", feature = "splice"))]
2923    fn exercise_splice_readable_pipe_full_recovery(force_growth_refusal: bool) -> bool {
2924        use std::{io::Read, os::unix::io::AsRawFd};
2925
2926        fn unread_bytes(socket: &TcpStream) -> usize {
2927            let mut n: libc::c_int = 0;
2928            let ret = unsafe { libc::ioctl(socket.as_raw_fd(), libc::FIONREAD, &mut n) };
2929            assert_eq!(ret, 0, "FIONREAD must succeed");
2930            n as usize
2931        }
2932
2933        fn discard_in_pipe(pipe: &mut Pipe<TcpStream, TestListener>) {
2934            let pending = pipe.splice_in_pending();
2935            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().in_pipe[0];
2936            let mut discarded = vec![0_u8; pending];
2937            let mut offset = 0;
2938            while offset < pending {
2939                // SAFETY: `pipe_read_end` remains owned by `pipe`, and the
2940                // writable slice covers exactly the bytes still requested.
2941                let read = unsafe {
2942                    libc::read(
2943                        pipe_read_end,
2944                        discarded[offset..].as_mut_ptr().cast(),
2945                        pending - offset,
2946                    )
2947                };
2948                assert!(
2949                    read > 0,
2950                    "read must drain the populated pipe: {}",
2951                    std::io::Error::last_os_error()
2952                );
2953                offset += read as usize;
2954            }
2955            pipe.splice_pipe.as_mut().unwrap().in_pipe_pending = 0;
2956        }
2957
2958        fn request_pipe_capacity(
2959            fd: libc::c_int,
2960            requested: usize,
2961            force_growth_refusal: bool,
2962        ) -> usize {
2963            // SAFETY: `fd` is a live pipe descriptor owned by the test's
2964            // `SplicePipe`; fcntl neither retains the descriptor nor a pointer.
2965            let requested = if force_growth_refusal {
2966                -1
2967            } else {
2968                libc::c_int::try_from(requested).expect("requested pipe size must fit c_int")
2969            };
2970            let set_ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, requested) };
2971            if force_growth_refusal {
2972                assert_eq!(set_ret, -1, "negative pipe growth must be refused");
2973            } else if set_ret < 0 {
2974                eprintln!(
2975                    "F_SETPIPE_SZ({requested}) unavailable for test pipe: {}",
2976                    std::io::Error::last_os_error()
2977                );
2978            }
2979            // SAFETY: same live pipe descriptor as above.
2980            let actual = unsafe { libc::fcntl(fd, libc::F_GETPIPE_SZ) };
2981            assert!(actual > 0, "F_GETPIPE_SZ must succeed");
2982            actual as usize
2983        }
2984
2985        fn set_buffer(socket: &impl AsRawFd, opt: libc::c_int, bytes: libc::c_int) {
2986            let ret = unsafe {
2987                libc::setsockopt(
2988                    socket.as_raw_fd(),
2989                    libc::SOL_SOCKET,
2990                    opt,
2991                    &bytes as *const libc::c_int as *const libc::c_void,
2992                    std::mem::size_of::<libc::c_int>() as libc::socklen_t,
2993                )
2994            };
2995            assert_eq!(ret, 0, "setsockopt must succeed");
2996        }
2997
2998        /// A connected pair whose buffers are pinned tiny *before* the
2999        /// handshake: SO_RCVBUF on the listener (inherited by the accepted
3000        /// socket, so the advertised window starts small) and SO_SNDBUF on
3001        /// the connecting side. Together they bound how many bytes the leg
3002        /// can absorb at once to a few KiB, with no autotuning and no
3003        /// dependence on window-update timing.
3004        fn tiny_backend_pair() -> (StdTcpStream, StdTcpStream) {
3005            let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
3006            set_buffer(&listener, libc::SO_RCVBUF, 4096);
3007            let address = listener.local_addr().expect("listener local addr");
3008            let socket = socket2_connect_with_sndbuf(address, 4096);
3009            let (peer, _) = listener.accept().expect("accept test server");
3010            socket.set_nonblocking(true).expect("client nonblocking");
3011            peer.set_nonblocking(true).expect("server nonblocking");
3012            (peer, socket)
3013        }
3014
3015        fn socket2_connect_with_sndbuf(address: SocketAddr, bytes: libc::c_int) -> StdTcpStream {
3016            use std::os::unix::io::FromRawFd;
3017            let fd =
3018                unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0) };
3019            assert!(fd >= 0, "socket() must succeed");
3020            let socket = unsafe { StdTcpStream::from_raw_fd(fd) };
3021            set_buffer(&socket, libc::SO_SNDBUF, bytes);
3022            let SocketAddr::V4(v4) = address else {
3023                panic!("test listener must be IPv4")
3024            };
3025            let sin = libc::sockaddr_in {
3026                sin_family: libc::AF_INET as libc::sa_family_t,
3027                sin_port: v4.port().to_be(),
3028                sin_addr: libc::in_addr {
3029                    s_addr: u32::from_ne_bytes(v4.ip().octets()),
3030                },
3031                sin_zero: [0; 8],
3032            };
3033            let ret = unsafe {
3034                libc::connect(
3035                    fd,
3036                    &sin as *const libc::sockaddr_in as *const libc::sockaddr,
3037                    std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
3038                )
3039            };
3040            assert_eq!(
3041                ret,
3042                0,
3043                "connect() must succeed: {}",
3044                std::io::Error::last_os_error()
3045            );
3046            socket
3047        }
3048
3049        let (mut frontend_peer, frontend_socket) = connected_pair();
3050        let (mut backend_peer, backend_socket) = tiny_backend_pair();
3051
3052        let mut pool = Pool::with_capacity(2, 2, 4096);
3053        let backend_buffer = pool.checkout().expect("backend buffer");
3054        let frontend_buffer = pool.checkout().expect("frontend buffer");
3055        let address = "127.0.0.1:0".parse().expect("test address");
3056        let listener = Rc::new(RefCell::new(TestListener { address }));
3057
3058        let mut pipe = Pipe::new(
3059            backend_buffer,
3060            None,
3061            Some(TcpStream::from_std(backend_socket)),
3062            None,
3063            None,
3064            None,
3065            None,
3066            frontend_buffer,
3067            Token(0),
3068            TcpStream::from_std(frontend_socket),
3069            listener,
3070            Protocol::TCP,
3071            Ulid::generate(),
3072            Ulid::generate(),
3073            None,
3074            WebSocketContext::Tcp,
3075        );
3076        pipe.set_back_token(Token(1));
3077        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
3078        pipe.frontend_readiness.event = Ready::READABLE;
3079        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
3080
3081        let slot = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
3082        assert!(slot > 0, "sysconf(_SC_PAGESIZE) must succeed");
3083        {
3084            let splice_pipe = pipe
3085                .splice_pipe
3086                .as_mut()
3087                .expect("Protocol::TCP must allocate the splice kernel pipe");
3088            for fd in [splice_pipe.in_pipe[0], splice_pipe.out_pipe[0]] {
3089                let ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, slot as libc::c_int) };
3090                assert_eq!(ret, slot as libc::c_int, "F_SETPIPE_SZ(page) must succeed");
3091            }
3092            splice_pipe.capacity = slot;
3093        }
3094
3095        // The mandatory drain/retry consumes at most two slots in total. A
3096        // third keeps bytes queued for the independent multi-slot phase.
3097        let payload = vec![0x5a_u8; 3 * slot];
3098        frontend_peer
3099            .write_all(&payload)
3100            .expect("write frontend payload");
3101        for _ in 0..1000 {
3102            if unread_bytes(pipe.frontend.socket_ref()) >= payload.len() {
3103                break;
3104            }
3105            std::thread::sleep(Duration::from_millis(1));
3106        }
3107        assert_eq!(unread_bytes(pipe.frontend.socket_ref()), payload.len());
3108
3109        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
3110        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3111        let first = pipe.splice_in_pending();
3112        assert!(
3113            first > 0 && first <= slot,
3114            "one slot absorbed (got {first})"
3115        );
3116
3117        // Partial drain toward the backend keeps the slot occupied.
3118        {
3119            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().in_pipe[0];
3120            let backend = pipe.backend_socket.as_ref().unwrap();
3121            let (drained, res) = splice::splice_out(pipe_read_end, backend, 1);
3122            assert_eq!((drained, res), (1, SocketResult::Continue));
3123            pipe.splice_pipe.as_mut().unwrap().in_pipe_pending -= drained;
3124        }
3125
3126        pipe.frontend_readiness.interest.insert(Ready::READABLE);
3127        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3128        let still_queued = unread_bytes(pipe.frontend.socket_ref());
3129        assert_eq!(still_queued, payload.len() - first);
3130        assert!(
3131            pipe.frontend_readiness.event.is_readable(),
3132            "READABLE event must survive a pipe-full EAGAIN with {still_queued} bytes queued"
3133        );
3134        assert!(
3135            !pipe.frontend_readiness.interest.is_readable(),
3136            "READABLE interest must be parked while the pipe is full"
3137        );
3138        assert!(
3139            pipe.backend_readiness.event.is_writable()
3140                && pipe.backend_readiness.interest.is_writable(),
3141            "the backend must be armed writable to drain the pipe"
3142        );
3143
3144        // Mandatory recovery: drain through the production path until the
3145        // single slot is free. This must re-arm the frontend read while the
3146        // READABLE event preserved above drives the retry, with no readiness
3147        // injected by the test.
3148        let mut backend_buf = [0_u8; 16 * 1024];
3149        for _ in 0..1000 {
3150            assert_ne!(
3151                pipe.splice_backend_writable(&mut metrics),
3152                SessionResult::Close
3153            );
3154            loop {
3155                match backend_peer.read(&mut backend_buf) {
3156                    Ok(0) => panic!("backend peer closed while draining the pipe"),
3157                    Ok(_) => {}
3158                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
3159                    Err(error) => panic!("failed to read drained pipe bytes: {error}"),
3160                }
3161            }
3162            if pipe.splice_in_pending() == 0 {
3163                break;
3164            }
3165            std::thread::sleep(Duration::from_millis(1));
3166        }
3167        assert_eq!(
3168            pipe.splice_in_pending(),
3169            0,
3170            "the single-slot pipe must fully drain"
3171        );
3172        assert!(
3173            pipe.frontend_readiness.interest.is_readable()
3174                && pipe.frontend_readiness.event.is_readable(),
3175            "the real backend drain must re-arm the preserved frontend read"
3176        );
3177        let unread_before_retry = unread_bytes(pipe.frontend.socket_ref());
3178        assert_eq!(unread_before_retry, still_queued);
3179        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3180        let unread_after_retry = unread_bytes(pipe.frontend.socket_ref());
3181        assert!(
3182            unread_after_retry < unread_before_retry,
3183            "the preserved READABLE event must resume frontend progress after the drain \
3184             ({unread_before_retry} before, {unread_after_retry} after)"
3185        );
3186        assert!(pipe.splice_in_pending() > 0);
3187
3188        // Empty the recovered slot before starting the independent partial-
3189        // drain phase, then request sixteen page-derived slots. Pipe growth is
3190        // a capability rather than a production requirement, so use the
3191        // realised size and skip only this phase when the kernel denies it.
3192        discard_in_pipe(&mut pipe);
3193        let requested_capacity = slot.checked_mul(16).expect("pipe size must fit usize");
3194        let capacity = {
3195            let splice_pipe = pipe.splice_pipe.as_mut().unwrap();
3196            let in_capacity = request_pipe_capacity(
3197                splice_pipe.in_pipe[0],
3198                requested_capacity,
3199                force_growth_refusal,
3200            );
3201            let out_capacity = request_pipe_capacity(
3202                splice_pipe.out_pipe[0],
3203                requested_capacity,
3204                force_growth_refusal,
3205            );
3206            let capacity = in_capacity.min(out_capacity);
3207            splice_pipe.capacity = capacity;
3208            capacity
3209        };
3210        let minimum_capacity = slot
3211            .checked_mul(2)
3212            .expect("minimum pipe size must fit usize")
3213            .max(32 * 1024);
3214        if capacity < minimum_capacity {
3215            eprintln!(
3216                "skipping multi-slot partial-drain phase: realised capacity {capacity} is below \
3217                 required {minimum_capacity}"
3218            );
3219            return false;
3220        }
3221
3222        // Fill the pipe well past what the backend leg can absorb (tiny
3223        // pinned SO_SNDBUF + SO_RCVBUF, a few KiB in total), so that a drain
3224        // can only move part of it.
3225        frontend_peer
3226            .write_all(&vec![0x5a_u8; capacity])
3227            .expect("write more frontend payload");
3228        for _ in 0..1000 {
3229            if pipe.splice_in_pending() >= capacity / 2 {
3230                break;
3231            }
3232            pipe.frontend_readiness.interest.insert(Ready::READABLE);
3233            pipe.frontend_readiness.event.insert(Ready::READABLE);
3234            assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3235            std::thread::sleep(Duration::from_millis(1));
3236        }
3237        assert!(
3238            pipe.splice_in_pending() >= capacity / 2,
3239            "the pipe must hold at least half its realised capacity (got {})",
3240            pipe.splice_in_pending()
3241        );
3242        pipe.frontend_readiness.interest.remove(Ready::READABLE);
3243
3244        // One drain: the backend leg absorbs a few KiB (its pinned buffers)
3245        // then EAGAINs, leaving most of the pipe pending -- a partial drain
3246        // with no timing involved.
3247        let pending_before_drain = pipe.splice_in_pending();
3248        assert_ne!(
3249            pipe.splice_backend_writable(&mut metrics),
3250            SessionResult::Close
3251        );
3252        let pending = pipe.splice_in_pending();
3253        assert!(
3254            pending < pending_before_drain,
3255            "the backend must accept some bytes ({pending_before_drain} pending)"
3256        );
3257        assert!(
3258            pending > 0,
3259            "the drain must be partial, not complete (accepted {})",
3260            pending_before_drain - pending
3261        );
3262        assert!(
3263            pipe.frontend_readiness.interest.is_readable(),
3264            "a partial drain of the pipe must re-arm the frontend read"
3265        );
3266
3267        // Re-arming is the partial-drain contract. Data progress is a separate
3268        // assertion: release every occupied slot first, then compare FIONREAD
3269        // immediately around the retry so no stale pre-write baseline is used.
3270        discard_in_pipe(&mut pipe);
3271        for _ in 0..1000 {
3272            if unread_bytes(pipe.frontend.socket_ref()) > 0 {
3273                break;
3274            }
3275            std::thread::sleep(Duration::from_millis(1));
3276        }
3277        let unread_before_retry = unread_bytes(pipe.frontend.socket_ref());
3278        assert!(
3279            unread_before_retry > 0,
3280            "request bytes must remain queued for the free-slot retry"
3281        );
3282        assert!(
3283            pipe.frontend_readiness.interest.is_readable()
3284                && pipe.frontend_readiness.event.is_readable(),
3285            "the partial backend drain must leave the frontend retry runnable"
3286        );
3287        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3288        let unread_after_retry = unread_bytes(pipe.frontend.socket_ref());
3289        assert!(
3290            unread_after_retry < unread_before_retry,
3291            "a retry with free pipe slots must consume request bytes \
3292             ({unread_before_retry} before, {unread_after_retry} after)"
3293        );
3294        assert!(pipe.splice_in_pending() > 0);
3295        true
3296    }
3297
3298    /// Frontend-direction twin of the test above: `splice_readable` must
3299    /// keep the READABLE event on a pipe-full EAGAIN, and a *partial*
3300    /// `splice_backend_writable` drain must re-arm the frontend read (the
3301    /// response direction already did so via `splice_writable`).
3302    #[cfg(all(target_os = "linux", feature = "splice"))]
3303    #[test]
3304    fn splice_readable_keeps_readable_event_when_pipe_slots_are_exhausted() {
3305        exercise_splice_readable_pipe_full_recovery(false);
3306    }
3307
3308    #[cfg(all(target_os = "linux", feature = "splice"))]
3309    #[test]
3310    fn splice_readable_recovers_when_pipe_growth_is_refused() {
3311        assert!(!exercise_splice_readable_pipe_full_recovery(true));
3312    }
3313    /// The access log's backend address is the one recorded when the backend
3314    /// was chosen, never a `getpeername(2)` on the backend socket: that call
3315    /// cost one syscall per session, and failed with `ENOTCONN` (logging no
3316    /// address) on a backend still connecting or already reset.
3317    ///
3318    /// The backend socket here is refused: nothing listens on its port, so
3319    /// on loopback it is reset before the first assertion and `peer_addr`
3320    /// can only fail. The address must still be reported, first from the
3321    /// `Backend` handed to `Pipe::new` (the WebSocket upgrade path), then from
3322    /// `set_backend_address` (the TCP proxy's dial, overwritten on a retry).
3323    ///
3324    /// To SEE THIS RED: make `Pipe::get_backend_address` return
3325    /// `self.backend_socket.as_ref().and_then(|backend| backend.peer_addr().ok())`
3326    /// again. The first assertion then fails with `None`.
3327    #[test]
3328    fn backend_address_is_reported_without_asking_the_socket() {
3329        let refused = {
3330            let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind a throwaway listener");
3331            listener
3332                .local_addr()
3333                .expect("throwaway listener local addr")
3334        };
3335        let backend_socket = TcpStream::connect(refused).expect("a nonblocking connect starts");
3336        let (_frontend_peer, frontend_socket) = connected_pair();
3337
3338        let mut pool = Pool::with_capacity(2, 2, 4096);
3339        let backend_buffer = pool.checkout().expect("backend buffer");
3340        let frontend_buffer = pool.checkout().expect("frontend buffer");
3341        let address = "127.0.0.1:0".parse().expect("test address");
3342        let listener = Rc::new(RefCell::new(TestListener { address }));
3343        let backend = Rc::new(RefCell::new(Backend::new(
3344            "backend-refused",
3345            refused,
3346            None,
3347            None,
3348            None,
3349        )));
3350
3351        let mut pipe = Pipe::new(
3352            backend_buffer,
3353            Some("backend-refused".to_owned()),
3354            Some(backend_socket),
3355            Some(backend),
3356            None,
3357            None,
3358            None,
3359            frontend_buffer,
3360            Token(0),
3361            TcpStream::from_std(frontend_socket),
3362            listener,
3363            Protocol::HTTP,
3364            Ulid::generate(),
3365            Ulid::generate(),
3366            None,
3367            WebSocketContext::Tcp,
3368        );
3369        assert_eq!(
3370            pipe.get_backend_address(),
3371            Some(refused),
3372            "the address of the backend handed to Pipe::new must be reported \
3373             even though its socket was refused"
3374        );
3375
3376        let retried: SocketAddr = "192.0.2.7:4242".parse().expect("retry address");
3377        pipe.set_backend_address(Some(retried));
3378        assert_eq!(
3379            pipe.get_backend_address(),
3380            Some(retried),
3381            "a later dial must replace the reported backend address"
3382        );
3383    }
3384}