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    /// Kernel-pipe pair used for zero-copy `splice(2)` forwarding on
122    /// `Protocol::TCP` listeners. Allocated lazily in `new()` and
123    /// `None` for WebSocket-after-upgrade paths or when allocation
124    /// failed (caller falls back to the buffered path).
125    #[cfg(all(target_os = "linux", feature = "splice"))]
126    splice_pipe: Option<SplicePipe>,
127}
128
129impl<Front: SocketHandler, L: ListenerHandler> Pipe<Front, L> {
130    /// Instantiate a new Pipe SessionState with:
131    ///
132    /// - frontend_interest: READABLE | WRITABLE | HUP | ERROR
133    /// - frontend_event: EMPTY
134    /// - backend_interest: READABLE | WRITABLE | HUP | ERROR
135    /// - backend_event: EMPTY
136    ///
137    /// Remember to set the events from the previous State!
138    #[allow(clippy::too_many_arguments)]
139    pub fn new(
140        backend_buffer: Checkout,
141        backend_id: Option<String>,
142        backend_socket: Option<TcpStream>,
143        backend: Option<Rc<RefCell<Backend>>>,
144        container_backend_timeout: Option<TimeoutContainer>,
145        container_frontend_timeout: Option<TimeoutContainer>,
146        cluster_id: Option<String>,
147        frontend_buffer: Checkout,
148        frontend_token: Token,
149        frontend: Front,
150        listener: Rc<RefCell<L>>,
151        protocol: Protocol,
152        session_id: Ulid,
153        request_id: Ulid,
154        session_address: Option<SocketAddr>,
155        websocket_context: WebSocketContext,
156    ) -> Pipe<Front, L> {
157        let frontend_status = ConnectionStatus::Normal;
158        let backend_status = if backend_socket.is_none() {
159            ConnectionStatus::Closed
160        } else {
161            ConnectionStatus::Normal
162        };
163
164        let mut session = Pipe {
165            backend_buffer,
166            backend_id,
167            backend_readiness: Readiness {
168                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
169                event: Ready::EMPTY,
170            },
171            backend_socket,
172            backend_status,
173            backend_token: None,
174            backend,
175            cluster_id,
176            container_backend_timeout,
177            container_frontend_timeout,
178            frontend_buffer,
179            frontend_readiness: Readiness {
180                interest: Ready::READABLE | Ready::WRITABLE | Ready::HUP | Ready::ERROR,
181                event: Ready::EMPTY,
182            },
183            frontend_status,
184            frontend_token,
185            frontend,
186            listener,
187            protocol,
188            session_id,
189            request_id,
190            session_address,
191            websocket_context,
192            tls_version: None,
193            tls_cipher: None,
194            tls_sni: None,
195            tls_alpn: None,
196            #[cfg(all(target_os = "linux", feature = "splice"))]
197            splice_pipe: if protocol == Protocol::TCP {
198                SplicePipe::new()
199            } else {
200                None
201            },
202        };
203
204        session.arm_inherited_buffer_writes();
205
206        trace!("{} created pipe", log_context!(session));
207        session
208    }
209
210    fn arm_inherited_buffer_writes(&mut self) {
211        if self.backend_buffer.available_data() > 0 {
212            self.frontend_readiness.arm_writable();
213        }
214        if self.frontend_buffer.available_data() > 0 && self.backend_socket.is_some() {
215            self.backend_readiness.arm_writable();
216        }
217    }
218
219    pub fn restore_readiness_events(&mut self, frontend_event: Ready, backend_event: Ready) {
220        self.frontend_readiness.event = frontend_event;
221        self.backend_readiness.event = backend_event;
222        self.arm_inherited_buffer_writes();
223    }
224
225    /// Stamp connection-scoped TLS metadata captured at handshake time onto
226    /// the pipe for access-log emission. Called from the HTTPS→WSS upgrade
227    /// path in `https.rs::upgrade_mux` after the `Pipe` has been built from
228    /// the prior mux `HttpContext`. Leaves plaintext paths (plain TCP, plain
229    /// WS, proxy-protocol) untouched so their access logs continue to emit
230    /// `None` for all TLS fields.
231    pub fn set_tls_metadata(
232        &mut self,
233        version: Option<&'static str>,
234        cipher: Option<&'static str>,
235        sni: Option<String>,
236        alpn: Option<&'static str>,
237    ) {
238        self.tls_version = version;
239        self.tls_cipher = cipher;
240        self.tls_sni = sni;
241        self.tls_alpn = alpn;
242    }
243
244    pub fn front_socket(&self) -> &TcpStream {
245        self.frontend.socket_ref()
246    }
247
248    pub fn front_socket_mut(&mut self) -> &mut TcpStream {
249        self.frontend.socket_mut()
250    }
251
252    pub fn back_socket(&self) -> Option<&TcpStream> {
253        self.backend_socket.as_ref()
254    }
255
256    pub fn back_socket_mut(&mut self) -> Option<&mut TcpStream> {
257        self.backend_socket.as_mut()
258    }
259
260    pub fn set_back_socket(&mut self, socket: TcpStream) {
261        self.backend_socket = Some(socket);
262        self.backend_status = ConnectionStatus::Normal;
263    }
264
265    pub fn back_token(&self) -> Vec<Token> {
266        self.backend_token.iter().cloned().collect()
267    }
268
269    fn reset_timeouts(&mut self) {
270        if let Some(t) = self.container_frontend_timeout.as_mut()
271            && !t.reset()
272        {
273            error!(
274                "{} Could not reset front timeout (pipe)",
275                log_context!(self)
276            );
277        }
278
279        if let Some(t) = self.container_backend_timeout.as_mut()
280            && !t.reset()
281        {
282            error!("{} Could not reset back timeout (pipe)", log_context!(self));
283        }
284    }
285
286    pub fn set_cluster_id(&mut self, cluster_id: Option<String>) {
287        self.cluster_id = cluster_id;
288    }
289
290    pub fn set_backend_id(&mut self, backend_id: Option<String>) {
291        self.backend_id = backend_id;
292    }
293
294    pub fn set_back_token(&mut self, token: Token) {
295        self.backend_token = Some(token);
296    }
297
298    pub fn get_session_address(&self) -> Option<SocketAddr> {
299        self.session_address
300            .or_else(|| self.frontend.socket_ref().peer_addr().ok())
301    }
302
303    pub fn get_backend_address(&self) -> Option<SocketAddr> {
304        self.backend_socket
305            .as_ref()
306            .and_then(|backend| backend.peer_addr().ok())
307    }
308
309    fn protocol_string(&self) -> &'static str {
310        match self.protocol {
311            Protocol::TCP => "TCP",
312            Protocol::HTTP => "WS",
313            Protocol::HTTPS => match self.frontend.protocol() {
314                TransportProtocol::Ssl2 => "WSS-SSL2",
315                TransportProtocol::Ssl3 => "WSS-SSL3",
316                TransportProtocol::Tls1_0 => "WSS-TLS1.0",
317                TransportProtocol::Tls1_1 => "WSS-TLS1.1",
318                TransportProtocol::Tls1_2 => "WSS-TLS1.2",
319                TransportProtocol::Tls1_3 => "WSS-TLS1.3",
320                _ => unreachable!(),
321            },
322            _ => unreachable!(),
323        }
324    }
325
326    pub fn log_request(&self, metrics: &SessionMetrics, error: bool, message: Option<&str>) {
327        let listener = self.listener.borrow();
328        let context = self.log_context();
329        let endpoint = self.log_endpoint();
330        metrics.register_end_of_session(&context);
331        log_access!(
332            error,
333            on_failure: { incr!(names::access_logs::UNSENT) },
334            message,
335            context,
336            session_address: self.get_session_address(),
337            backend_address: self.get_backend_address(),
338            protocol: self.protocol_string(),
339            endpoint,
340            tags: listener.get_tags(&listener.get_addr().to_string()),
341            client_rtt: socket_rtt(self.front_socket()),
342            server_rtt: self.backend_socket.as_ref().and_then(socket_rtt),
343            service_time: metrics.service_time(),
344            response_time: metrics.backend_response_time(),
345            request_time: metrics.request_time(),
346            start_time_ns: metrics.start_wall_ns(),
347            bytes_in: metrics.bin,
348            bytes_out: metrics.bout,
349            user_agent: None,
350            x_request_id: None,
351            // Pipe is post-upgrade; the TLS metadata was captured once at
352            // handshake in `https.rs::upgrade_handshake` and plumbed through
353            // via `set_tls_metadata`. Plaintext paths leave these fields as
354            // `None` — matching the TCP log shape.
355            tls_version: self.tls_version,
356            tls_cipher: self.tls_cipher,
357            tls_sni: self.tls_sni.as_deref(),
358            tls_alpn: self.tls_alpn,
359            xff_chain: None,
360            otel: None,
361        );
362    }
363
364    pub fn log_request_success(&self, metrics: &SessionMetrics) {
365        self.log_request(metrics, false, None);
366    }
367
368    pub fn log_request_error(&self, metrics: &SessionMetrics, message: &str) {
369        incr!(names::pipe::ERRORS);
370        error!(
371            "{} Could not process request properly got: {}",
372            log_context!(self),
373            message
374        );
375        self.print_state(self.protocol_string());
376        self.log_request(metrics, true, Some(message));
377    }
378
379    /// Access-log wrapper for benign idle-timeout tear-downs.
380    ///
381    /// Unlike `log_request_error`, this path logs at `debug!` and skips the
382    /// state dump — an idle pipe hitting its front/back_timeout is expected
383    /// behaviour (e.g. a WebSocket with no keepalive) and should not pollute
384    /// the error stream.
385    pub fn log_request_timeout(&self, metrics: &SessionMetrics, message: &str) {
386        debug!("{} pipe timeout: {}", log_context!(self), message);
387        self.log_request(metrics, true, Some(message));
388    }
389
390    /// Bytes currently sitting inside the `splice` frontend→backend
391    /// kernel pipe (`0` if splice is disabled or the pipe was not
392    /// allocated). Counted as "request in flight" by `check_connections`
393    /// so a half-closed session stays alive until the kernel drains.
394    #[cfg(all(target_os = "linux", feature = "splice"))]
395    fn splice_in_pending(&self) -> usize {
396        self.splice_pipe
397            .as_ref()
398            .map(|p| p.in_pipe_pending)
399            .unwrap_or(0)
400    }
401    #[cfg(not(all(target_os = "linux", feature = "splice")))]
402    fn splice_in_pending(&self) -> usize {
403        0
404    }
405
406    /// Bytes currently sitting inside the `splice` backend→frontend
407    /// kernel pipe. Counterpart to `splice_in_pending` for the response
408    /// direction.
409    #[cfg(all(target_os = "linux", feature = "splice"))]
410    fn splice_out_pending(&self) -> usize {
411        self.splice_pipe
412            .as_ref()
413            .map(|p| p.out_pipe_pending)
414            .unwrap_or(0)
415    }
416    #[cfg(not(all(target_os = "linux", feature = "splice")))]
417    fn splice_out_pending(&self) -> usize {
418        0
419    }
420
421    /// Realised kernel-pipe capacity per direction (`0` if splice is
422    /// disabled). Drives the "pipe is full" backpressure check in the
423    /// splice readable methods and the per-call `len` for `splice_in`.
424    #[cfg(all(target_os = "linux", feature = "splice"))]
425    fn splice_capacity(&self) -> usize {
426        self.splice_pipe.as_ref().map(|p| p.capacity).unwrap_or(0)
427    }
428
429    /// Tear down both readiness trackers ahead of a `SessionResult::Close`.
430    ///
431    /// This is the *write-only-shutdown discipline* (CLAUDE.md gotcha: never
432    /// `shutdown(Shutdown::Both)` on a TLS frontend — it emits a TCP RST that
433    /// truncates the already-queued response). `Pipe` never issues an explicit
434    /// `shutdown`; it closes purely by clearing interest+event so the event
435    /// loop stops driving I/O and lets the kernel flush queued bytes, with the
436    /// peer close arriving via the normal read path. The post-condition
437    /// asserts both trackers are fully cleared.
438    fn reset_readiness_for_close(&mut self) {
439        self.frontend_readiness.reset();
440        self.backend_readiness.reset();
441        debug_assert!(
442            self.frontend_readiness.interest.is_empty() && self.frontend_readiness.event.is_empty(),
443            "frontend readiness must be fully cleared on close (write-only-shutdown discipline)"
444        );
445        debug_assert!(
446            self.backend_readiness.interest.is_empty() && self.backend_readiness.event.is_empty(),
447            "backend readiness must be fully cleared on close (write-only-shutdown discipline)"
448        );
449    }
450
451    /// Wether the session should be kept open, depending on endpoints status
452    /// and buffer usage (both in memory and in kernel)
453    pub fn check_connections(&self) -> bool {
454        // In-flight accounting must never see more *buffered* bytes than the
455        // backing Checkout buffer can hold. We intentionally do NOT bound the
456        // splice-pending counters by the pipe `capacity`: a kernel pipe buffers
457        // well beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves
458        // skb-backed GRO segments, so `splice_*_pending` legitimately exceeds it
459        // (see `splice_readable`). A violation here means a `fill`/`consume`
460        // elsewhere desynced the counters, corrupting the keep-alive decision.
461        debug_assert!(
462            self.frontend_buffer.available_data() <= self.frontend_buffer.capacity(),
463            "frontend buffered data exceeds its capacity"
464        );
465        debug_assert!(
466            self.backend_buffer.available_data() <= self.backend_buffer.capacity(),
467            "backend buffered data exceeds its capacity"
468        );
469
470        let request_is_inflight = self.frontend_buffer.available_data() > 0
471            || self.frontend_readiness.event.is_readable()
472            || self.splice_in_pending() > 0;
473        let response_is_inflight = self.backend_buffer.available_data() > 0
474            || self.backend_readiness.event.is_readable()
475            || self.splice_out_pending() > 0;
476        match (self.frontend_status, self.backend_status) {
477            (ConnectionStatus::Normal, ConnectionStatus::Normal) => true,
478            (ConnectionStatus::Normal, ConnectionStatus::ReadOpen) => true,
479            (ConnectionStatus::Normal, ConnectionStatus::WriteOpen) => {
480                // technically we should keep it open, but we'll assume that if the front
481                // is not readable and there is no in flight data front -> back or back -> front,
482                // we'll close the session, otherwise it interacts badly with HTTP connections
483                // with Connection: close header and no Content-length
484                request_is_inflight || response_is_inflight
485            }
486            (ConnectionStatus::Normal, ConnectionStatus::Closed) => response_is_inflight,
487
488            (ConnectionStatus::WriteOpen, ConnectionStatus::Normal) => {
489                // technically we should keep it open, but we'll assume that if the back
490                // is not readable and there is no in flight data back -> front or front -> back, we'll close the session
491                request_is_inflight || response_is_inflight
492            }
493            (ConnectionStatus::WriteOpen, ConnectionStatus::ReadOpen) => true,
494            (ConnectionStatus::WriteOpen, ConnectionStatus::WriteOpen) => {
495                request_is_inflight || response_is_inflight
496            }
497            (ConnectionStatus::WriteOpen, ConnectionStatus::Closed) => response_is_inflight,
498
499            (ConnectionStatus::ReadOpen, ConnectionStatus::Normal) => true,
500            (ConnectionStatus::ReadOpen, ConnectionStatus::ReadOpen) => false,
501            (ConnectionStatus::ReadOpen, ConnectionStatus::WriteOpen) => true,
502            (ConnectionStatus::ReadOpen, ConnectionStatus::Closed) => false,
503
504            (ConnectionStatus::Closed, ConnectionStatus::Normal) => request_is_inflight,
505            (ConnectionStatus::Closed, ConnectionStatus::ReadOpen) => false,
506            (ConnectionStatus::Closed, ConnectionStatus::WriteOpen) => request_is_inflight,
507            (ConnectionStatus::Closed, ConnectionStatus::Closed) => false,
508        }
509    }
510
511    pub fn frontend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
512        self.log_request_success(metrics);
513        self.frontend_status = ConnectionStatus::Closed;
514        SessionResult::Close
515    }
516
517    pub fn backend_hup(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
518        self.backend_status = ConnectionStatus::Closed;
519        // The backend hung up: its status is now terminal regardless of which
520        // keep-alive branch we take below.
521        debug_assert!(
522            matches!(self.backend_status, ConnectionStatus::Closed),
523            "backend_hup must mark the backend Closed"
524        );
525        let pipe_has_data = self.splice_out_pending() > 0;
526        if self.backend_buffer.available_data() == 0 && !pipe_has_data {
527            // No buffered or in-kernel response data: there is nothing left to
528            // drain toward the frontend on this no-data branch.
529            debug_assert_eq!(
530                self.backend_buffer.available_data(),
531                0,
532                "no-data branch entered with response bytes still buffered"
533            );
534            if self.backend_readiness.event.is_readable() {
535                self.backend_readiness.interest.insert(Ready::READABLE);
536                debug!(
537                    "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in kernel.",
538                    log_context!(self)
539                );
540                SessionResult::Continue
541            } else {
542                self.log_request_success(metrics);
543                SessionResult::Close
544            }
545        } else {
546            debug!(
547                "{} Pipe::backend_hup: backend connection closed, keeping alive due to inflight data in buffers.",
548                log_context!(self)
549            );
550            self.frontend_readiness.arm_writable();
551            if self.backend_readiness.event.is_readable() {
552                self.backend_readiness.interest.insert(Ready::READABLE);
553            }
554            SessionResult::Continue
555        }
556    }
557
558    // Read content from the session
559    pub fn readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
560        #[cfg(all(target_os = "linux", feature = "splice"))]
561        if self.protocol == Protocol::TCP && self.splice_pipe.is_some() {
562            return self.splice_readable(metrics);
563        }
564
565        self.reset_timeouts();
566
567        trace!("{} pipe readable", log_context!(self));
568        if self.frontend_buffer.available_space() == 0 {
569            self.frontend_readiness.interest.remove(Ready::READABLE);
570            self.backend_readiness.arm_writable();
571            return SessionResult::Continue;
572        }
573
574        let space_before = self.frontend_buffer.available_space();
575        let data_before = self.frontend_buffer.available_data();
576        let bin_before = metrics.bin;
577        let (sz, res) = self.frontend.socket_read(self.frontend_buffer.space());
578        // `socket_read` fills `buf[..]` and returns `min(read, buf.len())`; it
579        // can never report more bytes than the space slice it was handed.
580        debug_assert!(
581            sz <= space_before,
582            "frontend socket_read reported more bytes ({sz}) than the buffer space offered ({space_before})"
583        );
584        debug!("{} Read {} bytes", log_context!(self), sz);
585
586        if sz > 0 {
587            //FIXME: replace with copy()
588            self.frontend_buffer.fill(sz);
589            // `fill(sz)` with `sz <= available_space` moves exactly `sz` bytes
590            // from free space into readable data — no truncation, no growth.
591            debug_assert_eq!(
592                self.frontend_buffer.available_data(),
593                data_before + sz,
594                "fill must grow readable data by exactly the bytes read"
595            );
596
597            count!(names::backend::BYTES_IN, sz as i64);
598            metrics.bin += sz;
599            // Front→proxy ingress metric advances by exactly the bytes read.
600            debug_assert_eq!(
601                metrics.bin,
602                bin_before + sz,
603                "metrics.bin must advance by exactly the bytes read"
604            );
605
606            if self.frontend_buffer.available_space() == 0 {
607                self.frontend_readiness.interest.remove(Ready::READABLE);
608            }
609            self.backend_readiness.arm_writable();
610        } else {
611            self.frontend_readiness.event.remove(Ready::READABLE);
612
613            if res == SocketResult::Continue {
614                self.frontend_status = match self.frontend_status {
615                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
616                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
617                    s => s,
618                };
619            }
620        }
621
622        if !self.check_connections() {
623            self.reset_readiness_for_close();
624            self.log_request_success(metrics);
625            return SessionResult::Close;
626        }
627
628        match res {
629            SocketResult::Error => {
630                self.reset_readiness_for_close();
631                self.log_request_error(metrics, "front socket read error");
632                return SessionResult::Close;
633            }
634            SocketResult::Closed => {
635                self.reset_readiness_for_close();
636                self.log_request_success(metrics);
637                return SessionResult::Close;
638            }
639            SocketResult::WouldBlock => {
640                self.frontend_readiness.event.remove(Ready::READABLE);
641            }
642            SocketResult::Continue => {}
643        };
644
645        self.backend_readiness.arm_writable();
646        SessionResult::Continue
647    }
648
649    // Forward content to session
650    pub fn writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
651        #[cfg(all(target_os = "linux", feature = "splice"))]
652        if self.protocol == Protocol::TCP && self.splice_pipe.is_some() {
653            return self.splice_writable(metrics);
654        }
655
656        trace!("{} Pipe writable", log_context!(self));
657        if self.backend_buffer.available_data() == 0 {
658            self.backend_readiness.interest.insert(Ready::READABLE);
659            self.frontend_readiness.interest.remove(Ready::WRITABLE);
660            return SessionResult::Continue;
661        }
662
663        let queued_total = self.backend_buffer.available_data();
664        let mut sz = 0usize;
665        let mut res = SocketResult::Continue;
666        while res == SocketResult::Continue {
667            // no more data in buffer, stop here
668            if self.backend_buffer.available_data() == 0 {
669                count!(names::backend::BYTES_OUT, sz as i64);
670                metrics.bout += sz;
671                self.backend_readiness.interest.insert(Ready::READABLE);
672                self.frontend_readiness.interest.remove(Ready::WRITABLE);
673                return SessionResult::Continue;
674            }
675            let queued = self.backend_buffer.available_data();
676            let (current_sz, current_res) = self.frontend.socket_write(self.backend_buffer.data());
677            // A partial write can never report more than was queued: the
678            // socket writes from `data()` and returns `min(written, data.len())`.
679            debug_assert!(
680                current_sz <= queued,
681                "frontend socket_write reported {current_sz} bytes but only {queued} were queued"
682            );
683            res = current_res;
684            let consumed = self.backend_buffer.consume(current_sz);
685            // `consume` drops exactly the written bytes (we already proved
686            // `current_sz <= available_data`, so no clamping occurs).
687            debug_assert_eq!(
688                consumed, current_sz,
689                "consume must drop exactly the bytes written to the frontend"
690            );
691            sz += current_sz;
692            // Cumulative transfer never overruns what was queued at entry.
693            debug_assert!(
694                sz <= queued_total,
695                "cumulative frontend write ({sz}) exceeded the queued backend data ({queued_total})"
696            );
697
698            if current_sz == 0 && res == SocketResult::Continue {
699                self.frontend_status = match self.frontend_status {
700                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
701                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
702                    s => s,
703                };
704            }
705
706            if !self.check_connections() {
707                metrics.bout += sz;
708                count!(names::backend::BYTES_OUT, sz as i64);
709                self.reset_readiness_for_close();
710                self.log_request_success(metrics);
711                return SessionResult::Close;
712            }
713        }
714
715        if sz > 0 {
716            count!(names::backend::BYTES_OUT, sz as i64);
717            self.backend_readiness.interest.insert(Ready::READABLE);
718            metrics.bout += sz;
719        }
720
721        debug!(
722            "{} Wrote {} bytes of {}",
723            log_context!(self),
724            sz,
725            self.backend_buffer.available_data()
726        );
727
728        match res {
729            SocketResult::Error => {
730                self.reset_readiness_for_close();
731                self.log_request_error(metrics, "front socket write error");
732                return SessionResult::Close;
733            }
734            SocketResult::Closed => {
735                self.reset_readiness_for_close();
736                self.log_request_success(metrics);
737                return SessionResult::Close;
738            }
739            SocketResult::WouldBlock => {
740                self.frontend_readiness.event.remove(Ready::WRITABLE);
741            }
742            SocketResult::Continue => {}
743        }
744
745        SessionResult::Continue
746    }
747
748    // Forward content to cluster
749    pub fn backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
750        #[cfg(all(target_os = "linux", feature = "splice"))]
751        if self.protocol == Protocol::TCP && self.splice_pipe.is_some() {
752            return self.splice_backend_writable(metrics);
753        }
754
755        trace!("{} pipe back_writable", log_context!(self));
756
757        if self.frontend_buffer.available_data() == 0 {
758            self.frontend_readiness.interest.insert(Ready::READABLE);
759            self.backend_readiness.interest.remove(Ready::WRITABLE);
760            return SessionResult::Continue;
761        }
762
763        let output_size = self.frontend_buffer.available_data();
764
765        let mut sz = 0usize;
766        let mut socket_res = SocketResult::Continue;
767
768        if let Some(ref mut backend) = self.backend_socket {
769            while socket_res == SocketResult::Continue {
770                // no more data in buffer, stop here
771                if self.frontend_buffer.available_data() == 0 {
772                    self.frontend_readiness.interest.insert(Ready::READABLE);
773                    self.backend_readiness.interest.remove(Ready::WRITABLE);
774                    count!(names::backend::BACK_BYTES_OUT, sz as i64);
775                    metrics.backend_bout += sz;
776                    return SessionResult::Continue;
777                }
778
779                let queued = self.frontend_buffer.available_data();
780                let (current_sz, current_res) = backend.socket_write(self.frontend_buffer.data());
781                // A partial write can never report more than was queued.
782                debug_assert!(
783                    current_sz <= queued,
784                    "backend socket_write reported {current_sz} bytes but only {queued} were queued"
785                );
786                socket_res = current_res;
787                let consumed = self.frontend_buffer.consume(current_sz);
788                debug_assert_eq!(
789                    consumed, current_sz,
790                    "consume must drop exactly the bytes written to the backend"
791                );
792                sz += current_sz;
793                // Cumulative transfer never overruns the data queued at entry.
794                debug_assert!(
795                    sz <= output_size,
796                    "cumulative backend write ({sz}) exceeded the queued frontend data ({output_size})"
797                );
798
799                if current_sz == 0 && current_res == SocketResult::Continue {
800                    self.backend_status = match self.backend_status {
801                        ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
802                        ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
803                        s => s,
804                    };
805                }
806            }
807        }
808
809        let backend_bout_before = metrics.backend_bout;
810        count!(names::backend::BACK_BYTES_OUT, sz as i64);
811        metrics.backend_bout += sz;
812        // Proxy→backend egress metric advances by exactly the bytes written.
813        debug_assert_eq!(
814            metrics.backend_bout,
815            backend_bout_before + sz,
816            "metrics.backend_bout must advance by exactly the bytes written"
817        );
818
819        if !self.check_connections() {
820            self.reset_readiness_for_close();
821            self.log_request_success(metrics);
822            return SessionResult::Close;
823        }
824
825        debug!(
826            "{} Wrote {} bytes of {}",
827            log_context!(self),
828            sz,
829            output_size
830        );
831
832        match socket_res {
833            SocketResult::Error => {
834                self.reset_readiness_for_close();
835                self.log_request_error(metrics, "back socket write error");
836                return SessionResult::Close;
837            }
838            SocketResult::Closed => {
839                self.reset_readiness_for_close();
840                self.log_request_success(metrics);
841                return SessionResult::Close;
842            }
843            SocketResult::WouldBlock => {
844                self.backend_readiness.event.remove(Ready::WRITABLE);
845            }
846            SocketResult::Continue => {}
847        }
848        SessionResult::Continue
849    }
850
851    // Read content from cluster
852    pub fn backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
853        #[cfg(all(target_os = "linux", feature = "splice"))]
854        if self.protocol == Protocol::TCP && self.splice_pipe.is_some() {
855            return self.splice_backend_readable(metrics);
856        }
857
858        self.reset_timeouts();
859
860        trace!("{} Pipe backend_readable", log_context!(self));
861        if self.backend_buffer.available_space() == 0 {
862            self.backend_readiness.interest.remove(Ready::READABLE);
863            return SessionResult::Continue;
864        }
865
866        let space_before = self.backend_buffer.available_space();
867        let data_before = self.backend_buffer.available_data();
868        let backend_bin_before = metrics.backend_bin;
869        if let Some(ref mut backend) = self.backend_socket {
870            let (size, remaining) = backend.socket_read(self.backend_buffer.space());
871            // `socket_read` reports at most the space slice it was handed.
872            debug_assert!(
873                size <= space_before,
874                "backend socket_read reported more bytes ({size}) than the buffer space offered ({space_before})"
875            );
876            self.backend_buffer.fill(size);
877            // `fill(size)` with `size <= available_space` moves exactly `size`
878            // bytes from free space into readable data.
879            debug_assert_eq!(
880                self.backend_buffer.available_data(),
881                data_before + size,
882                "fill must grow readable data by exactly the bytes read"
883            );
884
885            debug!("{} Read {} bytes", log_context!(self), size);
886
887            if remaining != SocketResult::Continue || size == 0 {
888                self.backend_readiness.event.remove(Ready::READABLE);
889            }
890            if size > 0 {
891                self.frontend_readiness.arm_writable();
892                count!(names::backend::BACK_BYTES_IN, size as i64);
893                metrics.backend_bin += size;
894                // Backend→proxy ingress metric advances by exactly bytes read.
895                debug_assert_eq!(
896                    metrics.backend_bin,
897                    backend_bin_before + size,
898                    "metrics.backend_bin must advance by exactly the bytes read"
899                );
900            }
901
902            if size == 0 && remaining == SocketResult::Closed {
903                self.backend_status = match self.backend_status {
904                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
905                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
906                    s => s,
907                };
908
909                if !self.check_connections() {
910                    self.reset_readiness_for_close();
911                    self.log_request_success(metrics);
912                    return SessionResult::Close;
913                }
914            }
915
916            match remaining {
917                SocketResult::Error => {
918                    self.reset_readiness_for_close();
919                    self.log_request_error(metrics, "back socket read error");
920                    return SessionResult::Close;
921                }
922                SocketResult::Closed => {
923                    if !self.check_connections() {
924                        self.reset_readiness_for_close();
925                        self.log_request_success(metrics);
926                        return SessionResult::Close;
927                    }
928                }
929                SocketResult::WouldBlock => {
930                    self.backend_readiness.event.remove(Ready::READABLE);
931                }
932                SocketResult::Continue => {}
933            }
934        }
935
936        SessionResult::Continue
937    }
938
939    /// Zero-copy fast path of `readable`: pull bytes off the frontend
940    /// socket into the kernel `in_pipe` via `splice(2)`, then mark the
941    /// backend writable so the data drains in the next event loop tick.
942    ///
943    /// Mirrors `readable`'s `ConnectionStatus` transitions and metric
944    /// emissions exactly so observability and the `check_connections`
945    /// state machine behave the same with or without the feature flag.
946    #[cfg(all(target_os = "linux", feature = "splice"))]
947    fn splice_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
948        self.reset_timeouts();
949
950        trace!("{} pipe splice_readable", log_context!(self));
951        let capacity = self.splice_capacity();
952        if self.splice_in_pending() >= capacity {
953            // Pipe is full — stop reading and let the backend drain it.
954            self.frontend_readiness.interest.remove(Ready::READABLE);
955            self.backend_readiness.arm_writable();
956            return SessionResult::Continue;
957        }
958
959        let pending_before = self.splice_in_pending();
960        let bin_before = metrics.bin;
961        let pipe_write_end = self.splice_pipe.as_ref().unwrap().in_pipe[1];
962        let (sz, res) = splice::splice_in(self.frontend.socket_ref(), pipe_write_end, capacity);
963        // `splice_in` is asked for at most `capacity` bytes, so the kernel can
964        // never report moving more than that in one call. We deliberately do
965        // NOT assert `in_pipe_pending <= capacity`: a kernel pipe buffers well
966        // beyond its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed
967        // segments — a GRO super-packet on loopback hands a single ring slot far
968        // more than a page — so byte-occupancy legitimately exceeds `capacity`.
969        // `capacity` is the per-call `len` and a soft backpressure threshold,
970        // not a hard occupancy bound.
971        debug_assert!(
972            sz <= capacity,
973            "splice_in reported {sz} bytes but was capped at len {capacity}"
974        );
975        debug!("{} Spliced {} bytes from frontend", log_context!(self), sz);
976
977        if sz > 0 {
978            self.splice_pipe.as_mut().unwrap().in_pipe_pending += sz;
979            // Pending advanced by exactly the spliced bytes (tracks real
980            // kernel-pipe occupancy; see the capacity note above).
981            debug_assert_eq!(
982                self.splice_in_pending(),
983                pending_before + sz,
984                "in_pipe_pending must grow by exactly the spliced bytes"
985            );
986            count!(names::backend::BYTES_IN, sz as i64);
987            metrics.bin += sz;
988            debug_assert_eq!(
989                metrics.bin,
990                bin_before + sz,
991                "metrics.bin must advance by exactly the spliced bytes"
992            );
993            self.backend_readiness.arm_writable();
994        } else {
995            self.frontend_readiness.event.remove(Ready::READABLE);
996
997            if res == SocketResult::Continue {
998                self.frontend_status = match self.frontend_status {
999                    ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1000                    ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1001                    s => s,
1002                };
1003            }
1004        }
1005
1006        if !self.check_connections() {
1007            self.reset_readiness_for_close();
1008            self.log_request_success(metrics);
1009            return SessionResult::Close;
1010        }
1011
1012        match res {
1013            SocketResult::Error => {
1014                self.reset_readiness_for_close();
1015                self.log_request_error(metrics, "splice front socket read error");
1016                return SessionResult::Close;
1017            }
1018            SocketResult::Closed => {
1019                self.reset_readiness_for_close();
1020                self.log_request_success(metrics);
1021                return SessionResult::Close;
1022            }
1023            SocketResult::WouldBlock => {
1024                self.frontend_readiness.event.remove(Ready::READABLE);
1025            }
1026            SocketResult::Continue => {}
1027        }
1028
1029        self.backend_readiness.arm_writable();
1030        SessionResult::Continue
1031    }
1032
1033    /// Zero-copy fast path of `writable`: drain the backend→frontend
1034    /// kernel `out_pipe` toward the frontend socket via `splice(2)`.
1035    /// Mirrors `writable`'s loop, status transitions, and metric
1036    /// emissions.
1037    #[cfg(all(target_os = "linux", feature = "splice"))]
1038    fn splice_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1039        trace!("{} Pipe splice_writable", log_context!(self));
1040        if self.splice_out_pending() == 0 {
1041            self.backend_readiness.interest.insert(Ready::READABLE);
1042            self.frontend_readiness.interest.remove(Ready::WRITABLE);
1043            return SessionResult::Continue;
1044        }
1045
1046        let mut sz = 0usize;
1047        let mut res = SocketResult::Continue;
1048        while res == SocketResult::Continue {
1049            let pending = self.splice_out_pending();
1050            // no more data in pipe, stop here
1051            if pending == 0 {
1052                count!(names::backend::BYTES_OUT, sz as i64);
1053                metrics.bout += sz;
1054                self.backend_readiness.interest.insert(Ready::READABLE);
1055                self.frontend_readiness.interest.remove(Ready::WRITABLE);
1056                return SessionResult::Continue;
1057            }
1058
1059            let pipe_read_end = self.splice_pipe.as_ref().unwrap().out_pipe[0];
1060            let (current_sz, current_res) =
1061                splice::splice_out(pipe_read_end, self.frontend.socket_ref(), pending);
1062            // `splice_out` was asked for `pending` bytes and can drain no more
1063            // than the pipe holds; draining more than `pending` would underflow
1064            // `out_pipe_pending` below.
1065            debug_assert!(
1066                current_sz <= pending,
1067                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1068            );
1069            res = current_res;
1070            if current_sz > 0 {
1071                self.splice_pipe.as_mut().unwrap().out_pipe_pending -= current_sz;
1072                debug_assert_eq!(
1073                    self.splice_out_pending(),
1074                    pending - current_sz,
1075                    "out_pipe_pending must shrink by exactly the drained bytes"
1076                );
1077            }
1078            sz += current_sz;
1079
1080            if current_sz == 0 && res == SocketResult::Continue {
1081                self.frontend_status = match self.frontend_status {
1082                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1083                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1084                    s => s,
1085                };
1086            }
1087
1088            if !self.check_connections() {
1089                metrics.bout += sz;
1090                count!(names::backend::BYTES_OUT, sz as i64);
1091                self.reset_readiness_for_close();
1092                self.log_request_success(metrics);
1093                return SessionResult::Close;
1094            }
1095        }
1096
1097        if sz > 0 {
1098            count!(names::backend::BYTES_OUT, sz as i64);
1099            self.backend_readiness.interest.insert(Ready::READABLE);
1100            metrics.bout += sz;
1101        }
1102
1103        debug!(
1104            "{} Spliced {} bytes (out_pipe_pending={})",
1105            log_context!(self),
1106            sz,
1107            self.splice_out_pending()
1108        );
1109
1110        match res {
1111            SocketResult::Error => {
1112                self.reset_readiness_for_close();
1113                self.log_request_error(metrics, "splice front socket write error");
1114                return SessionResult::Close;
1115            }
1116            SocketResult::Closed => {
1117                self.reset_readiness_for_close();
1118                self.log_request_success(metrics);
1119                return SessionResult::Close;
1120            }
1121            SocketResult::WouldBlock => {
1122                self.frontend_readiness.event.remove(Ready::WRITABLE);
1123            }
1124            SocketResult::Continue => {}
1125        }
1126
1127        SessionResult::Continue
1128    }
1129
1130    /// Zero-copy fast path of `backend_writable`: drain the
1131    /// frontend→backend kernel `in_pipe` toward the backend socket via
1132    /// `splice(2)`. Mirrors `backend_writable`'s loop, status
1133    /// transitions, and metric emissions.
1134    #[cfg(all(target_os = "linux", feature = "splice"))]
1135    fn splice_backend_writable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1136        trace!("{} pipe splice_backend_writable", log_context!(self));
1137
1138        if self.splice_in_pending() == 0 {
1139            self.frontend_readiness.interest.insert(Ready::READABLE);
1140            self.backend_readiness.interest.remove(Ready::WRITABLE);
1141            return SessionResult::Continue;
1142        }
1143
1144        let output_size = self.splice_in_pending();
1145        let mut sz = 0usize;
1146        let mut socket_res = SocketResult::Continue;
1147
1148        while socket_res == SocketResult::Continue {
1149            let pending = self.splice_in_pending();
1150            // no more data in pipe, stop here
1151            if pending == 0 {
1152                self.frontend_readiness.interest.insert(Ready::READABLE);
1153                self.backend_readiness.interest.remove(Ready::WRITABLE);
1154                count!(names::backend::BACK_BYTES_OUT, sz as i64);
1155                metrics.backend_bout += sz;
1156                return SessionResult::Continue;
1157            }
1158
1159            let pipe_read_end = self.splice_pipe.as_ref().unwrap().in_pipe[0];
1160            let (current_sz, current_res) = match self.backend_socket.as_ref() {
1161                Some(b) => splice::splice_out(pipe_read_end, b, pending),
1162                None => break,
1163            };
1164            // Draining more than `pending` would underflow `in_pipe_pending`.
1165            debug_assert!(
1166                current_sz <= pending,
1167                "splice_out drained {current_sz} bytes but only {pending} were pending (would underflow)"
1168            );
1169            socket_res = current_res;
1170            if current_sz > 0 {
1171                self.splice_pipe.as_mut().unwrap().in_pipe_pending -= current_sz;
1172                debug_assert_eq!(
1173                    self.splice_in_pending(),
1174                    pending - current_sz,
1175                    "in_pipe_pending must shrink by exactly the drained bytes"
1176                );
1177            }
1178            sz += current_sz;
1179            // Cumulative drain never exceeds what was pending at entry.
1180            debug_assert!(
1181                sz <= output_size,
1182                "cumulative splice drain ({sz}) exceeded the bytes pending at entry ({output_size})"
1183            );
1184
1185            if current_sz == 0 && current_res == SocketResult::Continue {
1186                self.backend_status = match self.backend_status {
1187                    ConnectionStatus::Normal => ConnectionStatus::ReadOpen,
1188                    ConnectionStatus::WriteOpen => ConnectionStatus::Closed,
1189                    s => s,
1190                };
1191            }
1192        }
1193
1194        count!(names::backend::BACK_BYTES_OUT, sz as i64);
1195        metrics.backend_bout += sz;
1196
1197        if !self.check_connections() {
1198            self.reset_readiness_for_close();
1199            self.log_request_success(metrics);
1200            return SessionResult::Close;
1201        }
1202
1203        debug!(
1204            "{} Spliced {} bytes of {}",
1205            log_context!(self),
1206            sz,
1207            output_size
1208        );
1209
1210        match socket_res {
1211            SocketResult::Error => {
1212                self.reset_readiness_for_close();
1213                self.log_request_error(metrics, "splice back socket write error");
1214                return SessionResult::Close;
1215            }
1216            SocketResult::Closed => {
1217                self.reset_readiness_for_close();
1218                self.log_request_success(metrics);
1219                return SessionResult::Close;
1220            }
1221            SocketResult::WouldBlock => {
1222                self.backend_readiness.event.remove(Ready::WRITABLE);
1223            }
1224            SocketResult::Continue => {}
1225        }
1226        SessionResult::Continue
1227    }
1228
1229    /// Zero-copy fast path of `backend_readable`: pull bytes off the
1230    /// backend socket into the kernel `out_pipe` via `splice(2)`, then
1231    /// mark the frontend writable so the data drains in the next event
1232    /// loop tick. Mirrors `backend_readable`'s status transitions and
1233    /// metric emissions.
1234    #[cfg(all(target_os = "linux", feature = "splice"))]
1235    fn splice_backend_readable(&mut self, metrics: &mut SessionMetrics) -> SessionResult {
1236        self.reset_timeouts();
1237
1238        trace!("{} Pipe splice_backend_readable", log_context!(self));
1239        let capacity = self.splice_capacity();
1240        if self.splice_out_pending() >= capacity {
1241            // Pipe is full — stop reading and let the frontend drain it.
1242            self.backend_readiness.interest.remove(Ready::READABLE);
1243            self.frontend_readiness.arm_writable();
1244            return SessionResult::Continue;
1245        }
1246
1247        let pending_before = self.splice_out_pending();
1248        let backend_bin_before = metrics.backend_bin;
1249        let pipe_write_end = self.splice_pipe.as_ref().unwrap().out_pipe[1];
1250        let (size, remaining) = match self.backend_socket.as_ref() {
1251            Some(b) => splice::splice_in(b, pipe_write_end, capacity),
1252            None => return SessionResult::Continue,
1253        };
1254        // `splice_in` is capped at `len = capacity`, so the kernel never reports
1255        // moving more than that per call. As in `splice_readable`, we do NOT
1256        // assert `out_pipe_pending <= capacity`: a kernel pipe holds well beyond
1257        // its nominal `F_GETPIPE_SZ` when `splice(2)` moves skb-backed (GRO)
1258        // segments, so byte-occupancy legitimately exceeds `capacity` — it is
1259        // only the per-call `len` and a soft backpressure threshold.
1260        debug_assert!(
1261            size <= capacity,
1262            "splice_in reported {size} bytes but was capped at len {capacity}"
1263        );
1264
1265        debug!("{} Spliced {} bytes from backend", log_context!(self), size);
1266
1267        if remaining != SocketResult::Continue || size == 0 {
1268            self.backend_readiness.event.remove(Ready::READABLE);
1269        }
1270        if size > 0 {
1271            self.splice_pipe.as_mut().unwrap().out_pipe_pending += size;
1272            debug_assert_eq!(
1273                self.splice_out_pending(),
1274                pending_before + size,
1275                "out_pipe_pending must grow by exactly the spliced bytes"
1276            );
1277            self.frontend_readiness.arm_writable();
1278            count!(names::backend::BACK_BYTES_IN, size as i64);
1279            metrics.backend_bin += size;
1280            debug_assert_eq!(
1281                metrics.backend_bin,
1282                backend_bin_before + size,
1283                "metrics.backend_bin must advance by exactly the spliced bytes"
1284            );
1285        }
1286
1287        if size == 0 && remaining == SocketResult::Closed {
1288            self.backend_status = match self.backend_status {
1289                ConnectionStatus::Normal => ConnectionStatus::WriteOpen,
1290                ConnectionStatus::ReadOpen => ConnectionStatus::Closed,
1291                s => s,
1292            };
1293
1294            if !self.check_connections() {
1295                self.reset_readiness_for_close();
1296                self.log_request_success(metrics);
1297                return SessionResult::Close;
1298            }
1299        }
1300
1301        match remaining {
1302            SocketResult::Error => {
1303                self.reset_readiness_for_close();
1304                self.log_request_error(metrics, "splice back socket read error");
1305                return SessionResult::Close;
1306            }
1307            SocketResult::Closed => {
1308                if !self.check_connections() {
1309                    self.reset_readiness_for_close();
1310                    self.log_request_success(metrics);
1311                    return SessionResult::Close;
1312                }
1313            }
1314            SocketResult::WouldBlock => {
1315                self.backend_readiness.event.remove(Ready::READABLE);
1316            }
1317            SocketResult::Continue => {}
1318        }
1319
1320        SessionResult::Continue
1321    }
1322
1323    pub fn log_context(&self) -> LogContext<'_> {
1324        LogContext {
1325            session_id: self.session_id,
1326            request_id: Some(self.request_id),
1327            cluster_id: self.cluster_id.as_deref(),
1328            backend_id: self.backend_id.as_deref(),
1329        }
1330    }
1331
1332    fn log_endpoint(&self) -> EndpointRecord<'_> {
1333        match &self.websocket_context {
1334            WebSocketContext::Http {
1335                method,
1336                authority,
1337                path,
1338                status,
1339                reason,
1340            } => EndpointRecord::Http {
1341                method: method.as_deref(),
1342                authority: authority.as_deref(),
1343                path: path.as_deref(),
1344                status: status.to_owned(),
1345                reason: reason.as_deref(),
1346            },
1347            WebSocketContext::Tcp => EndpointRecord::Tcp,
1348        }
1349    }
1350}
1351
1352impl<Front: SocketHandler, L: ListenerHandler> SessionState for Pipe<Front, L> {
1353    fn ready(
1354        &mut self,
1355        _session: Rc<RefCell<dyn crate::ProxySession>>,
1356        _proxy: Rc<RefCell<dyn crate::L7Proxy>>,
1357        metrics: &mut SessionMetrics,
1358    ) -> SessionResult {
1359        let mut counter = 0;
1360
1361        if self.frontend_readiness.event.is_hup() {
1362            return SessionResult::Close;
1363        }
1364
1365        while counter < MAX_LOOP_ITERATIONS {
1366            let frontend_interest = self.frontend_readiness.filter_interest();
1367            let backend_interest = self.backend_readiness.filter_interest();
1368
1369            trace!(
1370                "{} Frontend interest({:?}), backend interest({:?})",
1371                log_context!(self),
1372                frontend_interest,
1373                backend_interest
1374            );
1375            if frontend_interest.is_empty() && backend_interest.is_empty() {
1376                break;
1377            }
1378
1379            if self.backend_readiness.event.is_hup()
1380                && self.frontend_readiness.interest.is_writable()
1381                && !self.frontend_readiness.event.is_writable()
1382            {
1383                break;
1384            }
1385
1386            if frontend_interest.is_readable() && self.readable(metrics) == SessionResult::Close {
1387                return SessionResult::Close;
1388            }
1389
1390            if backend_interest.is_writable()
1391                && self.backend_writable(metrics) == SessionResult::Close
1392            {
1393                return SessionResult::Close;
1394            }
1395
1396            if backend_interest.is_readable()
1397                && self.backend_readable(metrics) == SessionResult::Close
1398            {
1399                return SessionResult::Close;
1400            }
1401
1402            if frontend_interest.is_writable() && self.writable(metrics) == SessionResult::Close {
1403                return SessionResult::Close;
1404            }
1405
1406            if backend_interest.is_hup() && self.backend_hup(metrics) == SessionResult::Close {
1407                return SessionResult::Close;
1408            }
1409
1410            if frontend_interest.is_error() {
1411                error!(
1412                    "{} Frontend socket error, disconnecting",
1413                    log_context!(self)
1414                );
1415
1416                self.frontend_readiness.interest = Ready::EMPTY;
1417                self.backend_readiness.interest = Ready::EMPTY;
1418
1419                return SessionResult::Close;
1420            }
1421
1422            if backend_interest.is_error() && self.backend_hup(metrics) == SessionResult::Close {
1423                self.frontend_readiness.interest = Ready::EMPTY;
1424                self.backend_readiness.interest = Ready::EMPTY;
1425
1426                error!("{} Backend socket error, disconnecting", log_context!(self));
1427                return SessionResult::Close;
1428            }
1429
1430            counter += 1;
1431        }
1432
1433        if counter >= MAX_LOOP_ITERATIONS {
1434            error!(
1435                "{}\tHandling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
1436                log_context!(self),
1437                MAX_LOOP_ITERATIONS
1438            );
1439
1440            incr!(names::http::INFINITE_LOOP_ERROR);
1441            self.print_state(self.protocol_string());
1442
1443            return SessionResult::Close;
1444        }
1445
1446        SessionResult::Continue
1447    }
1448
1449    fn update_readiness(&mut self, token: Token, events: Ready) {
1450        if self.frontend_token == token {
1451            self.frontend_readiness.event |= events;
1452        } else if self.backend_token == Some(token) {
1453            self.backend_readiness.event |= events;
1454        }
1455    }
1456
1457    fn timeout(&mut self, token: Token, metrics: &mut SessionMetrics) -> StateResult {
1458        //info!("got timeout for token: {:?}", token);
1459        if self.frontend_token == token {
1460            self.log_request_timeout(metrics, "frontend socket timeout");
1461            if let Some(timeout) = self.container_frontend_timeout.as_mut() {
1462                timeout.triggered()
1463            }
1464            return StateResult::CloseSession;
1465        }
1466
1467        if self.backend_token == Some(token) {
1468            //info!("backend timeout triggered for token {:?}", token);
1469            if let Some(timeout) = self.container_backend_timeout.as_mut() {
1470                timeout.triggered()
1471            }
1472
1473            self.log_request_timeout(metrics, "backend socket timeout");
1474            return StateResult::CloseSession;
1475        }
1476
1477        error!("{} Got timeout for an invalid token", log_context!(self));
1478        self.log_request_error(metrics, "invalid token timeout");
1479        StateResult::CloseSession
1480    }
1481
1482    fn cancel_timeouts(&mut self) {
1483        self.container_frontend_timeout.as_mut().map(|t| t.cancel());
1484        self.container_backend_timeout.as_mut().map(|t| t.cancel());
1485    }
1486
1487    fn close(&mut self, _proxy: Rc<RefCell<dyn L7Proxy>>, _metrics: &mut SessionMetrics) {
1488        if let Some(backend) = self.backend.as_mut() {
1489            let mut backend = backend.borrow_mut();
1490            backend.active_requests = backend.active_requests.saturating_sub(1);
1491        }
1492    }
1493
1494    fn print_state(&self, context: &str) {
1495        error!(
1496            "\
1497{} {} Session(Pipe)
1498\tFrontend:
1499\t\ttoken: {:?}\treadiness: {:?}
1500\tBackend:
1501\t\ttoken: {:?}\treadiness: {:?}",
1502            log_context!(self),
1503            context,
1504            self.frontend_token,
1505            self.frontend_readiness,
1506            self.backend_token,
1507            self.backend_readiness
1508        );
1509    }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use std::{
1515        collections::BTreeMap,
1516        io::Write,
1517        net::{TcpListener as StdTcpListener, TcpStream as StdTcpStream},
1518        time::Duration,
1519    };
1520
1521    use super::*;
1522    use crate::pool::Pool;
1523
1524    struct TestListener {
1525        address: SocketAddr,
1526    }
1527
1528    impl ListenerHandler for TestListener {
1529        fn get_addr(&self) -> &SocketAddr {
1530            &self.address
1531        }
1532
1533        fn get_tags(&self, _key: &str) -> Option<&sozu_command::logging::CachedTags> {
1534            None
1535        }
1536
1537        fn set_tags(&mut self, _key: String, _tags: Option<BTreeMap<String, String>>) {}
1538
1539        fn protocol(&self) -> Protocol {
1540            Protocol::HTTP
1541        }
1542
1543        fn public_address(&self) -> SocketAddr {
1544            self.address
1545        }
1546    }
1547
1548    fn connected_pair() -> (StdTcpStream, StdTcpStream) {
1549        let listener = StdTcpListener::bind("127.0.0.1:0").expect("bind test listener");
1550        let address = listener.local_addr().expect("listener local addr");
1551        let client = StdTcpStream::connect(address).expect("connect test client");
1552        let (server, _) = listener.accept().expect("accept test server");
1553        client.set_nonblocking(true).expect("client nonblocking");
1554        server.set_nonblocking(true).expect("server nonblocking");
1555        (client, server)
1556    }
1557
1558    #[test]
1559    fn backend_readable_arms_frontend_writable_event_when_buffering_response() {
1560        let (frontend_peer, frontend_socket) = connected_pair();
1561        let (mut backend_peer, backend_socket) = connected_pair();
1562
1563        let mut pool = Pool::with_capacity(2, 2, 4096);
1564        let backend_buffer = pool.checkout().expect("backend buffer");
1565        let frontend_buffer = pool.checkout().expect("frontend buffer");
1566        let address = "127.0.0.1:0".parse().expect("test address");
1567        let listener = Rc::new(RefCell::new(TestListener { address }));
1568
1569        let mut pipe = Pipe::new(
1570            backend_buffer,
1571            None,
1572            Some(TcpStream::from_std(backend_socket)),
1573            None,
1574            None,
1575            None,
1576            None,
1577            frontend_buffer,
1578            Token(0),
1579            TcpStream::from_std(frontend_socket),
1580            listener,
1581            Protocol::HTTP,
1582            Ulid::generate(),
1583            Ulid::generate(),
1584            None,
1585            WebSocketContext::Tcp,
1586        );
1587        pipe.set_back_token(Token(1));
1588        pipe.frontend_readiness.event = Ready::EMPTY;
1589        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1590        pipe.backend_readiness.event = Ready::READABLE;
1591        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1592
1593        backend_peer
1594            .write_all(b"server-speaks-first")
1595            .expect("write backend payload");
1596
1597        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1598        assert_eq!(pipe.backend_readable(&mut metrics), SessionResult::Continue);
1599
1600        assert!(
1601            pipe.backend_buffer.available_data() > 0,
1602            "backend_readable must buffer backend bytes"
1603        );
1604        assert!(
1605            pipe.frontend_readiness.interest.is_writable(),
1606            "buffered backend bytes must arm frontend WRITABLE interest"
1607        );
1608        assert!(
1609            pipe.frontend_readiness.event.is_writable(),
1610            "buffered backend bytes must queue a frontend WRITABLE event"
1611        );
1612
1613        drop(frontend_peer);
1614    }
1615
1616    #[test]
1617    fn frontend_readable_arms_backend_writable_event_when_buffering_request() {
1618        let (mut frontend_peer, frontend_socket) = connected_pair();
1619        let (backend_peer, backend_socket) = connected_pair();
1620
1621        let mut pool = Pool::with_capacity(2, 2, 4096);
1622        let backend_buffer = pool.checkout().expect("backend buffer");
1623        let frontend_buffer = pool.checkout().expect("frontend buffer");
1624        let address = "127.0.0.1:0".parse().expect("test address");
1625        let listener = Rc::new(RefCell::new(TestListener { address }));
1626
1627        let mut pipe = Pipe::new(
1628            backend_buffer,
1629            None,
1630            Some(TcpStream::from_std(backend_socket)),
1631            None,
1632            None,
1633            None,
1634            None,
1635            frontend_buffer,
1636            Token(0),
1637            TcpStream::from_std(frontend_socket),
1638            listener,
1639            Protocol::HTTP,
1640            Ulid::generate(),
1641            Ulid::generate(),
1642            None,
1643            WebSocketContext::Tcp,
1644        );
1645        pipe.set_back_token(Token(1));
1646        pipe.frontend_readiness.event = Ready::READABLE;
1647        pipe.frontend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1648        pipe.backend_readiness.event = Ready::EMPTY;
1649        pipe.backend_readiness.interest = Ready::READABLE | Ready::HUP | Ready::ERROR;
1650
1651        frontend_peer
1652            .write_all(b"client-speaks-after-upgrade")
1653            .expect("write frontend payload");
1654
1655        let mut metrics = SessionMetrics::new(Some(Duration::ZERO));
1656        assert_eq!(pipe.readable(&mut metrics), SessionResult::Continue);
1657
1658        assert!(
1659            pipe.frontend_buffer.available_data() > 0,
1660            "readable must buffer frontend bytes"
1661        );
1662        assert!(
1663            pipe.backend_readiness.interest.is_writable(),
1664            "buffered frontend bytes must arm backend WRITABLE interest"
1665        );
1666        assert!(
1667            pipe.backend_readiness.event.is_writable(),
1668            "buffered frontend bytes must queue a backend WRITABLE event"
1669        );
1670
1671        drop(backend_peer);
1672    }
1673
1674    #[test]
1675    fn restore_readiness_events_rearms_inherited_buffered_writes() {
1676        let (frontend_peer, frontend_socket) = connected_pair();
1677        let (backend_peer, backend_socket) = connected_pair();
1678
1679        let mut pool = Pool::with_capacity(2, 2, 4096);
1680        let mut backend_buffer = pool.checkout().expect("backend buffer");
1681        let mut frontend_buffer = pool.checkout().expect("frontend buffer");
1682        backend_buffer
1683            .write_all(b"backend bytes inherited from 101 read")
1684            .expect("write backend buffer");
1685        frontend_buffer
1686            .write_all(b"frontend bytes inherited from upgrade read")
1687            .expect("write frontend buffer");
1688        let address = "127.0.0.1:0".parse().expect("test address");
1689        let listener = Rc::new(RefCell::new(TestListener { address }));
1690
1691        let mut pipe = Pipe::new(
1692            backend_buffer,
1693            None,
1694            Some(TcpStream::from_std(backend_socket)),
1695            None,
1696            None,
1697            None,
1698            None,
1699            frontend_buffer,
1700            Token(0),
1701            TcpStream::from_std(frontend_socket),
1702            listener,
1703            Protocol::HTTP,
1704            Ulid::generate(),
1705            Ulid::generate(),
1706            None,
1707            WebSocketContext::Tcp,
1708        );
1709
1710        pipe.restore_readiness_events(Ready::EMPTY, Ready::EMPTY);
1711
1712        assert!(
1713            pipe.frontend_readiness.event.is_writable(),
1714            "restoring inherited frontend events must not park backend-buffered bytes"
1715        );
1716        assert!(
1717            pipe.backend_readiness.event.is_writable(),
1718            "restoring inherited backend events must not park frontend-buffered bytes"
1719        );
1720
1721        drop(frontend_peer);
1722        drop(backend_peer);
1723    }
1724}