Skip to main content

rama_core/stream/
forward.rs

1//! Bridge two duplex [`Stream`] + [`Sink`] endpoints by pumping items between them.
2//!
3//! Where [`crate::io::BridgeIo`] copies *bytes* between two byte-oriented
4//! halves, [`StreamForwardService`] copies *frames* (or any other typed
5//! items) between two `Stream + Sink` halves.
6//!
7//! Both halves must agree on the item type `T`. If they do not — for example
8//! one side carries `(Bytes, SocketAddr)` while the other carries plain
9//! `Bytes` — that mismatch is the *transport's* problem to solve, by using
10//! a connected variant of the underlying socket, or by mapping with
11//! [`StreamExt::map`] / [`SinkExt::with`] from [`futures`]. The forwarder
12//! itself stays dumb on purpose: it pumps, it does not translate.
13//!
14//! See [`crate::Service`] for the service abstraction this plugs into.
15
16use core::pin::Pin;
17use std::time::Duration;
18
19use futures::{Sink, SinkExt, Stream, StreamExt};
20use rama_error::{BoxError, ErrorExt};
21use rama_utils::macros::generate_set_and_with;
22
23use crate::Service;
24use crate::graceful::ShutdownGuard;
25use crate::telemetry::tracing;
26
27/// Reason why a rama bridge — byte-oriented (see `IoForwardService` in
28/// `rama-net`) or frame-oriented (see [`StreamForwardService`]) — terminated.
29///
30/// Shared vocabulary used in close-log events emitted by rama bridges.
31/// Consumers are free to emit any subset; each variant carries no metadata
32/// of its own.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum BridgeCloseReason {
36    /// Graceful shutdown was requested via the configured shutdown signal.
37    Shutdown,
38    /// The bridge observed no progress in either direction within the
39    /// configured idle window.
40    IdleTimeout,
41    /// The "left" / `a` side reached EOF.
42    PeerEofLeft,
43    /// The "right" / `b` side reached EOF.
44    PeerEofRight,
45    /// Read from the left half failed.
46    ReadErrorLeft,
47    /// Read from the right half failed.
48    ReadErrorRight,
49    /// Write to the left half failed.
50    WriteErrorLeft,
51    /// Write to the right half failed.
52    WriteErrorRight,
53    /// A protocol-peek read deadline elapsed before the peek completed.
54    /// Used by tproxy bridges that peek the first bytes for protocol detection.
55    PeekTimeout,
56    /// The flow handler did not produce a decision within the configured
57    /// deadline. The flow was rejected (or passed through, depending on
58    /// configuration) without bridging.
59    HandlerDeadline,
60    /// A backpressure-paused write side was never re-armed by its peer
61    /// drain signal within the configured maximum-pause window. Surfaces
62    /// stuck downstream writers (e.g. a Swift `flow.write` completion
63    /// handler that never invokes `signalServerDrain`) instead of
64    /// wedging the bridge indefinitely.
65    PausedTimeout,
66    /// The upstream (right / egress) half wrote no byte within the
67    /// configured first-byte window. Surfaces a silent origin that
68    /// accepts the connection but never responds, distinct from
69    /// [`IdleTimeout`](Self::IdleTimeout), which only fires when *neither*
70    /// direction moves (a silent origin still receives the client's bytes).
71    FirstByteTimeout,
72}
73
74impl core::fmt::Display for BridgeCloseReason {
75    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76        f.write_str(match self {
77            Self::Shutdown => "shutdown",
78            Self::IdleTimeout => "idle_timeout",
79            Self::PeerEofLeft => "peer_eof_left",
80            Self::PeerEofRight => "peer_eof_right",
81            Self::ReadErrorLeft => "read_error_left",
82            Self::ReadErrorRight => "read_error_right",
83            Self::WriteErrorLeft => "write_error_left",
84            Self::WriteErrorRight => "write_error_right",
85            Self::PeekTimeout => "peek_timeout",
86            Self::HandlerDeadline => "handler_deadline",
87            Self::PausedTimeout => "paused_timeout",
88            Self::FirstByteTimeout => "first_byte_timeout",
89        })
90    }
91}
92
93#[cfg(feature = "dial9")]
94#[cfg_attr(docsrs, doc(cfg(feature = "dial9")))]
95impl dial9_trace_format::TraceField for BridgeCloseReason {
96    type Ref<'a> = Self;
97
98    fn field_type() -> dial9_trace_format::types::FieldType {
99        dial9_trace_format::types::FieldType::U8
100    }
101
102    fn encode<W: std::io::Write>(
103        &self,
104        enc: &mut dial9_trace_format::EventEncoder<'_, W>,
105    ) -> std::io::Result<()> {
106        let code = match self {
107            Self::Shutdown => 1,
108            Self::IdleTimeout => 2,
109            Self::PeerEofLeft => 3,
110            Self::PeerEofRight => 4,
111            Self::ReadErrorLeft => 5,
112            Self::ReadErrorRight => 6,
113            Self::WriteErrorLeft => 7,
114            Self::WriteErrorRight => 8,
115            Self::PeekTimeout => 9,
116            Self::HandlerDeadline => 10,
117            Self::PausedTimeout => 11,
118            Self::FirstByteTimeout => 12,
119        };
120        enc.write_u8(code)
121    }
122
123    fn decode_ref<'a>(val: &dial9_trace_format::types::FieldValueRef<'a>) -> Option<Self::Ref<'a>> {
124        use dial9_trace_format::types::FieldValueRef;
125        match val {
126            FieldValueRef::Varint(1) => Some(Self::Shutdown),
127            FieldValueRef::Varint(2) => Some(Self::IdleTimeout),
128            FieldValueRef::Varint(3) => Some(Self::PeerEofLeft),
129            FieldValueRef::Varint(4) => Some(Self::PeerEofRight),
130            FieldValueRef::Varint(5) => Some(Self::ReadErrorLeft),
131            FieldValueRef::Varint(6) => Some(Self::ReadErrorRight),
132            FieldValueRef::Varint(7) => Some(Self::WriteErrorLeft),
133            FieldValueRef::Varint(8) => Some(Self::WriteErrorRight),
134            FieldValueRef::Varint(9) => Some(Self::PeekTimeout),
135            FieldValueRef::Varint(10) => Some(Self::HandlerDeadline),
136            FieldValueRef::Varint(11) => Some(Self::PausedTimeout),
137            FieldValueRef::Varint(12) => Some(Self::FirstByteTimeout),
138            _ => None,
139        }
140    }
141}
142
143/// Input to [`StreamForwardService`]: the two duplex endpoints to bridge.
144///
145/// Both `a` and `b` must be [`Stream`] + [`Sink`] over the *same* item type
146/// `T`. To bridge endpoints whose native item types differ, adapt one or
147/// both with [`StreamExt::map`] / [`SinkExt::with`] (or use a duplex wrapper
148/// such as `rama_udp::ConnectedUdpFramed` that exposes the desired type
149/// natively) before constructing the bridge.
150#[derive(Debug)]
151pub struct StreamBridge<A, B> {
152    /// The "left" / `a` endpoint.
153    pub a: A,
154    /// The "right" / `b` endpoint.
155    pub b: B,
156}
157
158impl<A, B> StreamBridge<A, B> {
159    /// Create a new [`StreamBridge`] from two duplex endpoints.
160    pub fn new(a: A, b: B) -> Self {
161        Self { a, b }
162    }
163}
164
165/// A [`Service`] which takes a [`StreamBridge`] and pumps frames between
166/// the two endpoints bidirectionally.
167///
168/// The service optionally observes a [`ShutdownGuard`] (for graceful
169/// termination) and an idle timeout that closes the bridge when no frame
170/// has been forwarded in either direction within the configured window.
171///
172/// Returns a [`BridgeCloseReason`] describing why the bridge ended.
173#[derive(Debug, Clone, Default)]
174pub struct StreamForwardService {
175    idle_timeout: Option<Duration>,
176    shutdown_guard: Option<ShutdownGuard>,
177}
178
179impl StreamForwardService {
180    /// Create a new [`StreamForwardService`] with no idle timeout and no
181    /// shutdown guard. Equivalent to [`StreamForwardService::default`].
182    #[must_use]
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    generate_set_and_with! {
188        /// Idle timeout. When set, the bridge closes with reason
189        /// [`BridgeCloseReason::IdleTimeout`] if no frame has been
190        /// forwarded in either direction within `timeout`.
191        ///
192        /// `None` (the default) disables idle detection.
193        pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
194            self.idle_timeout = timeout;
195            self
196        }
197    }
198
199    generate_set_and_with! {
200        /// [`ShutdownGuard`] used to observe graceful-shutdown signals.
201        /// When the guard fires, the bridge closes with reason
202        /// [`BridgeCloseReason::Shutdown`].
203        ///
204        /// `None` (the default) means no shutdown observation.
205        pub fn shutdown_guard(mut self, guard: Option<ShutdownGuard>) -> Self {
206            self.shutdown_guard = guard;
207            self
208        }
209    }
210}
211
212impl<A, B, T, EA, EB> Service<StreamBridge<A, B>> for StreamForwardService
213where
214    A: Stream<Item = Result<T, EA>> + Sink<T, Error = EA> + Send + Unpin + 'static,
215    B: Stream<Item = Result<T, EB>> + Sink<T, Error = EB> + Send + Unpin + 'static,
216    T: Send + 'static,
217    EA: Into<BoxError> + Send + 'static,
218    EB: Into<BoxError> + Send + 'static,
219{
220    type Output = BridgeCloseReason;
221    type Error = BoxError;
222
223    async fn serve(&self, bridge: StreamBridge<A, B>) -> Result<Self::Output, Self::Error> {
224        let StreamBridge { a, b } = bridge;
225        run_bridge(a, b, self.idle_timeout, self.shutdown_guard.clone()).await
226    }
227}
228
229async fn run_bridge<A, B, T, EA, EB>(
230    a: A,
231    b: B,
232    idle_timeout: Option<Duration>,
233    guard: Option<ShutdownGuard>,
234) -> Result<BridgeCloseReason, BoxError>
235where
236    A: Stream<Item = Result<T, EA>> + Sink<T, Error = EA> + Send + Unpin,
237    B: Stream<Item = Result<T, EB>> + Sink<T, Error = EB> + Send + Unpin,
238    T: Send,
239    EA: Into<BoxError> + Send,
240    EB: Into<BoxError> + Send,
241{
242    let (mut a_sink, mut a_stream) = a.split();
243    let (mut b_sink, mut b_stream) = b.split();
244
245    let mut a_done = false;
246    let mut b_done = false;
247    // The reason of whichever side ended first — that's the one that
248    // initiated the close. The other side just drained whatever the
249    // initiator had buffered before its half-close. Default value is
250    // never observed without being overwritten because the loop only
251    // exits via `a_done && b_done`, which requires at least one EOF
252    // arm to have run.
253    let mut first_eof = BridgeCloseReason::PeerEofLeft;
254
255    let mut idle: Option<Pin<Box<tokio::time::Sleep>>> =
256        idle_timeout.map(|d| Box::pin(tokio::time::sleep(d)));
257    // Progress counter: bumped on every successful forward. The idle arm
258    // re-checks this against `last_progress` before declaring a timeout,
259    // to absorb the race where idle fires in the same select tick that a
260    // forward also became ready.
261    let mut progress: u64 = 0;
262    let mut last_progress: u64 = 0;
263
264    let result = loop {
265        if a_done && b_done {
266            break Ok(first_eof);
267        }
268
269        let cancelled = async {
270            match guard.as_ref() {
271                Some(g) => g.cancelled().await,
272                None => core::future::pending::<()>().await,
273            }
274        };
275
276        let idle_tick = async {
277            match idle.as_mut() {
278                Some(s) => s.as_mut().await,
279                None => core::future::pending::<()>().await,
280            }
281        };
282
283        tokio::select! {
284            biased;
285            () = cancelled => break Ok(BridgeCloseReason::Shutdown),
286            () = idle_tick => {
287                // Re-check the progress counter: a forward may have
288                // completed in the same poll cycle that idle fired.
289                if progress != last_progress {
290                    last_progress = progress;
291                    if let (Some(d), Some(s)) = (idle_timeout, idle.as_mut()) {
292                        s.as_mut().reset(tokio::time::Instant::now() + d);
293                    }
294                    continue;
295                }
296                break Ok(BridgeCloseReason::IdleTimeout);
297            }
298
299            item = a_stream.next(), if !a_done => match item {
300                Some(Ok(t)) => {
301                    if let Err(e) = b_sink.send(t).await {
302                        break Err((BridgeCloseReason::WriteErrorRight, e.into_box_error()));
303                    }
304                    progress = progress.wrapping_add(1);
305                    if let (Some(d), Some(s)) = (idle_timeout, idle.as_mut()) {
306                        s.as_mut().reset(tokio::time::Instant::now() + d);
307                    }
308                }
309                Some(Err(e)) => break Err((BridgeCloseReason::ReadErrorLeft, e.into_box_error())),
310                None => {
311                    if !b_done {
312                        first_eof = BridgeCloseReason::PeerEofLeft;
313                    }
314                    a_done = true;
315                    if let Err(err) = b_sink.close().await {
316                        tracing::debug!(
317                            target: "rama_core::stream::forward",
318                            error = %err.into_box_error(),
319                            "stream forward bridge: error while half-closing `b` after `a` EOF",
320                        );
321                    }
322                }
323            },
324
325            item = b_stream.next(), if !b_done => match item {
326                Some(Ok(t)) => {
327                    if let Err(e) = a_sink.send(t).await {
328                        break Err((BridgeCloseReason::WriteErrorLeft, e.into_box_error()));
329                    }
330                    progress = progress.wrapping_add(1);
331                    if let (Some(d), Some(s)) = (idle_timeout, idle.as_mut()) {
332                        s.as_mut().reset(tokio::time::Instant::now() + d);
333                    }
334                }
335                Some(Err(e)) => break Err((BridgeCloseReason::ReadErrorRight, e.into_box_error())),
336                None => {
337                    if !a_done {
338                        first_eof = BridgeCloseReason::PeerEofRight;
339                    }
340                    b_done = true;
341                    if let Err(err) = a_sink.close().await {
342                        tracing::debug!(
343                            target: "rama_core::stream::forward",
344                            error = %err.into_box_error(),
345                            "stream forward bridge: error while half-closing `a` after `b` EOF",
346                        );
347                    }
348                }
349            },
350        }
351    };
352
353    match result {
354        Ok(reason) => {
355            tracing::trace!(
356                target: "rama_core::stream::forward",
357                reason = %reason,
358                "stream forward bridge closed",
359            );
360            Ok(reason)
361        }
362        Err((reason, err)) => {
363            tracing::debug!(
364                target: "rama_core::stream::forward",
365                reason = %reason,
366                error = %err,
367                "stream forward bridge closed with error",
368            );
369            Err(err)
370        }
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use futures::channel::mpsc;
378    use std::time::Instant;
379
380    /// Build a pair of duplex endpoints over `mpsc` channels for testing.
381    /// `(a, b)` are wired such that items sent on `a` arrive on `b.next()`
382    /// and vice versa.
383    fn duplex_pair<T: Send + 'static>() -> (DuplexEndpoint<T>, DuplexEndpoint<T>) {
384        let (a_tx, b_rx) = mpsc::unbounded::<T>();
385        let (b_tx, a_rx) = mpsc::unbounded::<T>();
386        (
387            DuplexEndpoint::new(a_tx, a_rx),
388            DuplexEndpoint::new(b_tx, b_rx),
389        )
390    }
391
392    struct DuplexEndpoint<T> {
393        tx: mpsc::UnboundedSender<T>,
394        rx: mpsc::UnboundedReceiver<T>,
395    }
396
397    impl<T> DuplexEndpoint<T> {
398        fn new(tx: mpsc::UnboundedSender<T>, rx: mpsc::UnboundedReceiver<T>) -> Self {
399            Self { tx, rx }
400        }
401    }
402
403    impl<T> Stream for DuplexEndpoint<T> {
404        type Item = Result<T, std::io::Error>;
405        fn poll_next(
406            mut self: Pin<&mut Self>,
407            cx: &mut core::task::Context<'_>,
408        ) -> core::task::Poll<Option<Self::Item>> {
409            Pin::new(&mut self.rx).poll_next(cx).map(|opt| opt.map(Ok))
410        }
411    }
412
413    impl<T> Sink<T> for DuplexEndpoint<T> {
414        type Error = std::io::Error;
415        fn poll_ready(
416            self: Pin<&mut Self>,
417            _cx: &mut core::task::Context<'_>,
418        ) -> core::task::Poll<Result<(), Self::Error>> {
419            core::task::Poll::Ready(Ok(()))
420        }
421        fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
422            self.get_mut()
423                .tx
424                .unbounded_send(item)
425                .map_err(|_err| std::io::Error::other("send on closed channel"))
426        }
427        fn poll_flush(
428            self: Pin<&mut Self>,
429            _cx: &mut core::task::Context<'_>,
430        ) -> core::task::Poll<Result<(), Self::Error>> {
431            core::task::Poll::Ready(Ok(()))
432        }
433        fn poll_close(
434            self: Pin<&mut Self>,
435            _cx: &mut core::task::Context<'_>,
436        ) -> core::task::Poll<Result<(), Self::Error>> {
437            self.tx.close_channel();
438            core::task::Poll::Ready(Ok(()))
439        }
440    }
441
442    impl<T> Unpin for DuplexEndpoint<T> {}
443
444    #[tokio::test]
445    async fn forwards_in_both_directions() {
446        let (mut a_user, a_proxy) = duplex_pair::<u32>();
447        let (mut b_user, b_proxy) = duplex_pair::<u32>();
448
449        let svc = StreamForwardService::new();
450        let task = tokio::spawn(async move {
451            svc.serve(StreamBridge::new(a_proxy, b_proxy))
452                .await
453                .unwrap()
454        });
455
456        a_user.send(1).await.unwrap();
457        a_user.send(2).await.unwrap();
458        let r1 = b_user.next().await.unwrap().unwrap();
459        let r2 = b_user.next().await.unwrap().unwrap();
460        assert_eq!((r1, r2), (1, 2));
461
462        b_user.send(10).await.unwrap();
463        let r = a_user.next().await.unwrap().unwrap();
464        assert_eq!(r, 10);
465
466        drop(a_user);
467        drop(b_user);
468        let reason = tokio::time::timeout(Duration::from_secs(2), task)
469            .await
470            .expect("bridge did not unwind within 2s")
471            .unwrap();
472        assert!(matches!(
473            reason,
474            BridgeCloseReason::PeerEofLeft | BridgeCloseReason::PeerEofRight
475        ));
476    }
477
478    #[tokio::test]
479    async fn idle_timeout_fires_on_no_progress() {
480        let (a_user, a_proxy) = duplex_pair::<u32>();
481        let (b_user, b_proxy) = duplex_pair::<u32>();
482
483        let svc = StreamForwardService::new().with_idle_timeout(Duration::from_millis(100));
484        let started = Instant::now();
485        let reason = tokio::time::timeout(
486            Duration::from_secs(2),
487            svc.serve(StreamBridge::new(a_proxy, b_proxy)),
488        )
489        .await
490        .expect("idle bridge did not unwind within 2s")
491        .unwrap();
492        let elapsed = started.elapsed();
493        assert_eq!(reason, BridgeCloseReason::IdleTimeout);
494        assert!(
495            elapsed >= Duration::from_millis(80),
496            "idle bridge unwound too early: {elapsed:?}",
497        );
498        // Keep peers alive past the assertion so they don't EOF early.
499        drop(a_user);
500        drop(b_user);
501    }
502
503    #[tokio::test]
504    async fn idle_timer_resets_on_activity() {
505        let (mut a_user, a_proxy) = duplex_pair::<u32>();
506        let (mut b_user, b_proxy) = duplex_pair::<u32>();
507
508        let svc = StreamForwardService::new().with_idle_timeout(Duration::from_millis(150));
509        let task = tokio::spawn(async move {
510            svc.serve(StreamBridge::new(a_proxy, b_proxy))
511                .await
512                .unwrap()
513        });
514
515        // Push one item every 50ms for ~400ms — total elapsed > idle
516        // window, but each individual gap is well below it. The bridge
517        // must not declare IdleTimeout.
518        for i in 0..8u32 {
519            a_user.send(i).await.unwrap();
520            let r = b_user.next().await.unwrap().unwrap();
521            assert_eq!(r, i);
522            tokio::time::sleep(Duration::from_millis(50)).await;
523        }
524
525        drop(a_user);
526        drop(b_user);
527        let reason = tokio::time::timeout(Duration::from_secs(2), task)
528            .await
529            .expect("bridge did not unwind on EOF within 2s")
530            .unwrap();
531        assert!(
532            matches!(
533                reason,
534                BridgeCloseReason::PeerEofLeft | BridgeCloseReason::PeerEofRight
535            ),
536            "expected EOF reason, got {reason}",
537        );
538    }
539
540    #[tokio::test]
541    async fn shutdown_guard_terminates_bridge() {
542        use crate::graceful::Shutdown;
543
544        let (tx, rx) = tokio::sync::oneshot::channel::<()>();
545        let shutdown = Shutdown::new(async move {
546            _ = rx.await;
547        });
548        let guard = shutdown.guard();
549
550        let (_a_user, a_proxy) = duplex_pair::<u32>();
551        let (_b_user, b_proxy) = duplex_pair::<u32>();
552
553        let svc = StreamForwardService::new().with_shutdown_guard(guard);
554        let task = tokio::spawn(async move {
555            svc.serve(StreamBridge::new(a_proxy, b_proxy))
556                .await
557                .unwrap()
558        });
559
560        // Bridge is idle but should not return on its own.
561        tokio::time::sleep(Duration::from_millis(20)).await;
562        assert!(!task.is_finished());
563
564        tx.send(()).unwrap();
565        let reason = tokio::time::timeout(Duration::from_secs(2), task)
566            .await
567            .expect("bridge did not unwind on shutdown within 2s")
568            .unwrap();
569        assert_eq!(reason, BridgeCloseReason::Shutdown);
570        drop(shutdown);
571    }
572
573    #[tokio::test]
574    async fn half_close_keeps_other_direction_alive() {
575        // After `a_user` drops, `a_proxy`'s stream EOFs (PeerEofLeft is
576        // pinned as first_eof) but the bridge must NOT unwind yet — it
577        // should keep pumping `b -> a` direction until `b_user` also
578        // drops. We assert this by:
579        //   1. Drop `a_user` early.
580        //   2. After a short pause, verify the service task hasn't
581        //      finished.
582        //   3. Drop `b_user`.
583        //   4. The bridge unwinds and `first_eof` wins → PeerEofLeft.
584        let (a_user, a_proxy) = duplex_pair::<u32>();
585        let (b_user, b_proxy) = duplex_pair::<u32>();
586
587        let svc = StreamForwardService::new();
588        let task = tokio::spawn(async move {
589            svc.serve(StreamBridge::new(a_proxy, b_proxy))
590                .await
591                .unwrap()
592        });
593
594        drop(a_user);
595        tokio::time::sleep(Duration::from_millis(50)).await;
596        assert!(
597            !task.is_finished(),
598            "bridge unwound before second side closed (a half-close should not be enough)",
599        );
600
601        drop(b_user);
602        let reason = tokio::time::timeout(Duration::from_secs(2), task)
603            .await
604            .expect("bridge did not unwind within 2s")
605            .unwrap();
606        // a_user dropped first → first_eof = PeerEofLeft.
607        assert_eq!(reason, BridgeCloseReason::PeerEofLeft);
608    }
609}