1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum BridgeCloseReason {
36 Shutdown,
38 IdleTimeout,
41 PeerEofLeft,
43 PeerEofRight,
45 ReadErrorLeft,
47 ReadErrorRight,
49 WriteErrorLeft,
51 WriteErrorRight,
53 PeekTimeout,
56 HandlerDeadline,
60 PausedTimeout,
66 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#[derive(Debug)]
151pub struct StreamBridge<A, B> {
152 pub a: A,
154 pub b: B,
156}
157
158impl<A, B> StreamBridge<A, B> {
159 pub fn new(a: A, b: B) -> Self {
161 Self { a, b }
162 }
163}
164
165#[derive(Debug, Clone, Default)]
174pub struct StreamForwardService {
175 idle_timeout: Option<Duration>,
176 shutdown_guard: Option<ShutdownGuard>,
177}
178
179impl StreamForwardService {
180 #[must_use]
183 pub fn new() -> Self {
184 Self::default()
185 }
186
187 generate_set_and_with! {
188 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 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 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 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 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 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 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 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 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 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 assert_eq!(reason, BridgeCloseReason::PeerEofLeft);
608 }
609}