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    /// A WebSocket pipe is targeted by the backend it was upgraded on.
1835    fn targets(&self, target: &crate::SessionTarget) -> bool {
1836        self.cluster_id.as_deref().is_some_and(|cluster_id| {
1837            target.matches(cluster_id, self.backend_id.as_deref(), self.backend_address)
1838        })
1839    }
1840}
1841
1842#[cfg(test)]
1843mod tests {
1844    use std::{
1845        collections::BTreeMap,
1846        io::Write,
1847        net::{TcpListener as StdTcpListener, TcpStream as StdTcpStream},
1848        time::Duration,
1849    };
1850
1851    use super::*;
1852    use crate::pool::Pool;
1853
1854    struct TestListener {
1855        address: SocketAddr,
1856    }
1857
1858    impl ListenerHandler for TestListener {
1859        fn get_addr(&self) -> &SocketAddr {
1860            &self.address
1861        }
1862
1863        fn get_tags(&self, _key: &str) -> Option<&sozu_command::logging::CachedTags> {
1864            None
1865        }
1866
1867        fn set_tags(&mut self, _key: String, _tags: Option<BTreeMap<String, String>>) {}
1868
1869        fn protocol(&self) -> Protocol {
1870            Protocol::HTTP
1871        }
1872
1873        fn public_address(&self) -> SocketAddr {
1874            self.address
1875        }
1876    }
1877
1878    fn connected_pair() -> (StdTcpStream, StdTcpStream) {
1879        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
1880        let address = listener.local_addr().expect("listener local addr");
1881        let client = StdTcpStream::connect(address).expect("connect test client");
1882        let (server, _) = listener.accept().expect("accept test server");
1883        client.set_nonblocking(true).expect("client nonblocking");
1884        server.set_nonblocking(true).expect("server nonblocking");
1885        (client, server)
1886    }
1887
1888    #[test]
1889    fn backend_readable_arms_frontend_writable_event_when_buffering_response() {
1890        let (frontend_peer, frontend_socket) = connected_pair();
1891        let (mut backend_peer, backend_socket) = connected_pair();
1892
1893        let mut pool = Pool::with_capacity(2, 2, 4096);
1894        let backend_buffer = pool.checkout().expect("backend buffer");
1895        let frontend_buffer = pool.checkout().expect("frontend buffer");
1896        let address = "127.0.0.1:0".parse().expect("test address");
1897        let listener = Rc::new(RefCell::new(TestListener { address }));
1898
1899        let mut pipe = Pipe::new(
1900            backend_buffer,
1901            None,
1902            Some(TcpStream::from_std(backend_socket)),
1903            None,
1904            None,
1905            None,
1906            None,
1907            frontend_buffer,
1908            Token(0),
1909            TcpStream::from_std(frontend_socket),
1910            listener,
1911            Protocol::HTTP,
1912            Ulid::generate(),
1913            Ulid::generate(),
1914            None,
1915            WebSocketContext::Tcp,
1916        );
1917        pipe.set_back_token(Token(1));
1918        pipe.frontend_readiness.event = Ready::EMPTY;
1919        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1920        pipe.backend_readiness.event = Ready::READABLE;
1921        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1922
1923        backend_peer
1924            .write_all(b"server-speaks-first")
1925            .expect("write backend payload");
1926
1927        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1928        assert_eq!(pipe.backend_readable(&mut metrics), SessionResult::Continue);
1929
1930        assert!(
1931            pipe.backend_buffer.available_data() > 0,
1932            "backend_readable must buffer backend bytes"
1933        );
1934        assert!(
1935            pipe.frontend_readiness.interest.is_writable(),
1936            "buffered backend bytes must arm frontend WRITABLE interest"
1937        );
1938        assert!(
1939            pipe.frontend_readiness.event.is_writable(),
1940            "buffered backend bytes must queue a frontend WRITABLE event"
1941        );
1942
1943        drop(frontend_peer);
1944    }
1945
1946    #[test]
1947    fn frontend_readable_arms_backend_writable_event_when_buffering_request() {
1948        let (mut frontend_peer, frontend_socket) = connected_pair();
1949        let (backend_peer, backend_socket) = connected_pair();
1950
1951        let mut pool = Pool::with_capacity(2, 2, 4096);
1952        let backend_buffer = pool.checkout().expect("backend buffer");
1953        let frontend_buffer = pool.checkout().expect("frontend buffer");
1954        let address = "127.0.0.1:0".parse().expect("test address");
1955        let listener = Rc::new(RefCell::new(TestListener { address }));
1956
1957        let mut pipe = Pipe::new(
1958            backend_buffer,
1959            None,
1960            Some(TcpStream::from_std(backend_socket)),
1961            None,
1962            None,
1963            None,
1964            None,
1965            frontend_buffer,
1966            Token(0),
1967            TcpStream::from_std(frontend_socket),
1968            listener,
1969            Protocol::HTTP,
1970            Ulid::generate(),
1971            Ulid::generate(),
1972            None,
1973            WebSocketContext::Tcp,
1974        );
1975        pipe.set_back_token(Token(1));
1976        pipe.frontend_readiness.event = Ready::READABLE;
1977        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1978        pipe.backend_readiness.event = Ready::EMPTY;
1979        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1980
1981        frontend_peer
1982            .write_all(b"client-speaks-after-upgrade")
1983            .expect("write frontend payload");
1984
1985        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1986        assert_eq!(pipe.readable(&mut metrics), SessionResult::Continue);
1987
1988        assert!(
1989            pipe.frontend_buffer.available_data() > 0,
1990            "readable must buffer frontend bytes"
1991        );
1992        assert!(
1993            pipe.backend_readiness.interest.is_writable(),
1994            "buffered frontend bytes must arm backend WRITABLE interest"
1995        );
1996        assert!(
1997            pipe.backend_readiness.event.is_writable(),
1998            "buffered frontend bytes must queue a backend WRITABLE event"
1999        );
2000
2001        drop(backend_peer);
2002    }
2003
2004    #[test]
2005    fn restore_readiness_events_rearms_inherited_buffered_writes() {
2006        let (frontend_peer, frontend_socket) = connected_pair();
2007        let (backend_peer, backend_socket) = connected_pair();
2008
2009        let mut pool = Pool::with_capacity(2, 2, 4096);
2010        let mut backend_buffer = pool.checkout().expect("backend buffer");
2011        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2012        backend_buffer
2013            .write_all(b"backend bytes inherited from 101 read")
2014            .expect("write backend buffer");
2015        frontend_buffer
2016            .write_all(b"frontend bytes inherited from upgrade read")
2017            .expect("write frontend buffer");
2018        let address = "127.0.0.1:0".parse().expect("test address");
2019        let listener = Rc::new(RefCell::new(TestListener { address }));
2020
2021        let mut pipe = Pipe::new(
2022            backend_buffer,
2023            None,
2024            Some(TcpStream::from_std(backend_socket)),
2025            None,
2026            None,
2027            None,
2028            None,
2029            frontend_buffer,
2030            Token(0),
2031            TcpStream::from_std(frontend_socket),
2032            listener,
2033            Protocol::HTTP,
2034            Ulid::generate(),
2035            Ulid::generate(),
2036            None,
2037            WebSocketContext::Tcp,
2038        );
2039
2040        pipe.restore_readiness_events(Ready::EMPTY, Ready::EMPTY);
2041
2042        assert!(
2043            pipe.frontend_readiness.event.is_writable(),
2044            "restoring inherited frontend events must not park backend-buffered bytes"
2045        );
2046        assert!(
2047            pipe.backend_readiness.event.is_writable(),
2048            "restoring inherited backend events must not park frontend-buffered bytes"
2049        );
2050
2051        drop(frontend_peer);
2052        drop(backend_peer);
2053    }
2054
2055    /// Regression guard for the splice/preread interaction: when `Pipe` is
2056    /// constructed with the splice fast path available (`Protocol::TCP`)
2057    /// AND a non-empty inherited `frontend_buffer` (the SNI-preread
2058    /// ClientHello replay scenario), `backend_writable` must drain those
2059    /// buffered bytes to the backend socket through the normal buffered
2060    /// path first. Without the gate in `backend_writable`, this call would
2061    /// dispatch straight to `splice_backend_writable`, which only drains the
2062    /// kernel pipe (`splice_in_pending`) — 0 here — and returns immediately
2063    /// without ever touching `frontend_buffer`, silently dropping the
2064    /// preread bytes.
2065    #[cfg(all(target_os = "linux", feature = "splice"))]
2066    #[test]
2067    fn backend_writable_drains_inherited_frontend_buffer_before_splice_engages() {
2068        use std::io::Read;
2069
2070        let (frontend_peer, frontend_socket) = connected_pair();
2071        let (mut backend_peer, backend_socket) = connected_pair();
2072
2073        let mut pool = Pool::with_capacity(2, 2, 4096);
2074        let backend_buffer = pool.checkout().expect("backend buffer");
2075        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2076        frontend_buffer
2077            .write_all(b"inherited-preread-client-hello")
2078            .expect("write inherited frontend buffer");
2079        let address = "127.0.0.1:0".parse().expect("test address");
2080        let listener = Rc::new(RefCell::new(TestListener { address }));
2081
2082        let mut pipe = Pipe::new(
2083            backend_buffer,
2084            None,
2085            Some(TcpStream::from_std(backend_socket)),
2086            None,
2087            None,
2088            None,
2089            None,
2090            frontend_buffer,
2091            Token(0),
2092            TcpStream::from_std(frontend_socket),
2093            listener,
2094            Protocol::TCP,
2095            Ulid::generate(),
2096            Ulid::generate(),
2097            None,
2098            WebSocketContext::Tcp,
2099        );
2100        pipe.set_back_token(Token(1));
2101
2102        assert!(
2103            pipe.splice_pipe.is_some(),
2104            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
2105        );
2106        assert!(
2107            pipe.frontend_buffer.available_data() > 0,
2108            "test setup must inherit a non-empty frontend buffer"
2109        );
2110
2111        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2112        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Continue);
2113
2114        assert_eq!(
2115            pipe.frontend_buffer.available_data(),
2116            0,
2117            "inherited preread bytes must drain through the buffered path before splice engages"
2118        );
2119
2120        let mut received = [0u8; 64];
2121        let n = backend_peer
2122            .read(&mut received)
2123            .expect("backend socket must have received the drained preread bytes");
2124        assert_eq!(&received[..n], b"inherited-preread-client-hello");
2125
2126        drop(frontend_peer);
2127    }
2128
2129    /// Regression guard for the close-before-flush data loss
2130    /// (sozu-proxy/sozu#1279): when the frontend read side reaches
2131    /// EOF while `frontend_buffer` still holds bytes queued for the backend,
2132    /// `readable` must NOT return `Close` (which discarded them, silently
2133    /// truncating the stream -- the reproducer was a payload coalesced with an
2134    /// SNI ClientHello). It must keep the session alive to drain, then the
2135    /// queued bytes must reach the backend and only then may the session
2136    /// close.
2137    #[test]
2138    fn frontend_eof_flushes_queued_backend_bytes_before_closing() {
2139        use std::io::{Read, Write};
2140
2141        let (frontend_peer, frontend_socket) = connected_pair();
2142        let (mut backend_peer, backend_socket) = connected_pair();
2143
2144        let mut pool = Pool::with_capacity(2, 2, 4096);
2145        let backend_buffer = pool.checkout().expect("backend buffer");
2146        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2147        // Bytes the frontend already delivered, still queued for the backend.
2148        frontend_buffer
2149            .write_all(b"front-to-back-tail-still-queued")
2150            .expect("seed queued frontend bytes");
2151        let address = "127.0.0.1:0".parse().expect("test address");
2152        let listener = Rc::new(RefCell::new(TestListener { address }));
2153
2154        let mut pipe = Pipe::new(
2155            backend_buffer,
2156            None,
2157            Some(TcpStream::from_std(backend_socket)),
2158            None,
2159            None,
2160            None,
2161            None,
2162            frontend_buffer,
2163            Token(0),
2164            TcpStream::from_std(frontend_socket),
2165            listener,
2166            Protocol::TCP,
2167            Ulid::generate(),
2168            Ulid::generate(),
2169            None,
2170            WebSocketContext::Tcp,
2171        );
2172        pipe.set_back_token(Token(1));
2173        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2174        pipe.frontend_readiness.event = Ready::READABLE;
2175
2176        // The frontend closes right after delivering its bytes (no gap): the
2177        // pipe reads EOF while `frontend_buffer` is still non-empty.
2178        drop(frontend_peer);
2179
2180        // Nonblocking EOF is not observable on the very first read on every
2181        // platform, so retry `readable` (bounded, no sleep) until it observes
2182        // the close. `readable` must never report `Close` while bytes remain
2183        // queued.
2184        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2185        let mut saw_eof = false;
2186        for _ in 0..100_000 {
2187            let result = pipe.readable(&mut metrics);
2188            assert_ne!(
2189                result,
2190                SessionResult::Close,
2191                "readable must not close while {} queued backend bytes remain",
2192                pipe.frontend_buffer.available_data()
2193            );
2194            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2195                saw_eof = true;
2196                break;
2197            }
2198        }
2199        assert!(
2200            saw_eof,
2201            "frontend EOF was never observed within the retry budget"
2202        );
2203        assert!(
2204            pipe.frontend_buffer.available_data() > 0,
2205            "the queued backend bytes must survive the frontend EOF, not be dropped"
2206        );
2207
2208        // Draining now delivers every queued byte to the backend. Nothing
2209        // else is in flight (backend still `Normal`) and the frontend read
2210        // side already hit EOF (`WriteOpen`), so `backend_writable` closes
2211        // the session itself as soon as the drain completes -- it does not
2212        // wait for a follow-up event that edge-triggered epoll would never
2213        // deliver (sozu-proxy/sozu#1290).
2214        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Close);
2215        assert_eq!(
2216            pipe.frontend_buffer.available_data(),
2217            0,
2218            "the queued bytes must all drain to the backend after EOF"
2219        );
2220
2221        let mut received = Vec::new();
2222        // Read until we have the full payload (a single read may segment).
2223        for _ in 0..100_000 {
2224            let mut buf = [0u8; 64];
2225            match backend_peer.read(&mut buf) {
2226                Ok(0) => break,
2227                Ok(n) => {
2228                    received.extend_from_slice(&buf[..n]);
2229                    if received.len() >= b"front-to-back-tail-still-queued".len() {
2230                        break;
2231                    }
2232                }
2233                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2234                Err(_) => break,
2235            }
2236        }
2237        assert_eq!(
2238            received, b"front-to-back-tail-still-queued",
2239            "the backend must receive the queued tail byte-for-byte, not a truncation"
2240        );
2241    }
2242
2243    /// Regression guard for the missing completion edge on the deferred
2244    /// close (sozu-proxy/sozu#1290): the test above proves the queued bytes
2245    /// survive the frontend's EOF; this proves the other half of the fix --
2246    /// that draining those bytes closes the session immediately, from that
2247    /// single `backend_writable` event, with no extra event injected by the
2248    /// test. Before the fix, `backend_writable`'s drained-buffer early
2249    /// return always reported `Continue`, even with the frontend already at
2250    /// EOF and nothing else in flight: edge-triggered epoll had already
2251    /// consumed the READABLE/HUP edge that signalled the EOF, so no other
2252    /// event existed to hang a follow-up close decision on and the session
2253    /// sat resident until an unrelated event or timeout.
2254    #[test]
2255    fn frontend_eof_with_queued_bytes_closes_on_the_draining_backend_writable_call() {
2256        use std::io::Read;
2257
2258        let (mut frontend_peer, frontend_socket) = connected_pair();
2259        let (mut backend_peer, backend_socket) = connected_pair();
2260
2261        let mut pool = Pool::with_capacity(2, 2, 4096);
2262        let backend_buffer = pool.checkout().expect("backend buffer");
2263        let frontend_buffer = pool.checkout().expect("frontend buffer");
2264        let address = "127.0.0.1:0".parse().expect("test address");
2265        let listener = Rc::new(RefCell::new(TestListener { address }));
2266
2267        let mut pipe = Pipe::new(
2268            backend_buffer,
2269            None,
2270            Some(TcpStream::from_std(backend_socket)),
2271            None,
2272            None,
2273            None,
2274            None,
2275            frontend_buffer,
2276            Token(0),
2277            TcpStream::from_std(frontend_socket),
2278            listener,
2279            Protocol::HTTP,
2280            Ulid::generate(),
2281            Ulid::generate(),
2282            None,
2283            WebSocketContext::Tcp,
2284        );
2285        pipe.set_back_token(Token(1));
2286        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2287        pipe.frontend_readiness.event = Ready::READABLE;
2288
2289        // The frontend delivers its bytes then closes right away -- no gap
2290        // -- so the real `readable()` call below observes EOF while bytes
2291        // are still queued in `frontend_buffer`.
2292        frontend_peer
2293            .write_all(b"queued-request-tail")
2294            .expect("write frontend payload");
2295        drop(frontend_peer);
2296
2297        // Drive the REAL ready pass: keep calling `readable` (bounded, no
2298        // sleep) until it observes the close, exactly like the event loop
2299        // would after mio reports the frontend's HUP/EOF.
2300        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2301        let mut saw_eof = false;
2302        for _ in 0..100_000 {
2303            let result = pipe.readable(&mut metrics);
2304            assert_ne!(
2305                result,
2306                SessionResult::Close,
2307                "readable must not close while queued backend bytes remain"
2308            );
2309            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2310                saw_eof = true;
2311                break;
2312            }
2313        }
2314        assert!(
2315            saw_eof,
2316            "frontend EOF was never observed within the retry budget"
2317        );
2318        assert!(
2319            pipe.frontend_buffer.available_data() > 0,
2320            "the queued bytes must survive the frontend EOF"
2321        );
2322        assert!(
2323            pipe.backend_readiness.interest.is_writable()
2324                && pipe.backend_readiness.event.is_writable(),
2325            "observing EOF with queued bytes must arm backend WRITABLE to drain them"
2326        );
2327
2328        // The backend becomes writable exactly once -- the real event the
2329        // arming above promises -- and drains everything in that single
2330        // call. No extra readable()/frontend_hup()/check_connections() call
2331        // is injected here: the session must close from THIS event alone.
2332        let result = pipe.backend_writable(&mut metrics);
2333
2334        assert_eq!(
2335            result,
2336            SessionResult::Close,
2337            "the single draining backend_writable call must close the session by itself -- \
2338             edge-triggered epoll already consumed the EOF edge and offers no other event to \
2339             hang the teardown on"
2340        );
2341        assert_eq!(
2342            pipe.frontend_buffer.available_data(),
2343            0,
2344            "the queued bytes must have fully drained before the session closed"
2345        );
2346
2347        let mut received = Vec::new();
2348        for _ in 0..100_000 {
2349            let mut buf = [0u8; 64];
2350            match backend_peer.read(&mut buf) {
2351                Ok(0) => break,
2352                Ok(n) => {
2353                    received.extend_from_slice(&buf[..n]);
2354                    if received.len() >= b"queued-request-tail".len() {
2355                        break;
2356                    }
2357                }
2358                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2359                Err(_) => break,
2360            }
2361        }
2362        assert_eq!(
2363            received, b"queued-request-tail",
2364            "the backend must still receive the queued bytes byte-for-byte before the close"
2365        );
2366    }
2367
2368    /// Regression guard for the HUP-path sibling of the close-before-flush
2369    /// data loss (sozu-proxy/sozu#1290): `command/src/ready.rs`'s
2370    /// `From<&mio::event::Event>` sets `Ready::HUP` whenever
2371    /// `is_read_closed()`/`is_write_closed()` fires, independently of
2372    /// `Ready::READABLE` -- so a client FIN that coalesces with the payload
2373    /// tail on a loaded event loop delivers a SINGLE epoll batch carrying
2374    /// BOTH bits. `frontend_hup` must not drop bytes still queued in
2375    /// `frontend_buffer` (already read) or still pending in the kernel
2376    /// receive buffer (signalled by the retained READABLE event) just
2377    /// because HUP also fired.
2378    #[test]
2379    fn frontend_hup_drains_inflight_request_bytes_before_closing() {
2380        let (frontend_peer, frontend_socket) = connected_pair();
2381        let (_backend_peer, backend_socket) = connected_pair();
2382
2383        let mut pool = Pool::with_capacity(2, 2, 4096);
2384        let backend_buffer = pool.checkout().expect("backend buffer");
2385        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2386        frontend_buffer
2387            .write_all(b"front-to-back-tail-still-queued")
2388            .expect("seed queued frontend bytes");
2389        let address = "127.0.0.1:0".parse().expect("test address");
2390        let listener = Rc::new(RefCell::new(TestListener { address }));
2391
2392        let mut pipe = Pipe::new(
2393            backend_buffer,
2394            None,
2395            Some(TcpStream::from_std(backend_socket)),
2396            None,
2397            None,
2398            None,
2399            None,
2400            frontend_buffer,
2401            Token(0),
2402            TcpStream::from_std(frontend_socket),
2403            listener,
2404            Protocol::TCP,
2405            Ulid::generate(),
2406            Ulid::generate(),
2407            None,
2408            WebSocketContext::Tcp,
2409        );
2410        pipe.set_back_token(Token(1));
2411        // The kernel receive buffer still has a tail behind the FIN: both
2412        // bits set in the same batch, exactly as `ready.rs` would produce.
2413        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2414        pipe.frontend_readiness.event = Ready::READABLE | Ready::HUP;
2415        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2416        pipe.backend_readiness.event = Ready::EMPTY;
2417
2418        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2419        let result = pipe.frontend_hup(&mut metrics);
2420
2421        assert_eq!(
2422            result,
2423            SessionResult::Continue,
2424            "frontend_hup must keep the session alive while request bytes are still in flight"
2425        );
2426        assert!(
2427            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2428            "frontend_hup must still mark the frontend Closed even on the drain branch"
2429        );
2430        assert!(
2431            pipe.backend_readiness.interest.is_writable()
2432                && pipe.backend_readiness.event.is_writable(),
2433            "the drain branch must arm backend WRITABLE to flush the queued frontend bytes"
2434        );
2435        assert!(
2436            pipe.frontend_readiness.interest.is_readable(),
2437            "the drain branch must retain frontend READABLE interest to read the kernel tail to EOF"
2438        );
2439
2440        drop(frontend_peer);
2441    }
2442
2443    /// Sibling of the above: when nothing is in flight (no buffered bytes,
2444    /// no pending READABLE event), `frontend_hup` keeps its pre-existing
2445    /// behavior of closing immediately -- there is nothing left to drain.
2446    #[test]
2447    fn frontend_hup_closes_immediately_when_nothing_is_inflight() {
2448        let (frontend_peer, frontend_socket) = connected_pair();
2449        let (_backend_peer, backend_socket) = connected_pair();
2450
2451        let mut pool = Pool::with_capacity(2, 2, 4096);
2452        let backend_buffer = pool.checkout().expect("backend buffer");
2453        let frontend_buffer = pool.checkout().expect("frontend buffer");
2454        let address = "127.0.0.1:0".parse().expect("test address");
2455        let listener = Rc::new(RefCell::new(TestListener { address }));
2456
2457        let mut pipe = Pipe::new(
2458            backend_buffer,
2459            None,
2460            Some(TcpStream::from_std(backend_socket)),
2461            None,
2462            None,
2463            None,
2464            None,
2465            frontend_buffer,
2466            Token(0),
2467            TcpStream::from_std(frontend_socket),
2468            listener,
2469            Protocol::TCP,
2470            Ulid::generate(),
2471            Ulid::generate(),
2472            None,
2473            WebSocketContext::Tcp,
2474        );
2475        pipe.set_back_token(Token(1));
2476        // HUP only, no readable event, no queued bytes: nothing to drain.
2477        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2478        pipe.frontend_readiness.event = Ready::HUP;
2479        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2480        pipe.backend_readiness.event = Ready::EMPTY;
2481
2482        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2483        let result = pipe.frontend_hup(&mut metrics);
2484
2485        assert_eq!(
2486            result,
2487            SessionResult::Close,
2488            "frontend_hup must close immediately when nothing is in flight (unchanged legacy behavior)"
2489        );
2490        assert!(
2491            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2492            "frontend_hup must mark the frontend Closed"
2493        );
2494        assert!(
2495            !pipe.backend_readiness.event.is_writable(),
2496            "the close branch must not arm backend WRITABLE when there is nothing queued"
2497        );
2498
2499        drop(frontend_peer);
2500    }
2501
2502    /// Regression guard for the SPLICE sibling of the close-before-flush
2503    /// data loss (sozu-proxy/sozu#1279 / #1290): `splice_readable`'s
2504    /// `SocketResult::Closed` arm used to return `Close` unconditionally,
2505    /// dropping whatever `splice_in_pending()` bytes sat in the kernel
2506    /// `in_pipe` when the frontend's FIN was observed. It must instead keep
2507    /// the session alive until `splice_backend_writable` drains the kernel
2508    /// pipe, then close on the next EOF re-observation once nothing is
2509    /// inflight.
2510    #[cfg(all(target_os = "linux", feature = "splice"))]
2511    #[test]
2512    fn splice_readable_eof_drains_kernel_pipe_bytes_before_closing() {
2513        use std::io::Read;
2514
2515        let (mut frontend_peer, frontend_socket) = connected_pair();
2516        let (mut backend_peer, backend_socket) = connected_pair();
2517
2518        let mut pool = Pool::with_capacity(2, 2, 4096);
2519        let backend_buffer = pool.checkout().expect("backend buffer");
2520        // The frontend buffer stays EMPTY: the splice fast path only engages
2521        // when no inherited userspace bytes remain (see `readable`'s gate).
2522        let frontend_buffer = pool.checkout().expect("frontend buffer");
2523        let address = "127.0.0.1:0".parse().expect("test address");
2524        let listener = Rc::new(RefCell::new(TestListener { address }));
2525
2526        let mut pipe = Pipe::new(
2527            backend_buffer,
2528            None,
2529            Some(TcpStream::from_std(backend_socket)),
2530            None,
2531            None,
2532            None,
2533            None,
2534            frontend_buffer,
2535            Token(0),
2536            TcpStream::from_std(frontend_socket),
2537            listener,
2538            Protocol::TCP,
2539            Ulid::generate(),
2540            Ulid::generate(),
2541            None,
2542            WebSocketContext::Tcp,
2543        );
2544        pipe.set_back_token(Token(1));
2545        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2546        pipe.frontend_readiness.event = Ready::READABLE;
2547
2548        assert!(
2549            pipe.splice_pipe.is_some(),
2550            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
2551        );
2552
2553        // 32 KiB: below the 64 KiB default pipe capacity so the whole
2554        // payload fits the kernel in_pipe without backpressure pauses.
2555        let payload = vec![0x5a_u8; 32 * 1024];
2556        // Environment precondition: once this user's pipe buffers exceed
2557        // `fs.pipe-user-pages-soft`, Linux creates every new pipe with 2 pages
2558        // (8 KiB) and refuses `F_SETPIPE_SZ` with EPERM, so `splice_readable`
2559        // parks at its capacity guard and the payload can never sit whole in
2560        // the in_pipe. Fail loudly and name the host limit rather than report
2561        // a false regression.
2562        let capacity = pipe.splice_capacity();
2563        assert!(
2564            capacity >= payload.len(),
2565            "environment precondition not met: splice pipe capacity is {capacity} bytes, \
2566             below the {}-byte payload; this user's pipe buffers likely exceed \
2567             fs.pipe-user-pages-soft ({}). Close pipe-heavy processes or raise the limit, \
2568             then rerun: this is a host limit, not a Sozu regression",
2569            payload.len(),
2570            std::fs::read_to_string("/proc/sys/fs/pipe-user-pages-soft")
2571                .map(|limit| limit.trim().to_owned())
2572                .unwrap_or_else(|error| format!("unreadable: {error}")),
2573        );
2574        frontend_peer
2575            .write_all(&payload)
2576            .expect("write frontend payload");
2577
2578        // Splice the payload into the kernel in_pipe (bounded retries:
2579        // loopback delivers the payload in several chunks, and an early
2580        // call can observe WouldBlock).
2581        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2582        for _ in 0..100_000 {
2583            if pipe.splice_in_pending() >= payload.len() {
2584                break;
2585            }
2586            let result = pipe.splice_readable(&mut metrics);
2587            assert_ne!(
2588                result,
2589                SessionResult::Close,
2590                "splice_readable must not close while splicing the payload in"
2591            );
2592        }
2593        assert_eq!(
2594            pipe.splice_in_pending(),
2595            payload.len(),
2596            "the whole payload must sit in the kernel in_pipe before EOF"
2597        );
2598
2599        // The frontend closes right after its payload: FIN behind the
2600        // spliced bytes.
2601        drop(frontend_peer);
2602
2603        // Keep calling until EOF is observed (WriteOpen transition). The
2604        // buggy arm returned `Close` here, dropping the kernel-pipe bytes.
2605        let mut saw_eof = false;
2606        for _ in 0..100_000 {
2607            let result = pipe.splice_readable(&mut metrics);
2608            assert_ne!(
2609                result,
2610                SessionResult::Close,
2611                "splice_readable must not close while {} kernel-pipe bytes remain",
2612                pipe.splice_in_pending()
2613            );
2614            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2615                saw_eof = true;
2616                break;
2617            }
2618        }
2619        assert!(
2620            saw_eof,
2621            "frontend EOF was never observed within the retry budget"
2622        );
2623        assert_eq!(
2624            pipe.splice_in_pending(),
2625            payload.len(),
2626            "the kernel-pipe bytes must survive the frontend EOF, not be dropped"
2627        );
2628        assert!(
2629            pipe.backend_readiness.interest.is_writable()
2630                && pipe.backend_readiness.event.is_writable(),
2631            "EOF with kernel-pipe bytes pending must arm backend WRITABLE to drain them"
2632        );
2633
2634        // Drain the kernel pipe to the backend and read the peer: the bytes
2635        // must arrive byte-identical (interleave drain + read so a full
2636        // backend socket buffer cannot deadlock the loop). Once the kernel
2637        // pipe is fully drained with the frontend already at EOF, the fix
2638        // under test (sozu-proxy/sozu#1290) closes the session as part of
2639        // that same drain call -- so `drain == Close` is now expected, but
2640        // ONLY once nothing is left pending; closing any earlier would be
2641        // the mid-flight truncation this test guards against. Keep reading
2642        // from `backend_peer` regardless of the session's reported status:
2643        // the bytes were already handed off to the kernel by `splice_out`,
2644        // independently of whether `Pipe` still considers itself open.
2645        let mut received = Vec::with_capacity(payload.len());
2646        let mut closed_during_drain = false;
2647        for _ in 0..100_000 {
2648            let drain = pipe.splice_backend_writable(&mut metrics);
2649            if drain == SessionResult::Close {
2650                assert_eq!(
2651                    pipe.splice_in_pending(),
2652                    0,
2653                    "the session must only close once the kernel pipe is fully drained, \
2654                     never mid-flight"
2655                );
2656                closed_during_drain = true;
2657            }
2658            let mut buf = [0u8; 16384];
2659            match backend_peer.read(&mut buf) {
2660                Ok(0) => break,
2661                Ok(n) => received.extend_from_slice(&buf[..n]),
2662                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
2663                Err(_) => break,
2664            }
2665            if received.len() >= payload.len() {
2666                break;
2667            }
2668        }
2669        assert_eq!(
2670            received, payload,
2671            "the backend must receive the spliced tail byte-for-byte, not a truncation"
2672        );
2673        assert_eq!(
2674            pipe.splice_in_pending(),
2675            0,
2676            "the kernel in_pipe must be fully drained"
2677        );
2678        assert!(
2679            closed_during_drain,
2680            "the fix under test (sozu-proxy/sozu#1290) must close the session as soon as \
2681             the kernel pipe drains, without waiting for a separate re-observation event"
2682        );
2683
2684        // Nothing inflight and the frontend is half-closed: the session was
2685        // already closed above, during the drain itself. Re-observing EOF
2686        // here proves that closed state is stable under a repeat call,
2687        // rather than being the sole mechanism that closes the session (as
2688        // it was before the fix).
2689        assert!(
2690            !pipe.check_connections(),
2691            "with nothing inflight and the frontend closed, the session must be closeable"
2692        );
2693        assert_eq!(
2694            pipe.splice_readable(&mut metrics),
2695            SessionResult::Close,
2696            "re-observing EOF with nothing inflight must (still) close the session"
2697        );
2698    }
2699
2700    /// Regression guard for the splice fast path: `splice(2)` from a socket
2701    /// into a pipe returns `EAGAIN` both when the *socket* has nothing left
2702    /// to read AND when the *pipe* has no free slot. A pipe holds
2703    /// `capacity / PAGE_SIZE` slots, every spliced skb fragment occupies one
2704    /// slot whatever its length, and a partially drained slot stays occupied.
2705    /// So the slots run out while `out_pipe_pending < capacity` (MSS-sized
2706    /// segments from a remote backend: 16 slots x ~1400 B, far below 64 KiB).
2707    /// Treating that `EAGAIN` as a drained socket clears the READABLE event;
2708    /// edge-triggered epoll never re-signals bytes already queued in the
2709    /// socket, so the tail of the response sits in the kernel recv queue
2710    /// forever and the client hangs at the end of the transfer.
2711    ///
2712    /// The test shrinks the pipe to a single 4 KiB slot and drains one byte
2713    /// of it so the slot is still held: the next `splice_in` hits `EAGAIN`
2714    /// with bytes still queued in the backend socket.
2715    #[cfg(all(target_os = "linux", feature = "splice"))]
2716    #[test]
2717    fn splice_backend_readable_keeps_readable_event_when_pipe_slots_are_exhausted() {
2718        use std::os::unix::io::AsRawFd;
2719
2720        fn unread_bytes(socket: &TcpStream) -> usize {
2721            let mut n: libc::c_int = 0;
2722            let ret = unsafe { libc::ioctl(socket.as_raw_fd(), libc::FIONREAD, &mut n) };
2723            assert_eq!(ret, 0, "FIONREAD must succeed");
2724            n as usize
2725        }
2726
2727        let (_frontend_peer, frontend_socket) = connected_pair();
2728        let (mut backend_peer, backend_socket) = connected_pair();
2729
2730        let mut pool = Pool::with_capacity(2, 2, 4096);
2731        let backend_buffer = pool.checkout().expect("backend buffer");
2732        let frontend_buffer = pool.checkout().expect("frontend buffer");
2733        let address = "127.0.0.1:0".parse().expect("test address");
2734        let listener = Rc::new(RefCell::new(TestListener { address }));
2735
2736        let mut pipe = Pipe::new(
2737            backend_buffer,
2738            None,
2739            Some(TcpStream::from_std(backend_socket)),
2740            None,
2741            None,
2742            None,
2743            None,
2744            frontend_buffer,
2745            Token(0),
2746            TcpStream::from_std(frontend_socket),
2747            listener,
2748            Protocol::TCP,
2749            Ulid::generate(),
2750            Ulid::generate(),
2751            None,
2752            WebSocketContext::Tcp,
2753        );
2754        pipe.set_back_token(Token(1));
2755        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2756        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2757        pipe.backend_readiness.event = Ready::READABLE;
2758
2759        // Shrink both kernel pipes to a single page: one slot each.
2760        let slot = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
2761        assert!(slot > 0, "sysconf(_SC_PAGESIZE) must succeed");
2762        {
2763            let splice_pipe = pipe
2764                .splice_pipe
2765                .as_mut()
2766                .expect("Protocol::TCP must allocate the splice kernel pipe");
2767            for fd in [splice_pipe.in_pipe[0], splice_pipe.out_pipe[0]] {
2768                let ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, slot as libc::c_int) };
2769                assert_eq!(ret, slot as libc::c_int, "F_SETPIPE_SZ(page) must succeed");
2770            }
2771            splice_pipe.capacity = slot;
2772        }
2773        let capacity = pipe.splice_capacity();
2774        assert_eq!(capacity, slot);
2775
2776        // Two slots' worth of response bytes queued in the backend socket.
2777        let payload = vec![0x5a_u8; 2 * slot];
2778        backend_peer
2779            .write_all(&payload)
2780            .expect("write backend payload");
2781        for _ in 0..1000 {
2782            if unread_bytes(pipe.backend_socket.as_ref().unwrap()) >= payload.len() {
2783                break;
2784            }
2785            std::thread::sleep(Duration::from_millis(1));
2786        }
2787        assert_eq!(
2788            unread_bytes(pipe.backend_socket.as_ref().unwrap()),
2789            payload.len(),
2790            "the whole payload must be queued in the backend socket"
2791        );
2792
2793        // First pass fills the single slot exactly: byte accounting agrees
2794        // with the kernel and the guard `out_pending >= capacity` engages.
2795        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2796        assert_ne!(
2797            pipe.splice_backend_readable(&mut metrics),
2798            SessionResult::Close
2799        );
2800        let first = pipe.splice_out_pending();
2801        assert!(
2802            first > 0 && first <= slot,
2803            "the single slot must absorb part of the payload (got {first})"
2804        );
2805        assert!(
2806            pipe.backend_readiness.event.is_readable(),
2807            "the READABLE event must survive a successful splice"
2808        );
2809
2810        // Partial drain toward the frontend (a short write): the slot is
2811        // still occupied but `out_pipe_pending` is now below `capacity`.
2812        {
2813            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().out_pipe[0];
2814            let (drained, res) = splice::splice_out(pipe_read_end, pipe.frontend.socket_ref(), 1);
2815            assert_eq!((drained, res), (1, SocketResult::Continue));
2816            pipe.splice_pipe.as_mut().unwrap().out_pipe_pending -= drained;
2817        }
2818        assert!(pipe.splice_out_pending() < capacity);
2819
2820        // Second pass: `splice_in` hits EAGAIN because the pipe has no free
2821        // slot, NOT because the socket is drained.
2822        pipe.backend_readiness.interest.insert(Ready::READABLE);
2823        assert_ne!(
2824            pipe.splice_backend_readable(&mut metrics),
2825            SessionResult::Close
2826        );
2827
2828        let still_queued = unread_bytes(pipe.backend_socket.as_ref().unwrap());
2829        assert_eq!(
2830            still_queued,
2831            payload.len() - first,
2832            "precondition: the rest of the payload must still sit in the socket"
2833        );
2834        assert!(
2835            pipe.backend_readiness.event.is_readable(),
2836            "READABLE event must survive a pipe-full EAGAIN: {still_queued} bytes are still \
2837             queued in the backend socket and edge-triggered epoll will never re-signal them"
2838        );
2839        assert!(
2840            !pipe.backend_readiness.interest.is_readable(),
2841            "READABLE interest must be parked while the pipe is full, or the ready loop spins"
2842        );
2843        assert!(
2844            pipe.frontend_readiness.event.is_writable()
2845                && pipe.frontend_readiness.interest.is_writable(),
2846            "the frontend must be armed writable to drain the pipe"
2847        );
2848
2849        // Recovery: draining the pipe toward the frontend re-arms the
2850        // backend READABLE interest; with the event still pending, the next
2851        // pass splices the second slot's worth of bytes out of the socket
2852        // without any new epoll edge.
2853        assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2854        assert_eq!(
2855            pipe.splice_out_pending(),
2856            0,
2857            "the pipe must be fully drained"
2858        );
2859        assert!(
2860            pipe.backend_readiness.interest.is_readable()
2861                && pipe.backend_readiness.event.is_readable(),
2862            "draining the pipe must re-arm the backend read"
2863        );
2864        assert_ne!(
2865            pipe.splice_backend_readable(&mut metrics),
2866            SessionResult::Close
2867        );
2868        assert!(
2869            unread_bytes(pipe.backend_socket.as_ref().unwrap()) < still_queued,
2870            "the remaining response bytes must start leaving the backend socket"
2871        );
2872        assert!(pipe.splice_out_pending() > 0);
2873
2874        // Drain socket -> pipe -> frontend until the socket is empty.
2875        for _ in 0..1000 {
2876            if unread_bytes(pipe.backend_socket.as_ref().unwrap()) == 0 {
2877                break;
2878            }
2879            assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2880            pipe.backend_readiness.interest.insert(Ready::READABLE);
2881            assert_ne!(
2882                pipe.splice_backend_readable(&mut metrics),
2883                SessionResult::Close
2884            );
2885        }
2886        assert_eq!(unread_bytes(pipe.backend_socket.as_ref().unwrap()), 0);
2887        if pipe.splice_out_pending() == 0 {
2888            // Re-create the ambiguous state: one slot's worth in the pipe.
2889            backend_peer.write_all(&payload[..slot]).expect("write");
2890            for _ in 0..1000 {
2891                if unread_bytes(pipe.backend_socket.as_ref().unwrap()) >= slot {
2892                    break;
2893                }
2894                std::thread::sleep(Duration::from_millis(1));
2895            }
2896            pipe.backend_readiness.interest.insert(Ready::READABLE);
2897            pipe.backend_readiness.event.insert(Ready::READABLE);
2898            assert_ne!(
2899                pipe.splice_backend_readable(&mut metrics),
2900                SessionResult::Close
2901            );
2902            assert_eq!(unread_bytes(pipe.backend_socket.as_ref().unwrap()), 0);
2903        }
2904        assert!(pipe.splice_out_pending() > 0);
2905
2906        // Socket empty and pipe non-empty: EAGAIN is now ambiguous again,
2907        // the event stays pending and the interest is parked until the
2908        // next drain, after which the genuine drained-socket EAGAIN (empty
2909        // pipe) clears the event.
2910        pipe.backend_readiness.interest.insert(Ready::READABLE);
2911        assert_ne!(
2912            pipe.splice_backend_readable(&mut metrics),
2913            SessionResult::Close
2914        );
2915        assert!(pipe.backend_readiness.event.is_readable());
2916        assert!(!pipe.backend_readiness.interest.is_readable());
2917        assert_ne!(pipe.splice_writable(&mut metrics), SessionResult::Close);
2918        assert_eq!(pipe.splice_out_pending(), 0);
2919        assert_ne!(
2920            pipe.splice_backend_readable(&mut metrics),
2921            SessionResult::Close
2922        );
2923        assert!(
2924            !pipe.backend_readiness.event.is_readable(),
2925            "a drained socket with an empty pipe must clear the READABLE event"
2926        );
2927    }
2928
2929    #[cfg(all(target_os = "linux", feature = "splice"))]
2930    fn exercise_splice_readable_pipe_full_recovery(force_growth_refusal: bool) -> bool {
2931        use std::{io::Read, os::unix::io::AsRawFd};
2932
2933        fn unread_bytes(socket: &TcpStream) -> usize {
2934            let mut n: libc::c_int = 0;
2935            let ret = unsafe { libc::ioctl(socket.as_raw_fd(), libc::FIONREAD, &mut n) };
2936            assert_eq!(ret, 0, "FIONREAD must succeed");
2937            n as usize
2938        }
2939
2940        fn discard_in_pipe(pipe: &mut Pipe<TcpStream, TestListener>) {
2941            let pending = pipe.splice_in_pending();
2942            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().in_pipe[0];
2943            let mut discarded = vec![0_u8; pending];
2944            let mut offset = 0;
2945            while offset < pending {
2946                // SAFETY: `pipe_read_end` remains owned by `pipe`, and the
2947                // writable slice covers exactly the bytes still requested.
2948                let read = unsafe {
2949                    libc::read(
2950                        pipe_read_end,
2951                        discarded[offset..].as_mut_ptr().cast(),
2952                        pending - offset,
2953                    )
2954                };
2955                assert!(
2956                    read > 0,
2957                    "read must drain the populated pipe: {}",
2958                    std::io::Error::last_os_error()
2959                );
2960                offset += read as usize;
2961            }
2962            pipe.splice_pipe.as_mut().unwrap().in_pipe_pending = 0;
2963        }
2964
2965        fn request_pipe_capacity(
2966            fd: libc::c_int,
2967            requested: usize,
2968            force_growth_refusal: bool,
2969        ) -> usize {
2970            // SAFETY: `fd` is a live pipe descriptor owned by the test's
2971            // `SplicePipe`; fcntl neither retains the descriptor nor a pointer.
2972            let requested = if force_growth_refusal {
2973                -1
2974            } else {
2975                libc::c_int::try_from(requested).expect("requested pipe size must fit c_int")
2976            };
2977            let set_ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, requested) };
2978            if force_growth_refusal {
2979                assert_eq!(set_ret, -1, "negative pipe growth must be refused");
2980            } else if set_ret < 0 {
2981                eprintln!(
2982                    "F_SETPIPE_SZ({requested}) unavailable for test pipe: {}",
2983                    std::io::Error::last_os_error()
2984                );
2985            }
2986            // SAFETY: same live pipe descriptor as above.
2987            let actual = unsafe { libc::fcntl(fd, libc::F_GETPIPE_SZ) };
2988            assert!(actual > 0, "F_GETPIPE_SZ must succeed");
2989            actual as usize
2990        }
2991
2992        fn set_buffer(socket: &impl AsRawFd, opt: libc::c_int, bytes: libc::c_int) {
2993            let ret = unsafe {
2994                libc::setsockopt(
2995                    socket.as_raw_fd(),
2996                    libc::SOL_SOCKET,
2997                    opt,
2998                    &bytes as *const libc::c_int as *const libc::c_void,
2999                    std::mem::size_of::<libc::c_int>() as libc::socklen_t,
3000                )
3001            };
3002            assert_eq!(ret, 0, "setsockopt must succeed");
3003        }
3004
3005        /// A connected pair whose buffers are pinned tiny *before* the
3006        /// handshake: SO_RCVBUF on the listener (inherited by the accepted
3007        /// socket, so the advertised window starts small) and SO_SNDBUF on
3008        /// the connecting side. Together they bound how many bytes the leg
3009        /// can absorb at once to a few KiB, with no autotuning and no
3010        /// dependence on window-update timing.
3011        fn tiny_backend_pair() -> (StdTcpStream, StdTcpStream) {
3012            let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
3013            set_buffer(&listener, libc::SO_RCVBUF, 4096);
3014            let address = listener.local_addr().expect("listener local addr");
3015            let socket = socket2_connect_with_sndbuf(address, 4096);
3016            let (peer, _) = listener.accept().expect("accept test server");
3017            socket.set_nonblocking(true).expect("client nonblocking");
3018            peer.set_nonblocking(true).expect("server nonblocking");
3019            (peer, socket)
3020        }
3021
3022        fn socket2_connect_with_sndbuf(address: SocketAddr, bytes: libc::c_int) -> StdTcpStream {
3023            use std::os::unix::io::FromRawFd;
3024            let fd =
3025                unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_CLOEXEC, 0) };
3026            assert!(fd >= 0, "socket() must succeed");
3027            let socket = unsafe { StdTcpStream::from_raw_fd(fd) };
3028            set_buffer(&socket, libc::SO_SNDBUF, bytes);
3029            let SocketAddr::V4(v4) = address else {
3030                panic!("test listener must be IPv4")
3031            };
3032            let sin = libc::sockaddr_in {
3033                sin_family: libc::AF_INET as libc::sa_family_t,
3034                sin_port: v4.port().to_be(),
3035                sin_addr: libc::in_addr {
3036                    s_addr: u32::from_ne_bytes(v4.ip().octets()),
3037                },
3038                sin_zero: [0; 8],
3039            };
3040            let ret = unsafe {
3041                libc::connect(
3042                    fd,
3043                    &sin as *const libc::sockaddr_in as *const libc::sockaddr,
3044                    std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
3045                )
3046            };
3047            assert_eq!(
3048                ret,
3049                0,
3050                "connect() must succeed: {}",
3051                std::io::Error::last_os_error()
3052            );
3053            socket
3054        }
3055
3056        let (mut frontend_peer, frontend_socket) = connected_pair();
3057        let (mut backend_peer, backend_socket) = tiny_backend_pair();
3058
3059        let mut pool = Pool::with_capacity(2, 2, 4096);
3060        let backend_buffer = pool.checkout().expect("backend buffer");
3061        let frontend_buffer = pool.checkout().expect("frontend buffer");
3062        let address = "127.0.0.1:0".parse().expect("test address");
3063        let listener = Rc::new(RefCell::new(TestListener { address }));
3064
3065        let mut pipe = Pipe::new(
3066            backend_buffer,
3067            None,
3068            Some(TcpStream::from_std(backend_socket)),
3069            None,
3070            None,
3071            None,
3072            None,
3073            frontend_buffer,
3074            Token(0),
3075            TcpStream::from_std(frontend_socket),
3076            listener,
3077            Protocol::TCP,
3078            Ulid::generate(),
3079            Ulid::generate(),
3080            None,
3081            WebSocketContext::Tcp,
3082        );
3083        pipe.set_back_token(Token(1));
3084        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
3085        pipe.frontend_readiness.event = Ready::READABLE;
3086        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
3087
3088        let slot = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
3089        assert!(slot > 0, "sysconf(_SC_PAGESIZE) must succeed");
3090        {
3091            let splice_pipe = pipe
3092                .splice_pipe
3093                .as_mut()
3094                .expect("Protocol::TCP must allocate the splice kernel pipe");
3095            for fd in [splice_pipe.in_pipe[0], splice_pipe.out_pipe[0]] {
3096                let ret = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, slot as libc::c_int) };
3097                assert_eq!(ret, slot as libc::c_int, "F_SETPIPE_SZ(page) must succeed");
3098            }
3099            splice_pipe.capacity = slot;
3100        }
3101
3102        // The mandatory drain/retry consumes at most two slots in total. A
3103        // third keeps bytes queued for the independent multi-slot phase.
3104        let payload = vec![0x5a_u8; 3 * slot];
3105        frontend_peer
3106            .write_all(&payload)
3107            .expect("write frontend payload");
3108        for _ in 0..1000 {
3109            if unread_bytes(pipe.frontend.socket_ref()) >= payload.len() {
3110                break;
3111            }
3112            std::thread::sleep(Duration::from_millis(1));
3113        }
3114        assert_eq!(unread_bytes(pipe.frontend.socket_ref()), payload.len());
3115
3116        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
3117        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3118        let first = pipe.splice_in_pending();
3119        assert!(
3120            first > 0 && first <= slot,
3121            "one slot absorbed (got {first})"
3122        );
3123
3124        // Partial drain toward the backend keeps the slot occupied.
3125        {
3126            let pipe_read_end = pipe.splice_pipe.as_ref().unwrap().in_pipe[0];
3127            let backend = pipe.backend_socket.as_ref().unwrap();
3128            let (drained, res) = splice::splice_out(pipe_read_end, backend, 1);
3129            assert_eq!((drained, res), (1, SocketResult::Continue));
3130            pipe.splice_pipe.as_mut().unwrap().in_pipe_pending -= drained;
3131        }
3132
3133        pipe.frontend_readiness.interest.insert(Ready::READABLE);
3134        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3135        let still_queued = unread_bytes(pipe.frontend.socket_ref());
3136        assert_eq!(still_queued, payload.len() - first);
3137        assert!(
3138            pipe.frontend_readiness.event.is_readable(),
3139            "READABLE event must survive a pipe-full EAGAIN with {still_queued} bytes queued"
3140        );
3141        assert!(
3142            !pipe.frontend_readiness.interest.is_readable(),
3143            "READABLE interest must be parked while the pipe is full"
3144        );
3145        assert!(
3146            pipe.backend_readiness.event.is_writable()
3147                && pipe.backend_readiness.interest.is_writable(),
3148            "the backend must be armed writable to drain the pipe"
3149        );
3150
3151        // Mandatory recovery: drain through the production path until the
3152        // single slot is free. This must re-arm the frontend read while the
3153        // READABLE event preserved above drives the retry, with no readiness
3154        // injected by the test.
3155        let mut backend_buf = [0_u8; 16 * 1024];
3156        for _ in 0..1000 {
3157            assert_ne!(
3158                pipe.splice_backend_writable(&mut metrics),
3159                SessionResult::Close
3160            );
3161            loop {
3162                match backend_peer.read(&mut backend_buf) {
3163                    Ok(0) => panic!("backend peer closed while draining the pipe"),
3164                    Ok(_) => {}
3165                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
3166                    Err(error) => panic!("failed to read drained pipe bytes: {error}"),
3167                }
3168            }
3169            if pipe.splice_in_pending() == 0 {
3170                break;
3171            }
3172            std::thread::sleep(Duration::from_millis(1));
3173        }
3174        assert_eq!(
3175            pipe.splice_in_pending(),
3176            0,
3177            "the single-slot pipe must fully drain"
3178        );
3179        assert!(
3180            pipe.frontend_readiness.interest.is_readable()
3181                && pipe.frontend_readiness.event.is_readable(),
3182            "the real backend drain must re-arm the preserved frontend read"
3183        );
3184        let unread_before_retry = unread_bytes(pipe.frontend.socket_ref());
3185        assert_eq!(unread_before_retry, still_queued);
3186        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3187        let unread_after_retry = unread_bytes(pipe.frontend.socket_ref());
3188        assert!(
3189            unread_after_retry < unread_before_retry,
3190            "the preserved READABLE event must resume frontend progress after the drain \
3191             ({unread_before_retry} before, {unread_after_retry} after)"
3192        );
3193        assert!(pipe.splice_in_pending() > 0);
3194
3195        // Empty the recovered slot before starting the independent partial-
3196        // drain phase, then request sixteen page-derived slots. Pipe growth is
3197        // a capability rather than a production requirement, so use the
3198        // realised size and skip only this phase when the kernel denies it.
3199        discard_in_pipe(&mut pipe);
3200        let requested_capacity = slot.checked_mul(16).expect("pipe size must fit usize");
3201        let capacity = {
3202            let splice_pipe = pipe.splice_pipe.as_mut().unwrap();
3203            let in_capacity = request_pipe_capacity(
3204                splice_pipe.in_pipe[0],
3205                requested_capacity,
3206                force_growth_refusal,
3207            );
3208            let out_capacity = request_pipe_capacity(
3209                splice_pipe.out_pipe[0],
3210                requested_capacity,
3211                force_growth_refusal,
3212            );
3213            let capacity = in_capacity.min(out_capacity);
3214            splice_pipe.capacity = capacity;
3215            capacity
3216        };
3217        let minimum_capacity = slot
3218            .checked_mul(2)
3219            .expect("minimum pipe size must fit usize")
3220            .max(32 * 1024);
3221        if capacity < minimum_capacity {
3222            eprintln!(
3223                "skipping multi-slot partial-drain phase: realised capacity {capacity} is below \
3224                 required {minimum_capacity}"
3225            );
3226            return false;
3227        }
3228
3229        // Fill the pipe well past what the backend leg can absorb (tiny
3230        // pinned SO_SNDBUF + SO_RCVBUF, a few KiB in total), so that a drain
3231        // can only move part of it.
3232        frontend_peer
3233            .write_all(&vec![0x5a_u8; capacity])
3234            .expect("write more frontend payload");
3235        for _ in 0..1000 {
3236            if pipe.splice_in_pending() >= capacity / 2 {
3237                break;
3238            }
3239            pipe.frontend_readiness.interest.insert(Ready::READABLE);
3240            pipe.frontend_readiness.event.insert(Ready::READABLE);
3241            assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3242            std::thread::sleep(Duration::from_millis(1));
3243        }
3244        assert!(
3245            pipe.splice_in_pending() >= capacity / 2,
3246            "the pipe must hold at least half its realised capacity (got {})",
3247            pipe.splice_in_pending()
3248        );
3249        pipe.frontend_readiness.interest.remove(Ready::READABLE);
3250
3251        // One drain: the backend leg absorbs a few KiB (its pinned buffers)
3252        // then EAGAINs, leaving most of the pipe pending -- a partial drain
3253        // with no timing involved.
3254        let pending_before_drain = pipe.splice_in_pending();
3255        assert_ne!(
3256            pipe.splice_backend_writable(&mut metrics),
3257            SessionResult::Close
3258        );
3259        let pending = pipe.splice_in_pending();
3260        assert!(
3261            pending < pending_before_drain,
3262            "the backend must accept some bytes ({pending_before_drain} pending)"
3263        );
3264        assert!(
3265            pending > 0,
3266            "the drain must be partial, not complete (accepted {})",
3267            pending_before_drain - pending
3268        );
3269        assert!(
3270            pipe.frontend_readiness.interest.is_readable(),
3271            "a partial drain of the pipe must re-arm the frontend read"
3272        );
3273
3274        // Re-arming is the partial-drain contract. Data progress is a separate
3275        // assertion: release every occupied slot first, then compare FIONREAD
3276        // immediately around the retry so no stale pre-write baseline is used.
3277        discard_in_pipe(&mut pipe);
3278        for _ in 0..1000 {
3279            if unread_bytes(pipe.frontend.socket_ref()) > 0 {
3280                break;
3281            }
3282            std::thread::sleep(Duration::from_millis(1));
3283        }
3284        let unread_before_retry = unread_bytes(pipe.frontend.socket_ref());
3285        assert!(
3286            unread_before_retry > 0,
3287            "request bytes must remain queued for the free-slot retry"
3288        );
3289        assert!(
3290            pipe.frontend_readiness.interest.is_readable()
3291                && pipe.frontend_readiness.event.is_readable(),
3292            "the partial backend drain must leave the frontend retry runnable"
3293        );
3294        assert_ne!(pipe.splice_readable(&mut metrics), SessionResult::Close);
3295        let unread_after_retry = unread_bytes(pipe.frontend.socket_ref());
3296        assert!(
3297            unread_after_retry < unread_before_retry,
3298            "a retry with free pipe slots must consume request bytes \
3299             ({unread_before_retry} before, {unread_after_retry} after)"
3300        );
3301        assert!(pipe.splice_in_pending() > 0);
3302        true
3303    }
3304
3305    /// Frontend-direction twin of the test above: `splice_readable` must
3306    /// keep the READABLE event on a pipe-full EAGAIN, and a *partial*
3307    /// `splice_backend_writable` drain must re-arm the frontend read (the
3308    /// response direction already did so via `splice_writable`).
3309    #[cfg(all(target_os = "linux", feature = "splice"))]
3310    #[test]
3311    fn splice_readable_keeps_readable_event_when_pipe_slots_are_exhausted() {
3312        exercise_splice_readable_pipe_full_recovery(false);
3313    }
3314
3315    #[cfg(all(target_os = "linux", feature = "splice"))]
3316    #[test]
3317    fn splice_readable_recovers_when_pipe_growth_is_refused() {
3318        assert!(!exercise_splice_readable_pipe_full_recovery(true));
3319    }
3320    /// The access log's backend address is the one recorded when the backend
3321    /// was chosen, never a `getpeername(2)` on the backend socket: that call
3322    /// cost one syscall per session, and failed with `ENOTCONN` (logging no
3323    /// address) on a backend still connecting or already reset.
3324    ///
3325    /// The backend socket here is refused: nothing listens on its port, so
3326    /// on loopback it is reset before the first assertion and `peer_addr`
3327    /// can only fail. The address must still be reported, first from the
3328    /// `Backend` handed to `Pipe::new` (the WebSocket upgrade path), then from
3329    /// `set_backend_address` (the TCP proxy's dial, overwritten on a retry).
3330    ///
3331    /// To SEE THIS RED: make `Pipe::get_backend_address` return
3332    /// `self.backend_socket.as_ref().and_then(|backend| backend.peer_addr().ok())`
3333    /// again. The first assertion then fails with `None`.
3334    #[test]
3335    fn backend_address_is_reported_without_asking_the_socket() {
3336        let refused = {
3337            let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind a throwaway listener");
3338            listener
3339                .local_addr()
3340                .expect("throwaway listener local addr")
3341        };
3342        let backend_socket = TcpStream::connect(refused).expect("a nonblocking connect starts");
3343        let (_frontend_peer, frontend_socket) = connected_pair();
3344
3345        let mut pool = Pool::with_capacity(2, 2, 4096);
3346        let backend_buffer = pool.checkout().expect("backend buffer");
3347        let frontend_buffer = pool.checkout().expect("frontend buffer");
3348        let address = "127.0.0.1:0".parse().expect("test address");
3349        let listener = Rc::new(RefCell::new(TestListener { address }));
3350        let backend = Rc::new(RefCell::new(Backend::new(
3351            "backend-refused",
3352            refused,
3353            None,
3354            None,
3355            None,
3356        )));
3357
3358        let mut pipe = Pipe::new(
3359            backend_buffer,
3360            Some("backend-refused".to_owned()),
3361            Some(backend_socket),
3362            Some(backend),
3363            None,
3364            None,
3365            None,
3366            frontend_buffer,
3367            Token(0),
3368            TcpStream::from_std(frontend_socket),
3369            listener,
3370            Protocol::HTTP,
3371            Ulid::generate(),
3372            Ulid::generate(),
3373            None,
3374            WebSocketContext::Tcp,
3375        );
3376        assert_eq!(
3377            pipe.get_backend_address(),
3378            Some(refused),
3379            "the address of the backend handed to Pipe::new must be reported \
3380             even though its socket was refused"
3381        );
3382
3383        let retried: SocketAddr = "192.0.2.7:4242".parse().expect("retry address");
3384        pipe.set_backend_address(Some(retried));
3385        assert_eq!(
3386            pipe.get_backend_address(),
3387            Some(retried),
3388            "a later dial must replace the reported backend address"
3389        );
3390    }
3391}