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};
18
19use crate::metrics::names;
20use crate::{
21    L7Proxy, ListenerHandler, Protocol, Readiness, SessionMetrics, SessionResult, StateResult,
22    backends::Backend,
23    pool::Checkout,
24    protocol::{SessionState, http::parser::Method},
25    socket::{SocketHandler, SocketResult, TransportProtocol, stats::socket_rtt},
26    sozu_command::ready::Ready,
27    timer::TimeoutContainer,
28};
29
30#[cfg(all(target_os = "linux", feature = "splice"))]
31use crate::splice::{self, SplicePipe};
32
33/// This macro is defined uniquely in this module to help the tracking of
34/// pipelining issues inside Sōzu. Colored output uses bold bright-white
35/// (uniform across every protocol) for the protocol label, light grey for the
36/// `Session` keyword, gray for keys and bright white for values. The
37/// `[ulid - - -]` context comes first to stay aligned with `MUX-*` and
38/// `SOCKET` log lines.
39macro_rules! log_context {
40    ($self:expr) => {{
41        let (open, reset, grey, gray, white) = ansi_palette();
42        format!(
43            "{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 >>>",
44            open = open,
45            reset = reset,
46            grey = grey,
47            gray = gray,
48            white = white,
49            ctx = $self.log_context(),
50            address = $self.session_address.map(|addr| addr.to_string()).unwrap_or_else(|| "<none>".to_string()),
51            frontend = $self.frontend_token.0,
52            frontend_readiness = $self.frontend_readiness,
53            frontend_status = $self.frontend_status,
54            backend = $self.backend_token.map(|token| token.0.to_string()).unwrap_or_else(|| "<none>".to_string()),
55            backend_status = $self.backend_status,
56            backend_readiness = $self.backend_readiness,
57        )
58    }};
59}
60
61#[derive(PartialEq, Eq)]
62pub enum SessionStatus {
63    Normal,
64    DefaultAnswer,
65}
66
67#[derive(Copy, Clone, Debug)]
68enum ConnectionStatus {
69    Normal,
70    ReadOpen,
71    WriteOpen,
72    Closed,
73}
74
75/// matches sozu_command_lib::logging::access_logs::EndpointRecords
76pub enum WebSocketContext {
77    Http {
78        method: Option<Method>,
79        authority: Option<String>,
80        path: Option<String>,
81        status: Option<u16>,
82        reason: Option<String>,
83    },
84    Tcp,
85}
86
87pub struct Pipe<Front: SocketHandler, L: ListenerHandler> {
88    backend_buffer: Checkout,
89    backend_id: Option<String>,
90    pub backend_readiness: Readiness,
91    backend_socket: Option<TcpStream>,
92    backend_status: ConnectionStatus,
93    backend_token: Option<Token>,
94    pub backend: Option<Rc<RefCell<Backend>>>,
95    cluster_id: Option<String>,
96    pub container_backend_timeout: Option<TimeoutContainer>,
97    pub container_frontend_timeout: Option<TimeoutContainer>,
98    frontend_buffer: Checkout,
99    pub frontend_readiness: Readiness,
100    frontend_status: ConnectionStatus,
101    frontend_token: Token,
102    frontend: Front,
103    listener: Rc<RefCell<L>>,
104    protocol: Protocol,
105    /// Connection/session ULID inherited from the parent mux or handshake.
106    /// Emitted in the first slot of the legacy log-context bracket.
107    session_id: Ulid,
108    request_id: Ulid,
109    session_address: Option<SocketAddr>,
110    websocket_context: WebSocketContext,
111    /// Connection-scoped TLS metadata captured at handshake completion,
112    /// inherited from the upstream mux `HttpContext` when `Pipe` is created
113    /// via WSS upgrade. `None` on plaintext paths (plain TCP, plain WS,
114    /// proxy-protocol) where no TLS was terminated by Sōzu.
115    tls_version: Option<&'static str>,
116    tls_cipher: Option<&'static str>,
117    /// Negotiated SNI hostname, pre-lowercased, no port. `None` on plaintext
118    /// paths or when the client omitted the SNI extension.
119    tls_sni: Option<String>,
120    tls_alpn: Option<&'static str>,
121    /// Override for the access-log tags lookup key. `None` (every path but
122    /// TCP SNI-preread) falls back to the historical bare listener-address
123    /// key; SNI-routed TCP sessions carry the matched frontend's composed
124    /// key (`sni_tags_key` in `lib/src/tcp.rs`) so one listener's many
125    /// SNI/ALPN fronts each log their own tags.
126    tags_key: Option<String>,
127    /// Kernel-pipe pair used for zero-copy `splice(2)` forwarding on
128    /// `Protocol::TCP` listeners. Allocated lazily in `new()` and
129    /// `None` for WebSocket-after-upgrade paths or when allocation
130    /// failed (caller falls back to the buffered path).
131    #[cfg(all(target_os = "linux", feature = "splice"))]
132    splice_pipe: Option<SplicePipe>,
133}
134
135impl<Front: SocketHandler, L: ListenerHandler> Pipe<Front, L> {
136    /// Instantiate a new Pipe SessionState with:
137    ///
138    /// - frontend_interest: READABLE | WRITABLE | HUP | ERROR
139    /// - frontend_event: EMPTY
140    /// - backend_interest: READABLE | WRITABLE | HUP | ERROR
141    /// - backend_event: EMPTY
142    ///
143    /// Remember to set the events from the previous State!
144    #[allow(clippy::too_many_arguments)]
145    pub fn new(
146        backend_buffer: Checkout,
147        backend_id: Option<String>,
148        backend_socket: Option<TcpStream>,
149        backend: Option<Rc<RefCell<Backend>>>,
150        container_backend_timeout: Option<TimeoutContainer>,
151        container_frontend_timeout: Option<TimeoutContainer>,
152        cluster_id: Option<String>,
153        frontend_buffer: Checkout,
154        frontend_token: Token,
155        frontend: Front,
156        listener: Rc<RefCell<L>>,
157        protocol: Protocol,
158        session_id: Ulid,
159        request_id: Ulid,
160        session_address: Option<SocketAddr>,
161        websocket_context: WebSocketContext,
162    ) -> Pipe<Front, L> {
163        let frontend_status = ConnectionStatus::Normal;
164        let backend_status = if backend_socket.is_none() {
165            ConnectionStatus::Closed
166        } else {
167            ConnectionStatus::Normal
168        };
169
170        let mut session = Pipe {
171            backend_buffer,
172            backend_id,
173            backend_readiness: Readiness {
174                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
175                event: Ready::EMPTY,
176            },
177            backend_socket,
178            backend_status,
179            backend_token: None,
180            backend,
181            cluster_id,
182            container_backend_timeout,
183            container_frontend_timeout,
184            frontend_buffer,
185            frontend_readiness: Readiness {
186                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
187                event: Ready::EMPTY,
188            },
189            frontend_status,
190            frontend_token,
191            frontend,
192            listener,
193            protocol,
194            session_id,
195            request_id,
196            session_address,
197            websocket_context,
198            tls_version: None,
199            tls_cipher: None,
200            tls_sni: None,
201            tls_alpn: None,
202            tags_key: None,
203            #[cfg(all(target_os = "linux", feature = "splice"))]
204            splice_pipe: if protocol == Protocol::TCP {
205                SplicePipe::new()
206            } else {
207                None
208            },
209        };
210
211        session.arm_inherited_buffer_writes();
212
213        trace!("{} created pipe", log_context!(session));
214        session
215    }
216
217    fn arm_inherited_buffer_writes(&mut self) {
218        if self.backend_buffer.available_data() > 0 {
219            self.frontend_readiness.arm_writable();
220        }
221        if self.frontend_buffer.available_data() > 0 && self.backend_socket.is_some() {
222            self.backend_readiness.arm_writable();
223        }
224    }
225
226    pub fn restore_readiness_events(&mut self, frontend_event: Ready, backend_event: Ready) {
227        self.frontend_readiness.event = frontend_event;
228        self.backend_readiness.event = backend_event;
229        self.arm_inherited_buffer_writes();
230    }
231
232    /// Stamp connection-scoped TLS metadata onto the pipe for access-log
233    /// emission. Two caller classes exist:
234    ///
235    /// - the HTTPS→WSS upgrade path (`https.rs::upgrade_mux`), which stamps
236    ///   all four fields captured at handshake time from the prior mux
237    ///   `HttpContext` (version/cipher/SNI/ALPN);
238    /// - the TCP SNI-preread upgrade paths (`tcp.rs::upgrade_send` and
239    ///   `tcp.rs::build_pipe_from_preread`), which stamp the routed SNI and
240    ///   the offered-ALPN label with `version`/`cipher` deliberately `None`:
241    ///   Sōzu never terminates that TLS session — it only parses the
242    ///   ClientHello in passthrough — so it never learns the negotiated
243    ///   parameters.
244    ///
245    /// Plain (non-SNI-routed) TCP, plain WS, and proxy-protocol paths never
246    /// call this, so their access logs continue to emit `None` for all TLS
247    /// fields.
248    pub fn set_tls_metadata(
249        &mut self,
250        version: Option<&'static str>,
251        cipher: Option<&'static str>,
252        sni: Option<String>,
253        alpn: Option<&'static str>,
254    ) {
255        self.tls_version = version;
256        self.tls_cipher = cipher;
257        self.tls_sni = sni;
258        self.tls_alpn = alpn;
259    }
260
261    pub fn front_socket(&self) -> &TcpStream {
262        self.frontend.socket_ref()
263    }
264
265    pub fn front_socket_mut(&mut self) -> &mut TcpStream {
266        self.frontend.socket_mut()
267    }
268
269    pub fn back_socket(&self) -> Option<&TcpStream> {
270        self.backend_socket.as_ref()
271    }
272
273    pub fn back_socket_mut(&mut self) -> Option<&mut TcpStream> {
274        self.backend_socket.as_mut()
275    }
276
277    pub fn set_back_socket(&mut self, socket: TcpStream) {
278        self.backend_socket = Some(socket);
279        self.backend_status = ConnectionStatus::Normal;
280    }
281
282    pub fn back_token(&self) -> Vec<Token> {
283        self.backend_token.iter().cloned().collect()
284    }
285
286    fn reset_timeouts(&mut self) {
287        if let Some(t) = self.container_frontend_timeout.as_mut()
288            && !t.reset()
289        {
290            error!(
291                "{} Could not reset front timeout (pipe)",
292                log_context!(self)
293            );
294        }
295
296        if let Some(t) = self.container_backend_timeout.as_mut()
297            && !t.reset()
298        {
299            error!("{} Could not reset back timeout (pipe)", log_context!(self));
300        }
301    }
302
303    pub fn set_cluster_id(&mut self, cluster_id: Option<String>) {
304        self.cluster_id = cluster_id;
305    }
306
307    /// Override the access-log tags lookup key (see the `tags_key` field
308    /// doc). Called from `lib/src/tcp.rs`'s pipe-building upgrade paths for
309    /// SNI-routed sessions; `None` (the default) keeps the historical
310    /// bare-address lookup.
311    pub fn set_tags_key(&mut self, tags_key: Option<String>) {
312        self.tags_key = tags_key;
313    }
314
315    pub fn set_backend_id(&mut self, backend_id: Option<String>) {
316        self.backend_id = backend_id;
317    }
318
319    pub fn set_back_token(&mut self, token: Token) {
320        self.backend_token = Some(token);
321    }
322
323    pub fn get_session_address(&self) -> Option<SocketAddr> {
324        self.session_address
325            .or_else(|| self.frontend.socket_ref().peer_addr().ok())
326    }
327
328    pub fn get_backend_address(&self) -> Option<SocketAddr> {
329        self.backend_socket
330            .as_ref()
331            .and_then(|backend| backend.peer_addr().ok())
332    }
333
334    fn protocol_string(&self) -> &'static str {
335        match self.protocol {
336            Protocol::TCP => "TCP",
337            Protocol::HTTP => "WS",
338            Protocol::HTTPS => match self.frontend.protocol() {
339                TransportProtocol::Ssl2 => "WSS-SSL2",
340                TransportProtocol::Ssl3 => "WSS-SSL3",
341                TransportProtocol::Tls1_0 => "WSS-TLS1.0",
342                TransportProtocol::Tls1_1 => "WSS-TLS1.1",
343                TransportProtocol::Tls1_2 => "WSS-TLS1.2",
344                TransportProtocol::Tls1_3 => "WSS-TLS1.3",
345                _ => unreachable!(),
346            },
347            _ => unreachable!(),
348        }
349    }
350
351    pub fn log_request(&self, metrics: &SessionMetrics, error: bool, message: Option<&str>) {
352        let listener = self.listener.borrow();
353        let context = self.log_context();
354        let endpoint = self.log_endpoint();
355        metrics.register_end_of_session(&context);
356        // TCP SNI-preread sessions carry the matched frontend's own tags
357        // key (`set_tags_key`); every other path keeps the historical
358        // bare-address lookup.
359        let address_key = listener.get_addr().to_string();
360        let tags_key = self.tags_key.as_deref().unwrap_or(&address_key);
361        log_access!(
362            error,
363            on_failure: { incr!(names::access_logs::UNSENT) },
364            message,
365            context,
366            session_address: self.get_session_address(),
367            backend_address: self.get_backend_address(),
368            protocol: self.protocol_string(),
369            endpoint,
370            tags: listener.get_tags(tags_key),
371            client_rtt: socket_rtt(self.front_socket()),
372            server_rtt: self.backend_socket.as_ref().and_then(socket_rtt),
373            service_time: metrics.service_time(),
374            response_time: metrics.backend_response_time(),
375            request_time: metrics.request_time(),
376            start_time_ns: metrics.start_wall_ns(),
377            bytes_in: metrics.bin,
378            bytes_out: metrics.bout,
379            user_agent: None,
380            x_request_id: None,
381            // Pipe is post-upgrade; the TLS metadata was captured once at
382            // handshake in `https.rs::upgrade_handshake` and plumbed through
383            // via `set_tls_metadata`. Plaintext paths leave these fields as
384            // `None` — matching the TCP log shape.
385            tls_version: self.tls_version,
386            tls_cipher: self.tls_cipher,
387            tls_sni: self.tls_sni.as_deref(),
388            tls_alpn: self.tls_alpn,
389            xff_chain: None,
390            otel: None,
391        );
392    }
393
394    pub fn log_request_success(&self, metrics: &SessionMetrics) {
395        self.log_request(metrics, false, None);
396    }
397
398    pub fn log_request_error(&self, metrics: &SessionMetrics, message: &str) {
399        incr!(names::pipe::ERRORS);
400        error!(
401            "{} Could not process request properly got: {}",
402            log_context!(self),
403            message
404        );
405        self.print_state(self.protocol_string());
406        self.log_request(metrics, true, Some(message));
407    }
408
409    /// Access-log wrapper for benign idle-timeout tear-downs.
410    ///
411    /// Unlike `log_request_error`, this path logs at `debug!` and skips the
412    /// state dump — an idle pipe hitting its front/back_timeout is expected
413    /// behaviour (e.g. a WebSocket with no keepalive) and should not pollute
414    /// the error stream.
415    pub fn log_request_timeout(&self, metrics: &SessionMetrics, message: &str) {
416        debug!("{} pipe timeout: {}", log_context!(self), message);
417        self.log_request(metrics, true, Some(message));
418    }
419
420    /// Bytes currently sitting inside the `splice` frontend→backend
421    /// kernel pipe (`0` if splice is disabled or the pipe was not
422    /// allocated). Counted as "request in flight" by `check_connections`
423    /// so a half-closed session stays alive until the kernel drains.
424    #[cfg(all(target_os = "linux", feature = "splice"))]
425    fn splice_in_pending(&self) -> usize {
426        self.splice_pipe
427            .as_ref()
428            .map(|p| p.in_pipe_pending)
429            .unwrap_or(0)
430    }
431    #[cfg(not(all(target_os = "linux", feature = "splice")))]
432    fn splice_in_pending(&self) -> usize {
433        0
434    }
435
436    /// Bytes currently sitting inside the `splice` backend→frontend
437    /// kernel pipe. Counterpart to `splice_in_pending` for the response
438    /// direction.
439    #[cfg(all(target_os = "linux", feature = "splice"))]
440    fn splice_out_pending(&self) -> usize {
441        self.splice_pipe
442            .as_ref()
443            .map(|p| p.out_pipe_pending)
444            .unwrap_or(0)
445    }
446    #[cfg(not(all(target_os = "linux", feature = "splice")))]
447    fn splice_out_pending(&self) -> usize {
448        0
449    }
450
451    /// Realised kernel-pipe capacity per direction (`0` if splice is
452    /// disabled). Drives the "pipe is full" backpressure check in the
453    /// splice readable methods and the per-call `len` for `splice_in`.
454    #[cfg(all(target_os = "linux", feature = "splice"))]
455    fn splice_capacity(&self) -> usize {
456        self.splice_pipe.as_ref().map(|p| p.capacity).unwrap_or(0)
457    }
458
459    /// Tear down both readiness trackers ahead of a `SessionResult::Close`.
460    ///
461    /// This is the *write-only-shutdown discipline* (CLAUDE.md gotcha: never
462    /// `shutdown(Shutdown::Both)` on a TLS frontend — it emits a TCP RST that
463    /// truncates the already-queued response). `Pipe` never issues an explicit
464    /// `shutdown`; it closes purely by clearing interest+event so the event
465    /// loop stops driving I/O and lets the kernel flush queued bytes, with the
466    /// peer close arriving via the normal read path. The post-condition
467    /// asserts both trackers are fully cleared.
468    fn reset_readiness_for_close(&mut self) {
469        self.frontend_readiness.reset();
470        self.backend_readiness.reset();
471        debug_assert!(
472            self.frontend_readiness.interest.is_empty() && self.frontend_readiness.event.is_empty(),
473            "frontend readiness must be fully cleared on close (write-only-shutdown discipline)"
474        );
475        debug_assert!(
476            self.backend_readiness.interest.is_empty() && self.backend_readiness.event.is_empty(),
477            "backend readiness must be fully cleared on close (write-only-shutdown discipline)"
478        );
479    }
480
481    /// Wether the session should be kept open, depending on endpoints status
482    /// and buffer usage (both in memory and in kernel)
483    pub fn check_connections(&self) -> bool {
484        // In-flight accounting must never see more *buffered* bytes than the
485        // backing Checkout buffer can hold. We intentionally do NOT bound the
486        // splice-pending counters by the pipe `capacity`: a kernel pipe buffers
487        // well beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves
488        // skb-backed GRO segments, so `splice_*_pending` legitimately exceeds it
489        // (see `splice_readable`). A violation here means a `fill`/`consume`
490        // elsewhere desynced the counters, corrupting the keep-alive decision.
491        debug_assert!(
492            self.frontend_buffer.available_data() <= self.frontend_buffer.capacity(),
493            "frontend buffered data exceeds its capacity"
494        );
495        debug_assert!(
496            self.backend_buffer.available_data() <= self.backend_buffer.capacity(),
497            "backend buffered data exceeds its capacity"
498        );
499
500        let request_is_inflight = self.frontend_buffer.available_data() > 0
501            || self.frontend_readiness.event.is_readable()
502            || self.splice_in_pending() > 0;
503        let response_is_inflight = self.backend_buffer.available_data() > 0
504            || self.backend_readiness.event.is_readable()
505            || self.splice_out_pending() > 0;
506        match (self.frontend_status, self.backend_status) {
507            (ConnectionStatus::Normal, ConnectionStatus::Normal) => true,
508            (ConnectionStatus::Normal, ConnectionStatus::ReadOpen) => true,
509            (ConnectionStatus::Normal, ConnectionStatus::WriteOpen) => {
510                // technically we should keep it open, but we'll assume that if the front
511                // is not readable and there is no in flight data front -> back or back -> front,
512                // we'll close the session, otherwise it interacts badly with HTTP connections
513                // with Connection: close header and no Content-length
514                request_is_inflight || response_is_inflight
515            }
516            (ConnectionStatus::Normal, ConnectionStatus::Closed) => response_is_inflight,
517
518            (ConnectionStatus::WriteOpen, ConnectionStatus::Normal) => {
519                // technically we should keep it open, but we'll assume that if the back
520                // is not readable and there is no in flight data back -> front or front -> back, we'll close the session
521                request_is_inflight || response_is_inflight
522            }
523            (ConnectionStatus::WriteOpen, ConnectionStatus::ReadOpen) => true,
524            (ConnectionStatus::WriteOpen, ConnectionStatus::WriteOpen) => {
525                request_is_inflight || response_is_inflight
526            }
527            (ConnectionStatus::WriteOpen, ConnectionStatus::Closed) => response_is_inflight,
528
529            (ConnectionStatus::ReadOpen, ConnectionStatus::Normal) => true,
530            (ConnectionStatus::ReadOpen, ConnectionStatus::ReadOpen) => false,
531            (ConnectionStatus::ReadOpen, ConnectionStatus::WriteOpen) => true,
532            (ConnectionStatus::ReadOpen, ConnectionStatus::Closed) => false,
533
534            (ConnectionStatus::Closed, ConnectionStatus::Normal) => request_is_inflight,
535            (ConnectionStatus::Closed, ConnectionStatus::ReadOpen) => false,
536            (ConnectionStatus::Closed, ConnectionStatus::WriteOpen) => request_is_inflight,
537            (ConnectionStatus::Closed, ConnectionStatus::Closed) => false,
538        }
539    }
540
541    pub fn frontend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
542        self.frontend_status = ConnectionStatus::Closed;
543        // The frontend hung up: its status is now terminal regardless of
544        // which branch we take below (mirrors `backend_hup`).
545        debug_assert!(
546            matches!(self.frontend_status, ConnectionStatus::Closed),
547            "frontend_hup must mark the frontend Closed"
548        );
549        // EPOLLRDHUP only means the client sent FIN; on a loaded event loop it
550        // can coalesce with the payload tail into the same epoll batch, so
551        // bytes may still sit in `frontend_buffer` (already read), in the
552        // kernel receive buffer (not read yet, signalled by a pending
553        // READABLE event), or in the splice `in_pipe` (`splice_in_pending`)
554        // — see the sibling `SocketResult::Closed` drains in `readable` and
555        // `splice_readable` below and `check_connections`'s
556        // `request_is_inflight` (sozu-proxy/sozu#1290, the HUP-path variant
557        // of the same close-before-flush truncation).
558        let request_is_inflight = self.frontend_buffer.available_data() > 0
559            || self.frontend_readiness.event.is_readable()
560            || self.splice_in_pending() > 0;
561        if request_is_inflight && self.backend_socket.is_some() {
562            // Positive space: the branch that keeps the session alive must
563            // never be entered without something left to drain.
564            debug_assert!(
565                self.frontend_buffer.available_data() > 0
566                    || self.frontend_readiness.event.is_readable()
567                    || self.splice_in_pending() > 0,
568                "drain branch entered without any observable in-flight request bytes"
569            );
570            if self.frontend_readiness.event.is_readable() {
571                // Keep reading: the kernel still has the tail of the payload
572                // queued behind the FIN. The `SocketResult::Closed` arm in
573                // `readable` finishes the lifecycle once that tail hits EOF.
574                self.frontend_readiness.interest.insert(Ready::READABLE);
575            }
576            self.backend_readiness.arm_writable();
577            debug!(
578                "{} Pipe::frontend_hup: frontend connection closed, keeping alive due to inflight request data.",
579                log_context!(self)
580            );
581            SessionResult::Continue
582        } else {
583            // Negative space: closing outright must never drop bytes still
584            // queued for a live backend.
585            debug_assert!(
586                self.backend_socket.is_none() || self.frontend_buffer.available_data() == 0,
587                "close branch entered with backend present but request bytes still queued"
588            );
589            self.log_request_success(metrics);
590            SessionResult::Close
591        }
592    }
593
594    pub fn backend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
595        self.backend_status = ConnectionStatus::Closed;
596        // The backend hung up: its status is now terminal regardless of which
597        // keep-alive branch we take below.
598        debug_assert!(
599            matches!(self.backend_status, ConnectionStatus::Closed),
600            "backend_hup must mark the backend Closed"
601        );
602        let pipe_has_data = self.splice_out_pending() > 0;
603        if self.backend_buffer.available_data() == 0 && !pipe_has_data {
604            // No buffered or in-kernel response data: there is nothing left to
605            // drain toward the frontend on this no-data branch.
606            debug_assert_eq!(
607                self.backend_buffer.available_data(),
608                0,
609                "no-data branch entered with response bytes still buffered"
610            );
611            if self.backend_readiness.event.is_readable() {
612                self.backend_readiness.interest.insert(Ready::READABLE);
613                debug!(
614                    "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in kernel.",
615                    log_context!(self)
616                );
617                SessionResult::Continue
618            } else {
619                self.log_request_success(metrics);
620                SessionResult::Close
621            }
622        } else {
623            debug!(
624                "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in buffers.",
625                log_context!(self)
626            );
627            self.frontend_readiness.arm_writable();
628            if self.backend_readiness.event.is_readable() {
629                self.backend_readiness.interest.insert(Ready::READABLE);
630            }
631            SessionResult::Continue
632        }
633    }
634
635    /// Shared tail of `readable`'s and `splice_readable`'s
636    /// `SocketResult::Closed` arms: the frontend read side just observed EOF.
637    /// Transition `frontend_status` to the half-closed state, clear the
638    /// READABLE interest/event (the read side is done), arm the backend
639    /// writable when bytes are still queued for it (`has_pending` —
640    /// `frontend_buffer.available_data() > 0` for the buffered path,
641    /// `splice_in_pending() > 0` for the splice path), and defer teardown to
642    /// `check_connections`, which closes only once nothing is left in flight
643    /// (mirrors `backend_hup`'s drain branch).
644    fn close_frontend_read_side(
645        &mut self,
646        metrics: &mut SessionMetrics,
647        has_pending: bool,
648    ) -> SessionResult {
649        self.frontend_status = match self.frontend_status {
650            ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
651            ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
652            s => s,
653        };
654        self.frontend_readiness.event.remove(Ready::READABLE);
655        self.frontend_readiness.interest.remove(Ready::READABLE);
656        if has_pending && self.backend_socket.is_some() {
657            self.backend_readiness.arm_writable();
658        }
659        if !self.check_connections() {
660            self.reset_readiness_for_close();
661            self.log_request_success(metrics);
662            return SessionResult::Close;
663        }
664        SessionResult::Continue
665    }
666
667    // Read content from the session
668    pub fn readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
669        // Inherited preread bytes (e.g. SNI ClientHello replay) sit in
670        // `frontend_buffer`; splice never drains that userspace buffer, so it
671        // must be empty before the fast path takes over.
672        #[cfg(all(target_os = "linux", feature = "splice"))]
673        if self.protocol == Protocol::TCP
674            && self.splice_pipe.is_some()
675            && self.frontend_buffer.available_data() == 0
676        {
677            return self.splice_readable(metrics);
678        }
679
680        self.reset_timeouts();
681
682        trace!("{} pipe readable", log_context!(self));
683        if self.frontend_buffer.available_space() == 0 {
684            self.frontend_readiness.interest.remove(Ready::READABLE);
685            self.backend_readiness.arm_writable();
686            return SessionResult::Continue;
687        }
688
689        let space_before = self.frontend_buffer.available_space();
690        let data_before = self.frontend_buffer.available_data();
691        let bin_before = metrics.bin;
692        let (sz, res) = self.frontend.socket_read(self.frontend_buffer.space());
693        // `socket_read` fills `buf[..]` and returns `min(read, buf.len())`; it
694        // can never report more bytes than the space slice it was handed.
695        debug_assert!(
696            sz <= space_before,
697            "frontend socket_read reported more bytes ({sz}) than the buffer space offered ({space_before})"
698        );
699        debug!("{} Read {} bytes", log_context!(self), sz);
700
701        if sz > 0 {
702            //FIXME: replace with copy()
703            self.frontend_buffer.fill(sz);
704            // `fill(sz)` with `sz <= available_space` moves exactly `sz` bytes
705            // from free space into readable data — no truncation, no growth.
706            debug_assert_eq!(
707                self.frontend_buffer.available_data(),
708                data_before + sz,
709                "fill must grow readable data by exactly the bytes read"
710            );
711
712            count!(names::backend::BYTES_IN, sz as i64);
713            metrics.bin += sz;
714            // Front→proxy ingress metric advances by exactly the bytes read.
715            debug_assert_eq!(
716                metrics.bin,
717                bin_before + sz,
718                "metrics.bin must advance by exactly the bytes read"
719            );
720
721            if self.frontend_buffer.available_space() == 0 {
722                self.frontend_readiness.interest.remove(Ready::READABLE);
723            }
724            self.backend_readiness.arm_writable();
725        } else {
726            self.frontend_readiness.event.remove(Ready::READABLE);
727
728            if res == SocketResult::Continue {
729                self.frontend_status = match self.frontend_status {
730                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
731                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
732                    s => s,
733                };
734            }
735        }
736
737        if !self.check_connections() {
738            self.reset_readiness_for_close();
739            self.log_request_success(metrics);
740            return SessionResult::Close;
741        }
742
743        match res {
744            SocketResult::Error => {
745                self.reset_readiness_for_close();
746                self.log_request_error(metrics, "front socket read error");
747                return SessionResult::Close;
748            }
749            SocketResult::Closed => {
750                // The frontend read side closed (EOF). Bytes it already
751                // delivered may still be queued in `frontend_buffer` for the
752                // backend; returning `Close` here unconditionally dropped
753                // them, silently truncating the stream whenever a front->back
754                // tail was still in flight (sozu-proxy/sozu#1279: a
755                // payload coalesced with the SNI ClientHello is the
756                // reproducer, but the drop hit any plain-TCP upload racing a
757                // frontend close). Transition to the half-closed `WriteOpen`
758                // status (same mapping as the zero-byte `Continue` read above),
759                // arm the backend writable to flush the queue, and defer the
760                // teardown to `check_connections`, which closes only once
761                // nothing is in flight (mirrors `backend_hup`'s drain branch).
762                // `frontend_hup` (EPOLLRDHUP, above) has the same drain
763                // requirement for the same reason: FIN can coalesce with the
764                // payload tail into one epoll batch on a loaded event loop.
765                // So does `splice_readable`'s own `Closed` arm, for bytes
766                // sitting in the kernel `in_pipe` instead of `frontend_buffer`.
767                let has_pending = self.frontend_buffer.available_data() > 0;
768                return self.close_frontend_read_side(metrics, has_pending);
769            }
770            SocketResult::WouldBlock => {
771                self.frontend_readiness.event.remove(Ready::READABLE);
772            }
773            SocketResult::Continue => {}
774        };
775
776        self.backend_readiness.arm_writable();
777        SessionResult::Continue
778    }
779
780    // Forward content to session
781    pub fn writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
782        // Mirror of the front-side preread gate: splice never drains the
783        // userspace `backend_buffer`, so it must be empty before the fast
784        // path takes over.
785        #[cfg(all(target_os = "linux", feature = "splice"))]
786        if self.protocol == Protocol::TCP
787            && self.splice_pipe.is_some()
788            && self.backend_buffer.available_data() == 0
789        {
790            return self.splice_writable(metrics);
791        }
792
793        trace!("{} Pipe writable", log_context!(self));
794        if self.backend_buffer.available_data() == 0 {
795            self.backend_readiness.interest.insert(Ready::READABLE);
796            self.frontend_readiness.interest.remove(Ready::WRITABLE);
797            return SessionResult::Continue;
798        }
799
800        let queued_total = self.backend_buffer.available_data();
801        let mut sz = 0usize;
802        let mut res = SocketResult::Continue;
803        while res == SocketResult::Continue {
804            // no more data in buffer, stop here
805            if self.backend_buffer.available_data() == 0 {
806                count!(names::backend::BYTES_OUT, sz as i64);
807                metrics.bout += sz;
808                self.backend_readiness.interest.insert(Ready::READABLE);
809                self.frontend_readiness.interest.remove(Ready::WRITABLE);
810                return SessionResult::Continue;
811            }
812            let queued = self.backend_buffer.available_data();
813            let (current_sz, current_res) = self.frontend.socket_write(self.backend_buffer.data());
814            // A partial write can never report more than was queued: the
815            // socket writes from `data()` and returns `min(written, data.len())`.
816            debug_assert!(
817                current_sz <= queued,
818                "frontend socket_write reported {current_sz} bytes but only {queued} were queued"
819            );
820            res = current_res;
821            let consumed = self.backend_buffer.consume(current_sz);
822            // `consume` drops exactly the written bytes (we already proved
823            // `current_sz <= available_data`, so no clamping occurs).
824            debug_assert_eq!(
825                consumed, current_sz,
826                "consume must drop exactly the bytes written to the frontend"
827            );
828            sz += current_sz;
829            // Cumulative transfer never overruns what was queued at entry.
830            debug_assert!(
831                sz <= queued_total,
832                "cumulative frontend write ({sz}) exceeded the queued backend data ({queued_total})"
833            );
834
835            if current_sz == 0 && res == SocketResult::Continue {
836                self.frontend_status = match self.frontend_status {
837                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
838                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
839                    s => s,
840                };
841            }
842
843            if !self.check_connections() {
844                metrics.bout += sz;
845                count!(names::backend::BYTES_OUT, sz as i64);
846                self.reset_readiness_for_close();
847                self.log_request_success(metrics);
848                return SessionResult::Close;
849            }
850        }
851
852        if sz > 0 {
853            count!(names::backend::BYTES_OUT, sz as i64);
854            self.backend_readiness.interest.insert(Ready::READABLE);
855            metrics.bout += sz;
856        }
857
858        debug!(
859            "{} Wrote {} bytes of {}",
860            log_context!(self),
861            sz,
862            self.backend_buffer.available_data()
863        );
864
865        match res {
866            SocketResult::Error => {
867                self.reset_readiness_for_close();
868                self.log_request_error(metrics, "front socket write error");
869                return SessionResult::Close;
870            }
871            SocketResult::Closed => {
872                self.reset_readiness_for_close();
873                self.log_request_success(metrics);
874                return SessionResult::Close;
875            }
876            SocketResult::WouldBlock => {
877                self.frontend_readiness.event.remove(Ready::WRITABLE);
878            }
879            SocketResult::Continue => {}
880        }
881
882        SessionResult::Continue
883    }
884
885    // Forward content to cluster
886    pub fn backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
887        // Inherited preread bytes (e.g. SNI ClientHello replay) sit in
888        // `frontend_buffer`; splice never drains that userspace buffer, so it
889        // must be empty before the fast path takes over.
890        #[cfg(all(target_os = "linux", feature = "splice"))]
891        if self.protocol == Protocol::TCP
892            && self.splice_pipe.is_some()
893            && self.frontend_buffer.available_data() == 0
894        {
895            return self.splice_backend_writable(metrics);
896        }
897
898        trace!("{} pipe back_writable", log_context!(self));
899
900        if self.frontend_buffer.available_data() == 0 {
901            self.frontend_readiness.interest.insert(Ready::READABLE);
902            self.backend_readiness.interest.remove(Ready::WRITABLE);
903            return SessionResult::Continue;
904        }
905
906        let output_size = self.frontend_buffer.available_data();
907
908        let mut sz = 0usize;
909        let mut socket_res = SocketResult::Continue;
910        // Set when the loop below exits because `frontend_buffer` just ran
911        // dry (as opposed to exiting because of a socket error/close/would-
912        // block). `backend` (below) borrows `self.backend_socket` for the
913        // whole loop, so the drained-buffer close gate has to live outside
914        // this `if let` — it needs `&mut self` for `check_connections` and
915        // friends, which would conflict with `backend` if run inline.
916        let mut drained = false;
917
918        if let Some(ref mut backend) = self.backend_socket {
919            while socket_res == SocketResult::Continue {
920                // no more data in buffer, stop here
921                if self.frontend_buffer.available_data() == 0 {
922                    self.frontend_readiness.interest.insert(Ready::READABLE);
923                    self.backend_readiness.interest.remove(Ready::WRITABLE);
924                    drained = true;
925                    break;
926                }
927
928                let queued = self.frontend_buffer.available_data();
929                let (current_sz, current_res) = backend.socket_write(self.frontend_buffer.data());
930                // A partial write can never report more than was queued.
931                debug_assert!(
932                    current_sz <= queued,
933                    "backend socket_write reported {current_sz} bytes but only {queued} were queued"
934                );
935                socket_res = current_res;
936                let consumed = self.frontend_buffer.consume(current_sz);
937                debug_assert_eq!(
938                    consumed, current_sz,
939                    "consume must drop exactly the bytes written to the backend"
940                );
941                sz += current_sz;
942                // Cumulative transfer never overruns the data queued at entry.
943                debug_assert!(
944                    sz <= output_size,
945                    "cumulative backend write ({sz}) exceeded the queued frontend data ({output_size})"
946                );
947
948                if current_sz == 0 && current_res == SocketResult::Continue {
949                    self.backend_status = match self.backend_status {
950                        ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
951                        ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
952                        s => s,
953                    };
954                }
955            }
956        }
957
958        if drained {
959            count!(names::backend::BACK_BYTES_OUT, sz as i64);
960            metrics.backend_bout += sz;
961            // The queued request bytes just fully drained toward the
962            // backend. This early return used to always report `Continue`,
963            // even when the frontend read side had already hit EOF
964            // (`WriteOpen`/`Closed`, set by `readable`'s/`splice_readable`'s
965            // `SocketResult::Closed` arm — sozu-proxy/sozu#1290): the
966            // READABLE edge that would have driven a follow-up `readable()`
967            // call was already consumed by that arm, so edge-triggered epoll
968            // offers no other event to hang the teardown on and the session
969            // sat resident until an unrelated event or timeout. Run the same
970            // `check_connections` gate every other close goes through so an
971            // already-half-closed session tears down as soon as the final
972            // queued bytes drain. Healthy sessions (frontend still
973            // `Normal`/`ReadOpen`) are unaffected: the gate is skipped
974            // entirely and this keeps returning `Continue`, matching the
975            // pre-fix behavior for normal keepalive flow.
976            if matches!(
977                self.frontend_status,
978                ConnectionStatus::WriteOpen | ConnectionStatus::Closed
979            ) && !self.check_connections()
980            {
981                self.reset_readiness_for_close();
982                self.log_request_success(metrics);
983                return SessionResult::Close;
984            }
985            return SessionResult::Continue;
986        }
987
988        let backend_bout_before = metrics.backend_bout;
989        count!(names::backend::BACK_BYTES_OUT, sz as i64);
990        metrics.backend_bout += sz;
991        // Proxy→backend egress metric advances by exactly the bytes written.
992        debug_assert_eq!(
993            metrics.backend_bout,
994            backend_bout_before + sz,
995            "metrics.backend_bout must advance by exactly the bytes written"
996        );
997
998        if !self.check_connections() {
999            self.reset_readiness_for_close();
1000            self.log_request_success(metrics);
1001            return SessionResult::Close;
1002        }
1003
1004        debug!(
1005            "{} Wrote {} bytes of {}",
1006            log_context!(self),
1007            sz,
1008            output_size
1009        );
1010
1011        match socket_res {
1012            SocketResult::Error => {
1013                self.reset_readiness_for_close();
1014                self.log_request_error(metrics, "back socket write error");
1015                return SessionResult::Close;
1016            }
1017            SocketResult::Closed => {
1018                self.reset_readiness_for_close();
1019                self.log_request_success(metrics);
1020                return SessionResult::Close;
1021            }
1022            SocketResult::WouldBlock => {
1023                self.backend_readiness.event.remove(Ready::WRITABLE);
1024            }
1025            SocketResult::Continue => {}
1026        }
1027        SessionResult::Continue
1028    }
1029
1030    // Read content from cluster
1031    pub fn backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1032        // Mirror of the front-side preread gate: splice never drains the
1033        // userspace `backend_buffer`, so it must be empty before the fast
1034        // path takes over.
1035        #[cfg(all(target_os = "linux", feature = "splice"))]
1036        if self.protocol == Protocol::TCP
1037            && self.splice_pipe.is_some()
1038            && self.backend_buffer.available_data() == 0
1039        {
1040            return self.splice_backend_readable(metrics);
1041        }
1042
1043        self.reset_timeouts();
1044
1045        trace!("{} Pipe backend_readable", log_context!(self));
1046        if self.backend_buffer.available_space() == 0 {
1047            self.backend_readiness.interest.remove(Ready::READABLE);
1048            return SessionResult::Continue;
1049        }
1050
1051        let space_before = self.backend_buffer.available_space();
1052        let data_before = self.backend_buffer.available_data();
1053        let backend_bin_before = metrics.backend_bin;
1054        if let Some(ref mut backend) = self.backend_socket {
1055            let (size, remaining) = backend.socket_read(self.backend_buffer.space());
1056            // `socket_read` reports at most the space slice it was handed.
1057            debug_assert!(
1058                size <= space_before,
1059                "backend socket_read reported more bytes ({size}) than the buffer space offered ({space_before})"
1060            );
1061            self.backend_buffer.fill(size);
1062            // `fill(size)` with `size <= available_space` moves exactly `size`
1063            // bytes from free space into readable data.
1064            debug_assert_eq!(
1065                self.backend_buffer.available_data(),
1066                data_before + size,
1067                "fill must grow readable data by exactly the bytes read"
1068            );
1069
1070            debug!("{} Read {} bytes", log_context!(self), size);
1071
1072            if remaining != SocketResult::Continue || size == 0 {
1073                self.backend_readiness.event.remove(Ready::READABLE);
1074            }
1075            if size > 0 {
1076                self.frontend_readiness.arm_writable();
1077                count!(names::backend::BACK_BYTES_IN, size as i64);
1078                metrics.backend_bin += size;
1079                // Backend→proxy ingress metric advances by exactly bytes read.
1080                debug_assert_eq!(
1081                    metrics.backend_bin,
1082                    backend_bin_before + size,
1083                    "metrics.backend_bin must advance by exactly the bytes read"
1084                );
1085            }
1086
1087            if size == 0 && remaining == SocketResult::Closed {
1088                self.backend_status = match self.backend_status {
1089                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1090                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1091                    s => s,
1092                };
1093
1094                if !self.check_connections() {
1095                    self.reset_readiness_for_close();
1096                    self.log_request_success(metrics);
1097                    return SessionResult::Close;
1098                }
1099            }
1100
1101            match remaining {
1102                SocketResult::Error => {
1103                    self.reset_readiness_for_close();
1104                    self.log_request_error(metrics, "back socket read error");
1105                    return SessionResult::Close;
1106                }
1107                SocketResult::Closed => {
1108                    if !self.check_connections() {
1109                        self.reset_readiness_for_close();
1110                        self.log_request_success(metrics);
1111                        return SessionResult::Close;
1112                    }
1113                }
1114                SocketResult::WouldBlock => {
1115                    self.backend_readiness.event.remove(Ready::READABLE);
1116                }
1117                SocketResult::Continue => {}
1118            }
1119        }
1120
1121        SessionResult::Continue
1122    }
1123
1124    /// Zero-copy fast path of `readable`: pull bytes off the frontend
1125    /// socket into the kernel `in_pipe` via `splice(2)`, then mark the
1126    /// backend writable so the data drains in the next event loop tick.
1127    ///
1128    /// Mirrors `readable`'s `ConnectionStatus` transitions and metric
1129    /// emissions exactly so observability and the `check_connections`
1130    /// state machine behave the same with or without the feature flag.
1131    #[cfg(all(target_os = "linux", feature = "splice"))]
1132    fn splice_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1133        self.reset_timeouts();
1134
1135        trace!("{} pipe splice_readable", log_context!(self));
1136        let capacity = self.splice_capacity();
1137        if self.splice_in_pending() >= capacity {
1138            // Pipe is full — stop reading and let the backend drain it.
1139            self.frontend_readiness.interest.remove(Ready::READABLE);
1140            self.backend_readiness.arm_writable();
1141            return SessionResult::Continue;
1142        }
1143
1144        let pending_before = self.splice_in_pending();
1145        let bin_before = metrics.bin;
1146        let pipe_write_end = self.splice_pipe.as_ref().unwrap().in_pipe[1];
1147        let (sz, res) = splice::splice_in(self.frontend.socket_ref(), pipe_write_end, capacity);
1148        // `splice_in` is asked for at most `capacity` bytes, so the kernel can
1149        // never report moving more than that in one call. We deliberately do
1150        // NOT assert `in_pipe_pending <= capacity`: a kernel pipe buffers well
1151        // beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed
1152        // segments — a GRO super-packet on loopback hands a single ring slot far
1153        // more than a page — so byte-occupancy legitimately exceeds `capacity`.
1154        // `capacity` is the per-call `len` and a soft backpressure threshold,
1155        // not a hard occupancy bound.
1156        debug_assert!(
1157            sz <= capacity,
1158            "splice_in reported {sz} bytes but was capped at len {capacity}"
1159        );
1160        debug!("{} Spliced {} bytes from frontend", log_context!(self), sz);
1161
1162        if sz > 0 {
1163            self.splice_pipe.as_mut().unwrap().in_pipe_pending += sz;
1164            // Pending advanced by exactly the spliced bytes (tracks real
1165            // kernel-pipe occupancy; see the capacity note above).
1166            debug_assert_eq!(
1167                self.splice_in_pending(),
1168                pending_before + sz,
1169                "in_pipe_pending must grow by exactly the spliced bytes"
1170            );
1171            count!(names::backend::BYTES_IN, sz as i64);
1172            metrics.bin += sz;
1173            debug_assert_eq!(
1174                metrics.bin,
1175                bin_before + sz,
1176                "metrics.bin must advance by exactly the spliced bytes"
1177            );
1178            self.backend_readiness.arm_writable();
1179        } else {
1180            self.frontend_readiness.event.remove(Ready::READABLE);
1181
1182            if res == SocketResult::Continue {
1183                self.frontend_status = match self.frontend_status {
1184                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1185                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1186                    s => s,
1187                };
1188            }
1189        }
1190
1191        if !self.check_connections() {
1192            self.reset_readiness_for_close();
1193            self.log_request_success(metrics);
1194            return SessionResult::Close;
1195        }
1196
1197        match res {
1198            SocketResult::Error => {
1199                self.reset_readiness_for_close();
1200                self.log_request_error(metrics, "splice front socket read error");
1201                return SessionResult::Close;
1202            }
1203            SocketResult::Closed => {
1204                // The frontend read side closed (EOF). This is the SPLICE
1205                // sibling of `readable`'s `SocketResult::Closed` drain above
1206                // (sozu-proxy/sozu#1279) and of `frontend_hup`'s
1207                // drain branch: bytes already spliced into the kernel
1208                // `in_pipe` (`splice_in_pending()`) still belong to the
1209                // backend, and returning `Close` here unconditionally
1210                // dropped them. Transition to the half-closed `WriteOpen`
1211                // status, arm the backend writable to drain the kernel pipe
1212                // via `splice_backend_writable`, and defer teardown to
1213                // `check_connections`, whose `request_is_inflight` already
1214                // counts `splice_in_pending() > 0`; once the pipe drains,
1215                // the next `splice_readable` re-observes EOF with nothing
1216                // inflight and closes through the `check_connections` gate
1217                // above this match.
1218                let has_pending = self.splice_in_pending() > 0;
1219                return self.close_frontend_read_side(metrics, has_pending);
1220            }
1221            SocketResult::WouldBlock => {
1222                self.frontend_readiness.event.remove(Ready::READABLE);
1223            }
1224            SocketResult::Continue => {}
1225        }
1226
1227        self.backend_readiness.arm_writable();
1228        SessionResult::Continue
1229    }
1230
1231    /// Zero-copy fast path of `writable`: drain the backend→frontend
1232    /// kernel `out_pipe` toward the frontend socket via `splice(2)`.
1233    /// Mirrors `writable`'s loop, status transitions, and metric
1234    /// emissions.
1235    #[cfg(all(target_os = "linux", feature = "splice"))]
1236    fn splice_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1237        trace!("{} Pipe splice_writable", log_context!(self));
1238        if self.splice_out_pending() == 0 {
1239            self.backend_readiness.interest.insert(Ready::READABLE);
1240            self.frontend_readiness.interest.remove(Ready::WRITABLE);
1241            return SessionResult::Continue;
1242        }
1243
1244        let mut sz = 0usize;
1245        let mut res = SocketResult::Continue;
1246        while res == SocketResult::Continue {
1247            let pending = self.splice_out_pending();
1248            // no more data in pipe, stop here
1249            if pending == 0 {
1250                count!(names::backend::BYTES_OUT, sz as i64);
1251                metrics.bout += sz;
1252                self.backend_readiness.interest.insert(Ready::READABLE);
1253                self.frontend_readiness.interest.remove(Ready::WRITABLE);
1254                return SessionResult::Continue;
1255            }
1256
1257            let pipe_read_end = self.splice_pipe.as_ref().unwrap().out_pipe[0];
1258            let (current_sz, current_res) =
1259                splice::splice_out(pipe_read_end, self.frontend.socket_ref(), pending);
1260            // `splice_out` was asked for `pending` bytes and can drain no more
1261            // than the pipe holds; draining more than `pending` would underflow
1262            // `out_pipe_pending` below.
1263            debug_assert!(
1264                current_sz <= pending,
1265                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1266            );
1267            res = current_res;
1268            if current_sz > 0 {
1269                self.splice_pipe.as_mut().unwrap().out_pipe_pending -= current_sz;
1270                debug_assert_eq!(
1271                    self.splice_out_pending(),
1272                    pending - current_sz,
1273                    "out_pipe_pending must shrink by exactly the drained bytes"
1274                );
1275            }
1276            sz += current_sz;
1277
1278            if current_sz == 0 && res == SocketResult::Continue {
1279                self.frontend_status = match self.frontend_status {
1280                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1281                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1282                    s => s,
1283                };
1284            }
1285
1286            if !self.check_connections() {
1287                metrics.bout += sz;
1288                count!(names::backend::BYTES_OUT, sz as i64);
1289                self.reset_readiness_for_close();
1290                self.log_request_success(metrics);
1291                return SessionResult::Close;
1292            }
1293        }
1294
1295        if sz > 0 {
1296            count!(names::backend::BYTES_OUT, sz as i64);
1297            self.backend_readiness.interest.insert(Ready::READABLE);
1298            metrics.bout += sz;
1299        }
1300
1301        debug!(
1302            "{} Spliced {} bytes (out_pipe_pending={})",
1303            log_context!(self),
1304            sz,
1305            self.splice_out_pending()
1306        );
1307
1308        match res {
1309            SocketResult::Error => {
1310                self.reset_readiness_for_close();
1311                self.log_request_error(metrics, "splice front socket write error");
1312                return SessionResult::Close;
1313            }
1314            SocketResult::Closed => {
1315                self.reset_readiness_for_close();
1316                self.log_request_success(metrics);
1317                return SessionResult::Close;
1318            }
1319            SocketResult::WouldBlock => {
1320                self.frontend_readiness.event.remove(Ready::WRITABLE);
1321            }
1322            SocketResult::Continue => {}
1323        }
1324
1325        SessionResult::Continue
1326    }
1327
1328    /// Zero-copy fast path of `backend_writable`: drain the
1329    /// frontend→backend kernel `in_pipe` toward the backend socket via
1330    /// `splice(2)`. Mirrors `backend_writable`'s loop, status
1331    /// transitions, and metric emissions.
1332    #[cfg(all(target_os = "linux", feature = "splice"))]
1333    fn splice_backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1334        trace!("{} pipe splice_backend_writable", log_context!(self));
1335
1336        if self.splice_in_pending() == 0 {
1337            self.frontend_readiness.interest.insert(Ready::READABLE);
1338            self.backend_readiness.interest.remove(Ready::WRITABLE);
1339            return SessionResult::Continue;
1340        }
1341
1342        let output_size = self.splice_in_pending();
1343        let mut sz = 0usize;
1344        let mut socket_res = SocketResult::Continue;
1345
1346        while socket_res == SocketResult::Continue {
1347            let pending = self.splice_in_pending();
1348            // no more data in pipe, stop here
1349            if pending == 0 {
1350                self.frontend_readiness.interest.insert(Ready::READABLE);
1351                self.backend_readiness.interest.remove(Ready::WRITABLE);
1352                count!(names::backend::BACK_BYTES_OUT, sz as i64);
1353                metrics.backend_bout += sz;
1354                // The queued bytes just fully drained out of the kernel
1355                // `in_pipe`. This early return used to always report
1356                // `Continue`, even when the frontend read side had already
1357                // hit EOF (`WriteOpen`/`Closed`, set by `splice_readable`'s
1358                // `SocketResult::Closed` arm — sozu-proxy/sozu#1290): the
1359                // READABLE edge that would have driven a follow-up
1360                // `splice_readable` call was already consumed by that arm, so
1361                // edge-triggered epoll offers no other event to hang the
1362                // teardown on and the session sat resident until an
1363                // unrelated event or timeout. Run the same
1364                // `check_connections` gate every other close goes through so
1365                // an already-half-closed session tears down as soon as the
1366                // final queued bytes drain. Healthy sessions (frontend still
1367                // `Normal`/`ReadOpen`) are unaffected: the gate is skipped
1368                // entirely and this keeps returning `Continue`, matching the
1369                // pre-fix behavior for normal keepalive flow.
1370                if matches!(
1371                    self.frontend_status,
1372                    ConnectionStatus::WriteOpen | ConnectionStatus::Closed
1373                ) && !self.check_connections()
1374                {
1375                    self.reset_readiness_for_close();
1376                    self.log_request_success(metrics);
1377                    return SessionResult::Close;
1378                }
1379                return SessionResult::Continue;
1380            }
1381
1382            let pipe_read_end = self.splice_pipe.as_ref().unwrap().in_pipe[0];
1383            let (current_sz, current_res) = match self.backend_socket.as_ref() {
1384                Some(b) => splice::splice_out(pipe_read_end, b, pending),
1385                None => break,
1386            };
1387            // Draining more than `pending` would underflow `in_pipe_pending`.
1388            debug_assert!(
1389                current_sz <= pending,
1390                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1391            );
1392            socket_res = current_res;
1393            if current_sz > 0 {
1394                self.splice_pipe.as_mut().unwrap().in_pipe_pending -= current_sz;
1395                debug_assert_eq!(
1396                    self.splice_in_pending(),
1397                    pending - current_sz,
1398                    "in_pipe_pending must shrink by exactly the drained bytes"
1399                );
1400            }
1401            sz += current_sz;
1402            // Cumulative drain never exceeds what was pending at entry.
1403            debug_assert!(
1404                sz <= output_size,
1405                "cumulative splice drain ({sz}) exceeded the bytes pending at entry ({output_size})"
1406            );
1407
1408            if current_sz == 0 && current_res == SocketResult::Continue {
1409                self.backend_status = match self.backend_status {
1410                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1411                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1412                    s => s,
1413                };
1414            }
1415        }
1416
1417        count!(names::backend::BACK_BYTES_OUT, sz as i64);
1418        metrics.backend_bout += sz;
1419
1420        if !self.check_connections() {
1421            self.reset_readiness_for_close();
1422            self.log_request_success(metrics);
1423            return SessionResult::Close;
1424        }
1425
1426        debug!(
1427            "{} Spliced {} bytes of {}",
1428            log_context!(self),
1429            sz,
1430            output_size
1431        );
1432
1433        match socket_res {
1434            SocketResult::Error => {
1435                self.reset_readiness_for_close();
1436                self.log_request_error(metrics, "splice back socket write error");
1437                return SessionResult::Close;
1438            }
1439            SocketResult::Closed => {
1440                self.reset_readiness_for_close();
1441                self.log_request_success(metrics);
1442                return SessionResult::Close;
1443            }
1444            SocketResult::WouldBlock => {
1445                self.backend_readiness.event.remove(Ready::WRITABLE);
1446            }
1447            SocketResult::Continue => {}
1448        }
1449        SessionResult::Continue
1450    }
1451
1452    /// Zero-copy fast path of `backend_readable`: pull bytes off the
1453    /// backend socket into the kernel `out_pipe` via `splice(2)`, then
1454    /// mark the frontend writable so the data drains in the next event
1455    /// loop tick. Mirrors `backend_readable`'s status transitions and
1456    /// metric emissions.
1457    #[cfg(all(target_os = "linux", feature = "splice"))]
1458    fn splice_backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1459        self.reset_timeouts();
1460
1461        trace!("{} Pipe splice_backend_readable", log_context!(self));
1462        let capacity = self.splice_capacity();
1463        if self.splice_out_pending() >= capacity {
1464            // Pipe is full — stop reading and let the frontend drain it.
1465            self.backend_readiness.interest.remove(Ready::READABLE);
1466            self.frontend_readiness.arm_writable();
1467            return SessionResult::Continue;
1468        }
1469
1470        let pending_before = self.splice_out_pending();
1471        let backend_bin_before = metrics.backend_bin;
1472        let pipe_write_end = self.splice_pipe.as_ref().unwrap().out_pipe[1];
1473        let (size, remaining) = match self.backend_socket.as_ref() {
1474            Some(b) => splice::splice_in(b, pipe_write_end, capacity),
1475            None => return SessionResult::Continue,
1476        };
1477        // `splice_in` is capped at `len = capacity`, so the kernel never reports
1478        // moving more than that per call. As in `splice_readable`, we do NOT
1479        // assert `out_pipe_pending <= capacity`: a kernel pipe holds well beyond
1480        // its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed (GRO)
1481        // segments, so byte-occupancy legitimately exceeds `capacity` — it is
1482        // only the per-call `len` and a soft backpressure threshold.
1483        debug_assert!(
1484            size <= capacity,
1485            "splice_in reported {size} bytes but was capped at len {capacity}"
1486        );
1487
1488        debug!("{} Spliced {} bytes from backend", log_context!(self), size);
1489
1490        if remaining != SocketResult::Continue || size == 0 {
1491            self.backend_readiness.event.remove(Ready::READABLE);
1492        }
1493        if size > 0 {
1494            self.splice_pipe.as_mut().unwrap().out_pipe_pending += size;
1495            debug_assert_eq!(
1496                self.splice_out_pending(),
1497                pending_before + size,
1498                "out_pipe_pending must grow by exactly the spliced bytes"
1499            );
1500            self.frontend_readiness.arm_writable();
1501            count!(names::backend::BACK_BYTES_IN, size as i64);
1502            metrics.backend_bin += size;
1503            debug_assert_eq!(
1504                metrics.backend_bin,
1505                backend_bin_before + size,
1506                "metrics.backend_bin must advance by exactly the spliced bytes"
1507            );
1508        }
1509
1510        if size == 0 && remaining == SocketResult::Closed {
1511            self.backend_status = match self.backend_status {
1512                ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1513                ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1514                s => s,
1515            };
1516
1517            if !self.check_connections() {
1518                self.reset_readiness_for_close();
1519                self.log_request_success(metrics);
1520                return SessionResult::Close;
1521            }
1522        }
1523
1524        match remaining {
1525            SocketResult::Error => {
1526                self.reset_readiness_for_close();
1527                self.log_request_error(metrics, "splice back socket read error");
1528                return SessionResult::Close;
1529            }
1530            SocketResult::Closed => {
1531                if !self.check_connections() {
1532                    self.reset_readiness_for_close();
1533                    self.log_request_success(metrics);
1534                    return SessionResult::Close;
1535                }
1536            }
1537            SocketResult::WouldBlock => {
1538                self.backend_readiness.event.remove(Ready::READABLE);
1539            }
1540            SocketResult::Continue => {}
1541        }
1542
1543        SessionResult::Continue
1544    }
1545
1546    pub fn log_context(&self) -> LogContext<'_> {
1547        LogContext {
1548            session_id: self.session_id,
1549            request_id: Some(self.request_id),
1550            cluster_id: self.cluster_id.as_deref(),
1551            backend_id: self.backend_id.as_deref(),
1552        }
1553    }
1554
1555    fn log_endpoint(&self) -> EndpointRecord<'_> {
1556        match &self.websocket_context {
1557            WebSocketContext::Http {
1558                method,
1559                authority,
1560                path,
1561                status,
1562                reason,
1563            } => EndpointRecord::Http {
1564                method: method.as_deref(),
1565                authority: authority.as_deref(),
1566                path: path.as_deref(),
1567                status: status.to_owned(),
1568                reason: reason.as_deref(),
1569            },
1570            WebSocketContext::Tcp => EndpointRecord::Tcp,
1571        }
1572    }
1573}
1574
1575impl<Front: SocketHandler, L: ListenerHandler> SessionState for Pipe<Front, L> {
1576    fn ready(
1577        &mut self,
1578        _session: Rc<RefCell<dyn crate::ProxySession>>,
1579        _proxy: Rc<RefCell<dyn crate::L7Proxy>>,
1580        metrics: &mut SessionMetrics,
1581    ) -> SessionResult {
1582        let mut counter = 0;
1583
1584        if self.frontend_readiness.event.is_hup() {
1585            return SessionResult::Close;
1586        }
1587
1588        while counter < MAX_LOOP_ITERATIONS {
1589            let frontend_interest = self.frontend_readiness.filter_interest();
1590            let backend_interest = self.backend_readiness.filter_interest();
1591
1592            trace!(
1593                "{} Frontend interest({:?}), backend interest({:?})",
1594                log_context!(self),
1595                frontend_interest,
1596                backend_interest
1597            );
1598            if frontend_interest.is_empty() && backend_interest.is_empty() {
1599                break;
1600            }
1601
1602            if self.backend_readiness.event.is_hup()
1603                && self.frontend_readiness.interest.is_writable()
1604                && !self.frontend_readiness.event.is_writable()
1605            {
1606                break;
1607            }
1608
1609            if frontend_interest.is_readable() && self.readable(metrics) == SessionResult::Close {
1610                return SessionResult::Close;
1611            }
1612
1613            if backend_interest.is_writable()
1614                && self.backend_writable(metrics) == SessionResult::Close
1615            {
1616                return SessionResult::Close;
1617            }
1618
1619            if backend_interest.is_readable()
1620                && self.backend_readable(metrics) == SessionResult::Close
1621            {
1622                return SessionResult::Close;
1623            }
1624
1625            if frontend_interest.is_writable() && self.writable(metrics) == SessionResult::Close {
1626                return SessionResult::Close;
1627            }
1628
1629            if backend_interest.is_hup() && self.backend_hup(metrics) == SessionResult::Close {
1630                return SessionResult::Close;
1631            }
1632
1633            if frontend_interest.is_error() {
1634                error!(
1635                    "{} Frontend socket error, disconnecting",
1636                    log_context!(self)
1637                );
1638
1639                self.frontend_readiness.interest = Ready::EMPTY;
1640                self.backend_readiness.interest = Ready::EMPTY;
1641
1642                return SessionResult::Close;
1643            }
1644
1645            if backend_interest.is_error() && self.backend_hup(metrics) == SessionResult::Close {
1646                self.frontend_readiness.interest = Ready::EMPTY;
1647                self.backend_readiness.interest = Ready::EMPTY;
1648
1649                error!("{} Backend socket error, disconnecting", log_context!(self));
1650                return SessionResult::Close;
1651            }
1652
1653            counter += 1;
1654        }
1655
1656        if counter >= MAX_LOOP_ITERATIONS {
1657            error!(
1658                "{}\tHandling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
1659                log_context!(self),
1660                MAX_LOOP_ITERATIONS
1661            );
1662
1663            incr!(names::http::INFINITE_LOOP_ERROR);
1664            self.print_state(self.protocol_string());
1665
1666            return SessionResult::Close;
1667        }
1668
1669        SessionResult::Continue
1670    }
1671
1672    fn update_readiness(&mut self, token: Token, events: Ready) {
1673        if self.frontend_token == token {
1674            self.frontend_readiness.event |= events;
1675        } else if self.backend_token == Some(token) {
1676            self.backend_readiness.event |= events;
1677        }
1678    }
1679
1680    fn timeout(&mut self, token: Token, metrics: &mut SessionMetrics) -> StateResult {
1681        //info!("got timeout for token: {:?}", token);
1682        if self.frontend_token == token {
1683            self.log_request_timeout(metrics, "frontend socket timeout");
1684            if let Some(timeout) = self.container_frontend_timeout.as_mut() {
1685                timeout.triggered()
1686            }
1687            return StateResult::CloseSession;
1688        }
1689
1690        if self.backend_token == Some(token) {
1691            //info!("backend timeout triggered for token {:?}", token);
1692            if let Some(timeout) = self.container_backend_timeout.as_mut() {
1693                timeout.triggered()
1694            }
1695
1696            self.log_request_timeout(metrics, "backend socket timeout");
1697            return StateResult::CloseSession;
1698        }
1699
1700        error!("{} Got timeout for an invalid token", log_context!(self));
1701        self.log_request_error(metrics, "invalid token timeout");
1702        StateResult::CloseSession
1703    }
1704
1705    fn cancel_timeouts(&mut self) {
1706        self.container_frontend_timeout.as_mut().map(|t| t.cancel());
1707        self.container_backend_timeout.as_mut().map(|t| t.cancel());
1708    }
1709
1710    fn close(&mut self, _proxy: Rc<RefCell<dyn L7Proxy>>, _metrics: &mut SessionMetrics) {
1711        if let Some(backend) = self.backend.as_mut() {
1712            let mut backend = backend.borrow_mut();
1713            backend.active_requests = backend.active_requests.saturating_sub(1);
1714        }
1715    }
1716
1717    fn print_state(&self, context: &str) {
1718        error!(
1719            "\
1720{} {} Session(Pipe)
1721\tFrontend:
1722\t\ttoken: {:?}\treadiness: {:?}
1723\tBackend:
1724\t\ttoken: {:?}\treadiness: {:?}",
1725            log_context!(self),
1726            context,
1727            self.frontend_token,
1728            self.frontend_readiness,
1729            self.backend_token,
1730            self.backend_readiness
1731        );
1732    }
1733}
1734
1735#[cfg(test)]
1736mod tests {
1737    use std::{
1738        collections::BTreeMap,
1739        io::Write,
1740        net::{TcpListener as StdTcpListener, TcpStream as StdTcpStream},
1741        time::Duration,
1742    };
1743
1744    use super::*;
1745    use crate::pool::Pool;
1746
1747    struct TestListener {
1748        address: SocketAddr,
1749    }
1750
1751    impl ListenerHandler for TestListener {
1752        fn get_addr(&self) -> &SocketAddr {
1753            &self.address
1754        }
1755
1756        fn get_tags(&self, _key: &str) -> Option<&sozu_command::logging::CachedTags> {
1757            None
1758        }
1759
1760        fn set_tags(&mut self, _key: String, _tags: Option<BTreeMap<String, String>>) {}
1761
1762        fn protocol(&self) -> Protocol {
1763            Protocol::HTTP
1764        }
1765
1766        fn public_address(&self) -> SocketAddr {
1767            self.address
1768        }
1769    }
1770
1771    fn connected_pair() -> (StdTcpStream, StdTcpStream) {
1772        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
1773        let address = listener.local_addr().expect("listener local addr");
1774        let client = StdTcpStream::connect(address).expect("connect test client");
1775        let (server, _) = listener.accept().expect("accept test server");
1776        client.set_nonblocking(true).expect("client nonblocking");
1777        server.set_nonblocking(true).expect("server nonblocking");
1778        (client, server)
1779    }
1780
1781    #[test]
1782    fn backend_readable_arms_frontend_writable_event_when_buffering_response() {
1783        let (frontend_peer, frontend_socket) = connected_pair();
1784        let (mut backend_peer, backend_socket) = connected_pair();
1785
1786        let mut pool = Pool::with_capacity(2, 2, 4096);
1787        let backend_buffer = pool.checkout().expect("backend buffer");
1788        let frontend_buffer = pool.checkout().expect("frontend buffer");
1789        let address = "127.0.0.1:0".parse().expect("test address");
1790        let listener = Rc::new(RefCell::new(TestListener { address }));
1791
1792        let mut pipe = Pipe::new(
1793            backend_buffer,
1794            None,
1795            Some(TcpStream::from_std(backend_socket)),
1796            None,
1797            None,
1798            None,
1799            None,
1800            frontend_buffer,
1801            Token(0),
1802            TcpStream::from_std(frontend_socket),
1803            listener,
1804            Protocol::HTTP,
1805            Ulid::generate(),
1806            Ulid::generate(),
1807            None,
1808            WebSocketContext::Tcp,
1809        );
1810        pipe.set_back_token(Token(1));
1811        pipe.frontend_readiness.event = Ready::EMPTY;
1812        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1813        pipe.backend_readiness.event = Ready::READABLE;
1814        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1815
1816        backend_peer
1817            .write_all(b"server-speaks-first")
1818            .expect("write backend payload");
1819
1820        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1821        assert_eq!(pipe.backend_readable(&mut metrics), SessionResult::Continue);
1822
1823        assert!(
1824            pipe.backend_buffer.available_data() > 0,
1825            "backend_readable must buffer backend bytes"
1826        );
1827        assert!(
1828            pipe.frontend_readiness.interest.is_writable(),
1829            "buffered backend bytes must arm frontend WRITABLE interest"
1830        );
1831        assert!(
1832            pipe.frontend_readiness.event.is_writable(),
1833            "buffered backend bytes must queue a frontend WRITABLE event"
1834        );
1835
1836        drop(frontend_peer);
1837    }
1838
1839    #[test]
1840    fn frontend_readable_arms_backend_writable_event_when_buffering_request() {
1841        let (mut frontend_peer, frontend_socket) = connected_pair();
1842        let (backend_peer, backend_socket) = connected_pair();
1843
1844        let mut pool = Pool::with_capacity(2, 2, 4096);
1845        let backend_buffer = pool.checkout().expect("backend buffer");
1846        let frontend_buffer = pool.checkout().expect("frontend buffer");
1847        let address = "127.0.0.1:0".parse().expect("test address");
1848        let listener = Rc::new(RefCell::new(TestListener { address }));
1849
1850        let mut pipe = Pipe::new(
1851            backend_buffer,
1852            None,
1853            Some(TcpStream::from_std(backend_socket)),
1854            None,
1855            None,
1856            None,
1857            None,
1858            frontend_buffer,
1859            Token(0),
1860            TcpStream::from_std(frontend_socket),
1861            listener,
1862            Protocol::HTTP,
1863            Ulid::generate(),
1864            Ulid::generate(),
1865            None,
1866            WebSocketContext::Tcp,
1867        );
1868        pipe.set_back_token(Token(1));
1869        pipe.frontend_readiness.event = Ready::READABLE;
1870        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1871        pipe.backend_readiness.event = Ready::EMPTY;
1872        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1873
1874        frontend_peer
1875            .write_all(b"client-speaks-after-upgrade")
1876            .expect("write frontend payload");
1877
1878        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1879        assert_eq!(pipe.readable(&mut metrics), SessionResult::Continue);
1880
1881        assert!(
1882            pipe.frontend_buffer.available_data() > 0,
1883            "readable must buffer frontend bytes"
1884        );
1885        assert!(
1886            pipe.backend_readiness.interest.is_writable(),
1887            "buffered frontend bytes must arm backend WRITABLE interest"
1888        );
1889        assert!(
1890            pipe.backend_readiness.event.is_writable(),
1891            "buffered frontend bytes must queue a backend WRITABLE event"
1892        );
1893
1894        drop(backend_peer);
1895    }
1896
1897    #[test]
1898    fn restore_readiness_events_rearms_inherited_buffered_writes() {
1899        let (frontend_peer, frontend_socket) = connected_pair();
1900        let (backend_peer, backend_socket) = connected_pair();
1901
1902        let mut pool = Pool::with_capacity(2, 2, 4096);
1903        let mut backend_buffer = pool.checkout().expect("backend buffer");
1904        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
1905        backend_buffer
1906            .write_all(b"backend bytes inherited from 101 read")
1907            .expect("write backend buffer");
1908        frontend_buffer
1909            .write_all(b"frontend bytes inherited from upgrade read")
1910            .expect("write frontend buffer");
1911        let address = "127.0.0.1:0".parse().expect("test address");
1912        let listener = Rc::new(RefCell::new(TestListener { address }));
1913
1914        let mut pipe = Pipe::new(
1915            backend_buffer,
1916            None,
1917            Some(TcpStream::from_std(backend_socket)),
1918            None,
1919            None,
1920            None,
1921            None,
1922            frontend_buffer,
1923            Token(0),
1924            TcpStream::from_std(frontend_socket),
1925            listener,
1926            Protocol::HTTP,
1927            Ulid::generate(),
1928            Ulid::generate(),
1929            None,
1930            WebSocketContext::Tcp,
1931        );
1932
1933        pipe.restore_readiness_events(Ready::EMPTY, Ready::EMPTY);
1934
1935        assert!(
1936            pipe.frontend_readiness.event.is_writable(),
1937            "restoring inherited frontend events must not park backend-buffered bytes"
1938        );
1939        assert!(
1940            pipe.backend_readiness.event.is_writable(),
1941            "restoring inherited backend events must not park frontend-buffered bytes"
1942        );
1943
1944        drop(frontend_peer);
1945        drop(backend_peer);
1946    }
1947
1948    /// Regression guard for the splice/preread interaction: when `Pipe` is
1949    /// constructed with the splice fast path available (`Protocol::TCP`)
1950    /// AND a non-empty inherited `frontend_buffer` (the SNI-preread
1951    /// ClientHello replay scenario), `backend_writable` must drain those
1952    /// buffered bytes to the backend socket through the normal buffered
1953    /// path first. Without the gate in `backend_writable`, this call would
1954    /// dispatch straight to `splice_backend_writable`, which only drains the
1955    /// kernel pipe (`splice_in_pending`) — 0 here — and returns immediately
1956    /// without ever touching `frontend_buffer`, silently dropping the
1957    /// preread bytes.
1958    #[cfg(all(target_os = "linux", feature = "splice"))]
1959    #[test]
1960    fn backend_writable_drains_inherited_frontend_buffer_before_splice_engages() {
1961        use std::io::Read;
1962
1963        let (frontend_peer, frontend_socket) = connected_pair();
1964        let (mut backend_peer, backend_socket) = connected_pair();
1965
1966        let mut pool = Pool::with_capacity(2, 2, 4096);
1967        let backend_buffer = pool.checkout().expect("backend buffer");
1968        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
1969        frontend_buffer
1970            .write_all(b"inherited-preread-client-hello")
1971            .expect("write inherited frontend buffer");
1972        let address = "127.0.0.1:0".parse().expect("test address");
1973        let listener = Rc::new(RefCell::new(TestListener { address }));
1974
1975        let mut pipe = Pipe::new(
1976            backend_buffer,
1977            None,
1978            Some(TcpStream::from_std(backend_socket)),
1979            None,
1980            None,
1981            None,
1982            None,
1983            frontend_buffer,
1984            Token(0),
1985            TcpStream::from_std(frontend_socket),
1986            listener,
1987            Protocol::TCP,
1988            Ulid::generate(),
1989            Ulid::generate(),
1990            None,
1991            WebSocketContext::Tcp,
1992        );
1993        pipe.set_back_token(Token(1));
1994
1995        assert!(
1996            pipe.splice_pipe.is_some(),
1997            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
1998        );
1999        assert!(
2000            pipe.frontend_buffer.available_data() > 0,
2001            "test setup must inherit a non-empty frontend buffer"
2002        );
2003
2004        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2005        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Continue);
2006
2007        assert_eq!(
2008            pipe.frontend_buffer.available_data(),
2009            0,
2010            "inherited preread bytes must drain through the buffered path before splice engages"
2011        );
2012
2013        let mut received = [0u8; 64];
2014        let n = backend_peer
2015            .read(&mut received)
2016            .expect("backend socket must have received the drained preread bytes");
2017        assert_eq!(&received[..n], b"inherited-preread-client-hello");
2018
2019        drop(frontend_peer);
2020    }
2021
2022    /// Regression guard for the close-before-flush data loss
2023    /// (sozu-proxy/sozu#1279): when the frontend read side reaches
2024    /// EOF while `frontend_buffer` still holds bytes queued for the backend,
2025    /// `readable` must NOT return `Close` (which discarded them, silently
2026    /// truncating the stream -- the reproducer was a payload coalesced with an
2027    /// SNI ClientHello). It must keep the session alive to drain, then the
2028    /// queued bytes must reach the backend and only then may the session
2029    /// close.
2030    #[test]
2031    fn frontend_eof_flushes_queued_backend_bytes_before_closing() {
2032        use std::io::{Read, Write};
2033
2034        let (frontend_peer, frontend_socket) = connected_pair();
2035        let (mut backend_peer, backend_socket) = connected_pair();
2036
2037        let mut pool = Pool::with_capacity(2, 2, 4096);
2038        let backend_buffer = pool.checkout().expect("backend buffer");
2039        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2040        // Bytes the frontend already delivered, still queued for the backend.
2041        frontend_buffer
2042            .write_all(b"front-to-back-tail-still-queued")
2043            .expect("seed queued frontend bytes");
2044        let address = "127.0.0.1:0".parse().expect("test address");
2045        let listener = Rc::new(RefCell::new(TestListener { address }));
2046
2047        let mut pipe = Pipe::new(
2048            backend_buffer,
2049            None,
2050            Some(TcpStream::from_std(backend_socket)),
2051            None,
2052            None,
2053            None,
2054            None,
2055            frontend_buffer,
2056            Token(0),
2057            TcpStream::from_std(frontend_socket),
2058            listener,
2059            Protocol::TCP,
2060            Ulid::generate(),
2061            Ulid::generate(),
2062            None,
2063            WebSocketContext::Tcp,
2064        );
2065        pipe.set_back_token(Token(1));
2066        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2067        pipe.frontend_readiness.event = Ready::READABLE;
2068
2069        // The frontend closes right after delivering its bytes (no gap): the
2070        // pipe reads EOF while `frontend_buffer` is still non-empty.
2071        drop(frontend_peer);
2072
2073        // Nonblocking EOF is not observable on the very first read on every
2074        // platform, so retry `readable` (bounded, no sleep) until it observes
2075        // the close. `readable` must never report `Close` while bytes remain
2076        // queued.
2077        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2078        let mut saw_eof = false;
2079        for _ in 0..100_000 {
2080            let result = pipe.readable(&mut metrics);
2081            assert_ne!(
2082                result,
2083                SessionResult::Close,
2084                "readable must not close while {} queued backend bytes remain",
2085                pipe.frontend_buffer.available_data()
2086            );
2087            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2088                saw_eof = true;
2089                break;
2090            }
2091        }
2092        assert!(
2093            saw_eof,
2094            "frontend EOF was never observed within the retry budget"
2095        );
2096        assert!(
2097            pipe.frontend_buffer.available_data() > 0,
2098            "the queued backend bytes must survive the frontend EOF, not be dropped"
2099        );
2100
2101        // Draining now delivers every queued byte to the backend. Nothing
2102        // else is in flight (backend still `Normal`) and the frontend read
2103        // side already hit EOF (`WriteOpen`), so `backend_writable` closes
2104        // the session itself as soon as the drain completes -- it does not
2105        // wait for a follow-up event that edge-triggered epoll would never
2106        // deliver (sozu-proxy/sozu#1290).
2107        assert_eq!(pipe.backend_writable(&mut metrics), SessionResult::Close);
2108        assert_eq!(
2109            pipe.frontend_buffer.available_data(),
2110            0,
2111            "the queued bytes must all drain to the backend after EOF"
2112        );
2113
2114        let mut received = Vec::new();
2115        // Read until we have the full payload (a single read may segment).
2116        for _ in 0..100_000 {
2117            let mut buf = [0u8; 64];
2118            match backend_peer.read(&mut buf) {
2119                Ok(0) => break,
2120                Ok(n) => {
2121                    received.extend_from_slice(&buf[..n]);
2122                    if received.len() >= b"front-to-back-tail-still-queued".len() {
2123                        break;
2124                    }
2125                }
2126                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2127                Err(_) => break,
2128            }
2129        }
2130        assert_eq!(
2131            received, b"front-to-back-tail-still-queued",
2132            "the backend must receive the queued tail byte-for-byte, not a truncation"
2133        );
2134    }
2135
2136    /// Regression guard for the missing completion edge on the deferred
2137    /// close (sozu-proxy/sozu#1290): the test above proves the queued bytes
2138    /// survive the frontend's EOF; this proves the other half of the fix --
2139    /// that draining those bytes closes the session immediately, from that
2140    /// single `backend_writable` event, with no extra event injected by the
2141    /// test. Before the fix, `backend_writable`'s drained-buffer early
2142    /// return always reported `Continue`, even with the frontend already at
2143    /// EOF and nothing else in flight: edge-triggered epoll had already
2144    /// consumed the READABLE/HUP edge that signalled the EOF, so no other
2145    /// event existed to hang a follow-up close decision on and the session
2146    /// sat resident until an unrelated event or timeout.
2147    #[test]
2148    fn frontend_eof_with_queued_bytes_closes_on_the_draining_backend_writable_call() {
2149        use std::io::Read;
2150
2151        let (mut frontend_peer, frontend_socket) = connected_pair();
2152        let (mut backend_peer, backend_socket) = connected_pair();
2153
2154        let mut pool = Pool::with_capacity(2, 2, 4096);
2155        let backend_buffer = pool.checkout().expect("backend buffer");
2156        let frontend_buffer = pool.checkout().expect("frontend buffer");
2157        let address = "127.0.0.1:0".parse().expect("test address");
2158        let listener = Rc::new(RefCell::new(TestListener { address }));
2159
2160        let mut pipe = Pipe::new(
2161            backend_buffer,
2162            None,
2163            Some(TcpStream::from_std(backend_socket)),
2164            None,
2165            None,
2166            None,
2167            None,
2168            frontend_buffer,
2169            Token(0),
2170            TcpStream::from_std(frontend_socket),
2171            listener,
2172            Protocol::HTTP,
2173            Ulid::generate(),
2174            Ulid::generate(),
2175            None,
2176            WebSocketContext::Tcp,
2177        );
2178        pipe.set_back_token(Token(1));
2179        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2180        pipe.frontend_readiness.event = Ready::READABLE;
2181
2182        // The frontend delivers its bytes then closes right away -- no gap
2183        // -- so the real `readable()` call below observes EOF while bytes
2184        // are still queued in `frontend_buffer`.
2185        frontend_peer
2186            .write_all(b"queued-request-tail")
2187            .expect("write frontend payload");
2188        drop(frontend_peer);
2189
2190        // Drive the REAL ready pass: keep calling `readable` (bounded, no
2191        // sleep) until it observes the close, exactly like the event loop
2192        // would after mio reports the frontend's HUP/EOF.
2193        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2194        let mut saw_eof = false;
2195        for _ in 0..100_000 {
2196            let result = pipe.readable(&mut metrics);
2197            assert_ne!(
2198                result,
2199                SessionResult::Close,
2200                "readable must not close while queued backend bytes remain"
2201            );
2202            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2203                saw_eof = true;
2204                break;
2205            }
2206        }
2207        assert!(
2208            saw_eof,
2209            "frontend EOF was never observed within the retry budget"
2210        );
2211        assert!(
2212            pipe.frontend_buffer.available_data() > 0,
2213            "the queued bytes must survive the frontend EOF"
2214        );
2215        assert!(
2216            pipe.backend_readiness.interest.is_writable()
2217                && pipe.backend_readiness.event.is_writable(),
2218            "observing EOF with queued bytes must arm backend WRITABLE to drain them"
2219        );
2220
2221        // The backend becomes writable exactly once -- the real event the
2222        // arming above promises -- and drains everything in that single
2223        // call. No extra readable()/frontend_hup()/check_connections() call
2224        // is injected here: the session must close from THIS event alone.
2225        let result = pipe.backend_writable(&mut metrics);
2226
2227        assert_eq!(
2228            result,
2229            SessionResult::Close,
2230            "the single draining backend_writable call must close the session by itself -- \
2231             edge-triggered epoll already consumed the EOF edge and offers no other event to \
2232             hang the teardown on"
2233        );
2234        assert_eq!(
2235            pipe.frontend_buffer.available_data(),
2236            0,
2237            "the queued bytes must have fully drained before the session closed"
2238        );
2239
2240        let mut received = Vec::new();
2241        for _ in 0..100_000 {
2242            let mut buf = [0u8; 64];
2243            match backend_peer.read(&mut buf) {
2244                Ok(0) => break,
2245                Ok(n) => {
2246                    received.extend_from_slice(&buf[..n]);
2247                    if received.len() >= b"queued-request-tail".len() {
2248                        break;
2249                    }
2250                }
2251                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
2252                Err(_) => break,
2253            }
2254        }
2255        assert_eq!(
2256            received, b"queued-request-tail",
2257            "the backend must still receive the queued bytes byte-for-byte before the close"
2258        );
2259    }
2260
2261    /// Regression guard for the HUP-path sibling of the close-before-flush
2262    /// data loss (sozu-proxy/sozu#1290): `command/src/ready.rs`'s
2263    /// `From<&mio::event::Event>` sets `Ready::HUP` whenever
2264    /// `is_read_closed()`/`is_write_closed()` fires, independently of
2265    /// `Ready::READABLE` -- so a client FIN that coalesces with the payload
2266    /// tail on a loaded event loop delivers a SINGLE epoll batch carrying
2267    /// BOTH bits. `frontend_hup` must not drop bytes still queued in
2268    /// `frontend_buffer` (already read) or still pending in the kernel
2269    /// receive buffer (signalled by the retained READABLE event) just
2270    /// because HUP also fired.
2271    #[test]
2272    fn frontend_hup_drains_inflight_request_bytes_before_closing() {
2273        let (frontend_peer, frontend_socket) = connected_pair();
2274        let (_backend_peer, backend_socket) = connected_pair();
2275
2276        let mut pool = Pool::with_capacity(2, 2, 4096);
2277        let backend_buffer = pool.checkout().expect("backend buffer");
2278        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
2279        frontend_buffer
2280            .write_all(b"front-to-back-tail-still-queued")
2281            .expect("seed queued frontend bytes");
2282        let address = "127.0.0.1:0".parse().expect("test address");
2283        let listener = Rc::new(RefCell::new(TestListener { address }));
2284
2285        let mut pipe = Pipe::new(
2286            backend_buffer,
2287            None,
2288            Some(TcpStream::from_std(backend_socket)),
2289            None,
2290            None,
2291            None,
2292            None,
2293            frontend_buffer,
2294            Token(0),
2295            TcpStream::from_std(frontend_socket),
2296            listener,
2297            Protocol::TCP,
2298            Ulid::generate(),
2299            Ulid::generate(),
2300            None,
2301            WebSocketContext::Tcp,
2302        );
2303        pipe.set_back_token(Token(1));
2304        // The kernel receive buffer still has a tail behind the FIN: both
2305        // bits set in the same batch, exactly as `ready.rs` would produce.
2306        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2307        pipe.frontend_readiness.event = Ready::READABLE | Ready::HUP;
2308        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2309        pipe.backend_readiness.event = Ready::EMPTY;
2310
2311        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2312        let result = pipe.frontend_hup(&mut metrics);
2313
2314        assert_eq!(
2315            result,
2316            SessionResult::Continue,
2317            "frontend_hup must keep the session alive while request bytes are still in flight"
2318        );
2319        assert!(
2320            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2321            "frontend_hup must still mark the frontend Closed even on the drain branch"
2322        );
2323        assert!(
2324            pipe.backend_readiness.interest.is_writable()
2325                && pipe.backend_readiness.event.is_writable(),
2326            "the drain branch must arm backend WRITABLE to flush the queued frontend bytes"
2327        );
2328        assert!(
2329            pipe.frontend_readiness.interest.is_readable(),
2330            "the drain branch must retain frontend READABLE interest to read the kernel tail to EOF"
2331        );
2332
2333        drop(frontend_peer);
2334    }
2335
2336    /// Sibling of the above: when nothing is in flight (no buffered bytes,
2337    /// no pending READABLE event), `frontend_hup` keeps its pre-existing
2338    /// behavior of closing immediately -- there is nothing left to drain.
2339    #[test]
2340    fn frontend_hup_closes_immediately_when_nothing_is_inflight() {
2341        let (frontend_peer, frontend_socket) = connected_pair();
2342        let (_backend_peer, backend_socket) = connected_pair();
2343
2344        let mut pool = Pool::with_capacity(2, 2, 4096);
2345        let backend_buffer = pool.checkout().expect("backend buffer");
2346        let frontend_buffer = pool.checkout().expect("frontend buffer");
2347        let address = "127.0.0.1:0".parse().expect("test address");
2348        let listener = Rc::new(RefCell::new(TestListener { address }));
2349
2350        let mut pipe = Pipe::new(
2351            backend_buffer,
2352            None,
2353            Some(TcpStream::from_std(backend_socket)),
2354            None,
2355            None,
2356            None,
2357            None,
2358            frontend_buffer,
2359            Token(0),
2360            TcpStream::from_std(frontend_socket),
2361            listener,
2362            Protocol::TCP,
2363            Ulid::generate(),
2364            Ulid::generate(),
2365            None,
2366            WebSocketContext::Tcp,
2367        );
2368        pipe.set_back_token(Token(1));
2369        // HUP only, no readable event, no queued bytes: nothing to drain.
2370        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2371        pipe.frontend_readiness.event = Ready::HUP;
2372        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2373        pipe.backend_readiness.event = Ready::EMPTY;
2374
2375        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2376        let result = pipe.frontend_hup(&mut metrics);
2377
2378        assert_eq!(
2379            result,
2380            SessionResult::Close,
2381            "frontend_hup must close immediately when nothing is in flight (unchanged legacy behavior)"
2382        );
2383        assert!(
2384            matches!(pipe.frontend_status, ConnectionStatus::Closed),
2385            "frontend_hup must mark the frontend Closed"
2386        );
2387        assert!(
2388            !pipe.backend_readiness.event.is_writable(),
2389            "the close branch must not arm backend WRITABLE when there is nothing queued"
2390        );
2391
2392        drop(frontend_peer);
2393    }
2394
2395    /// Regression guard for the SPLICE sibling of the close-before-flush
2396    /// data loss (sozu-proxy/sozu#1279 / #1290): `splice_readable`'s
2397    /// `SocketResult::Closed` arm used to return `Close` unconditionally,
2398    /// dropping whatever `splice_in_pending()` bytes sat in the kernel
2399    /// `in_pipe` when the frontend's FIN was observed. It must instead keep
2400    /// the session alive until `splice_backend_writable` drains the kernel
2401    /// pipe, then close on the next EOF re-observation once nothing is
2402    /// inflight.
2403    #[cfg(all(target_os = "linux", feature = "splice"))]
2404    #[test]
2405    fn splice_readable_eof_drains_kernel_pipe_bytes_before_closing() {
2406        use std::io::Read;
2407
2408        let (mut frontend_peer, frontend_socket) = connected_pair();
2409        let (mut backend_peer, backend_socket) = connected_pair();
2410
2411        let mut pool = Pool::with_capacity(2, 2, 4096);
2412        let backend_buffer = pool.checkout().expect("backend buffer");
2413        // The frontend buffer stays EMPTY: the splice fast path only engages
2414        // when no inherited userspace bytes remain (see `readable`'s gate).
2415        let frontend_buffer = pool.checkout().expect("frontend buffer");
2416        let address = "127.0.0.1:0".parse().expect("test address");
2417        let listener = Rc::new(RefCell::new(TestListener { address }));
2418
2419        let mut pipe = Pipe::new(
2420            backend_buffer,
2421            None,
2422            Some(TcpStream::from_std(backend_socket)),
2423            None,
2424            None,
2425            None,
2426            None,
2427            frontend_buffer,
2428            Token(0),
2429            TcpStream::from_std(frontend_socket),
2430            listener,
2431            Protocol::TCP,
2432            Ulid::generate(),
2433            Ulid::generate(),
2434            None,
2435            WebSocketContext::Tcp,
2436        );
2437        pipe.set_back_token(Token(1));
2438        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
2439        pipe.frontend_readiness.event = Ready::READABLE;
2440
2441        assert!(
2442            pipe.splice_pipe.is_some(),
2443            "Protocol::TCP must allocate the splice kernel pipe for this test to be meaningful"
2444        );
2445
2446        // 32 KiB: below the 64 KiB default pipe capacity so the whole
2447        // payload fits the kernel in_pipe without backpressure pauses.
2448        let payload = vec![0x5a_u8; 32 * 1024];
2449        frontend_peer
2450            .write_all(&payload)
2451            .expect("write frontend payload");
2452
2453        // Splice the payload into the kernel in_pipe (bounded retries:
2454        // loopback delivers the payload in several chunks, and an early
2455        // call can observe WouldBlock).
2456        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
2457        for _ in 0..100_000 {
2458            if pipe.splice_in_pending() >= payload.len() {
2459                break;
2460            }
2461            let result = pipe.splice_readable(&mut metrics);
2462            assert_ne!(
2463                result,
2464                SessionResult::Close,
2465                "splice_readable must not close while splicing the payload in"
2466            );
2467        }
2468        assert_eq!(
2469            pipe.splice_in_pending(),
2470            payload.len(),
2471            "the whole payload must sit in the kernel in_pipe before EOF"
2472        );
2473
2474        // The frontend closes right after its payload: FIN behind the
2475        // spliced bytes.
2476        drop(frontend_peer);
2477
2478        // Keep calling until EOF is observed (WriteOpen transition). The
2479        // buggy arm returned `Close` here, dropping the kernel-pipe bytes.
2480        let mut saw_eof = false;
2481        for _ in 0..100_000 {
2482            let result = pipe.splice_readable(&mut metrics);
2483            assert_ne!(
2484                result,
2485                SessionResult::Close,
2486                "splice_readable must not close while {} kernel-pipe bytes remain",
2487                pipe.splice_in_pending()
2488            );
2489            if matches!(pipe.frontend_status, ConnectionStatus::WriteOpen) {
2490                saw_eof = true;
2491                break;
2492            }
2493        }
2494        assert!(
2495            saw_eof,
2496            "frontend EOF was never observed within the retry budget"
2497        );
2498        assert_eq!(
2499            pipe.splice_in_pending(),
2500            payload.len(),
2501            "the kernel-pipe bytes must survive the frontend EOF, not be dropped"
2502        );
2503        assert!(
2504            pipe.backend_readiness.interest.is_writable()
2505                && pipe.backend_readiness.event.is_writable(),
2506            "EOF with kernel-pipe bytes pending must arm backend WRITABLE to drain them"
2507        );
2508
2509        // Drain the kernel pipe to the backend and read the peer: the bytes
2510        // must arrive byte-identical (interleave drain + read so a full
2511        // backend socket buffer cannot deadlock the loop). Once the kernel
2512        // pipe is fully drained with the frontend already at EOF, the fix
2513        // under test (sozu-proxy/sozu#1290) closes the session as part of
2514        // that same drain call -- so `drain == Close` is now expected, but
2515        // ONLY once nothing is left pending; closing any earlier would be
2516        // the mid-flight truncation this test guards against. Keep reading
2517        // from `backend_peer` regardless of the session's reported status:
2518        // the bytes were already handed off to the kernel by `splice_out`,
2519        // independently of whether `Pipe` still considers itself open.
2520        let mut received = Vec::with_capacity(payload.len());
2521        let mut closed_during_drain = false;
2522        for _ in 0..100_000 {
2523            let drain = pipe.splice_backend_writable(&mut metrics);
2524            if drain == SessionResult::Close {
2525                assert_eq!(
2526                    pipe.splice_in_pending(),
2527                    0,
2528                    "the session must only close once the kernel pipe is fully drained, \
2529                     never mid-flight"
2530                );
2531                closed_during_drain = true;
2532            }
2533            let mut buf = [0u8; 16384];
2534            match backend_peer.read(&mut buf) {
2535                Ok(0) => break,
2536                Ok(n) => received.extend_from_slice(&buf[..n]),
2537                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
2538                Err(_) => break,
2539            }
2540            if received.len() >= payload.len() {
2541                break;
2542            }
2543        }
2544        assert_eq!(
2545            received, payload,
2546            "the backend must receive the spliced tail byte-for-byte, not a truncation"
2547        );
2548        assert_eq!(
2549            pipe.splice_in_pending(),
2550            0,
2551            "the kernel in_pipe must be fully drained"
2552        );
2553        assert!(
2554            closed_during_drain,
2555            "the fix under test (sozu-proxy/sozu#1290) must close the session as soon as \
2556             the kernel pipe drains, without waiting for a separate re-observation event"
2557        );
2558
2559        // Nothing inflight and the frontend is half-closed: the session was
2560        // already closed above, during the drain itself. Re-observing EOF
2561        // here proves that closed state is stable under a repeat call,
2562        // rather than being the sole mechanism that closes the session (as
2563        // it was before the fix).
2564        assert!(
2565            !pipe.check_connections(),
2566            "with nothing inflight and the frontend closed, the session must be closeable"
2567        );
2568        assert_eq!(
2569            pipe.splice_readable(&mut metrics),
2570            SessionResult::Close,
2571            "re-observing EOF with nothing inflight must (still) close the session"
2572        );
2573    }
2574}