1use std::{
5 pin::Pin,
6 sync::{
7 Arc,
8 atomic::{AtomicBool, AtomicUsize, Ordering},
9 },
10 task::{Context, Poll},
11 time::Duration,
12};
13
14use crossfire::mpsc;
15use futures::{
16 AsyncRead, AsyncReadExt, AsyncWrite, FutureExt, Sink, StreamExt,
17 channel::mpsc::{Receiver, Sender, channel},
18};
19use futures_timer::Delay;
20use hopr_api::network::NetworkStreamControl;
21use libp2p::PeerId;
22use tokio_util::{
23 codec::{Decoder, Encoder, FramedRead, FramedWrite},
24 compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt},
25};
26
27#[cfg(all(feature = "telemetry", not(test)))]
28lazy_static::lazy_static! {
29 static ref METRIC_RING_BUFFER_DROPPED: hopr_api::types::telemetry::SimpleCounter =
30 hopr_api::types::telemetry::SimpleCounter::new(
31 "hopr_egress_ring_buffer_dropped",
32 "Number of packets dropped due to per-peer egress channel overflow (drop-newest)",
33 )
34 .unwrap();
35}
36
37#[derive(Clone)]
55struct PeerSink<T: Send + 'static> {
56 tx: crossfire::MAsyncTx<mpsc::Array<T>>,
57 token: Arc<()>,
58 ready: Arc<AtomicBool>,
63}
64
65impl<T: Send + 'static> PeerSink<T> {
66 fn new(tx: crossfire::MAsyncTx<mpsc::Array<T>>) -> Self {
67 Self {
68 tx,
69 token: Arc::new(()),
70 ready: Arc::new(AtomicBool::new(false)),
71 }
72 }
73}
74
75type PeerStreamCache<T> = moka::sync::Cache<PeerId, PeerSink<T>>;
76
77#[derive(Debug)]
79enum EgressWriteError<E> {
80 Sink(E),
82 Stalled { timeout: Duration },
84}
85
86impl<E: std::fmt::Display> std::fmt::Display for EgressWriteError<E> {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 EgressWriteError::Sink(e) => write!(f, "egress sink error: {e}"),
90 EgressWriteError::Stalled { timeout } => {
91 write!(f, "egress write pump made no progress within {timeout:?}")
92 }
93 }
94 }
95}
96
97struct StallGuardSink<S> {
114 inner: S,
115 timeout: Duration,
116 timer: Delay,
120 armed: bool,
124}
125
126impl<S> StallGuardSink<S> {
127 fn new(inner: S, timeout: Duration) -> Self {
128 Self {
129 inner,
130 timeout,
131 timer: Delay::new(timeout),
132 armed: false,
133 }
134 }
135
136 fn poll_guarded<T, E>(
138 &mut self,
139 cx: &mut Context<'_>,
140 poll_inner: impl FnOnce(&mut S, &mut Context<'_>) -> Poll<Result<T, E>>,
141 ) -> Poll<Result<T, EgressWriteError<E>>> {
142 match poll_inner(&mut self.inner, cx) {
143 Poll::Ready(Ok(v)) => {
144 self.armed = false;
145 Poll::Ready(Ok(v))
146 }
147 Poll::Ready(Err(e)) => {
148 self.armed = false;
149 Poll::Ready(Err(EgressWriteError::Sink(e)))
150 }
151 Poll::Pending => {
152 if !self.armed {
153 self.timer.reset(self.timeout);
154 self.armed = true;
155 }
156 match self.timer.poll_unpin(cx) {
157 Poll::Ready(()) => {
158 self.armed = false;
159 Poll::Ready(Err(EgressWriteError::Stalled { timeout: self.timeout }))
160 }
161 Poll::Pending => Poll::Pending,
162 }
163 }
164 }
165 }
166}
167
168impl<S, T> Sink<T> for StallGuardSink<S>
169where
170 S: Sink<T> + Unpin,
171{
172 type Error = EgressWriteError<S::Error>;
173
174 fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
175 self.get_mut().poll_guarded(cx, |s, cx| Pin::new(s).poll_ready(cx))
176 }
177
178 fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
179 Pin::new(&mut self.get_mut().inner)
180 .start_send(item)
181 .map_err(EgressWriteError::Sink)
182 }
183
184 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
185 self.get_mut().poll_guarded(cx, |s, cx| Pin::new(s).poll_flush(cx))
186 }
187
188 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
189 self.get_mut().poll_guarded(cx, |s, cx| Pin::new(s).poll_close(cx))
190 }
191}
192
193#[allow(clippy::too_many_arguments)]
200fn spawn_stream_pumps<S, C>(
201 peer: PeerId,
202 stream: S,
203 rx: crossfire::AsyncRx<mpsc::Array<<C as Decoder>::Item>>,
204 cache: PeerStreamCache<<C as Decoder>::Item>,
205 token: Arc<()>,
206 codec: C,
207 ingress_from_peers: Sender<(PeerId, <C as Decoder>::Item)>,
208 frame_writer_backpressure_bytes: usize,
209 write_stall_timeout: Duration,
210 ready: Arc<AtomicBool>,
211) where
212 S: AsyncRead + AsyncWrite + Send + 'static,
213 C: Encoder<<C as Decoder>::Item> + Decoder + Send + Sync + Clone + 'static,
214 <C as Encoder<<C as Decoder>::Item>>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
215 <C as Decoder>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
216 <C as Decoder>::Item: AsRef<[u8]> + Clone + Send + 'static,
217{
218 let (stream_rx, stream_tx) = stream.split();
219 let cache_for_write = cache.clone();
220 let cache_for_read = cache.clone();
221 let token_write = token.clone();
222
223 let mut frame_writer = FramedWrite::new(stream_tx.compat_write(), codec.clone());
224
225 frame_writer.set_backpressure_boundary(frame_writer_backpressure_bytes);
231
232 let frame_writer = StallGuardSink::new(frame_writer, write_stall_timeout);
236
237 hopr_utils::runtime::prelude::spawn(
239 futures::future::lazy(move |_| {
240 ready.store(true, Ordering::Relaxed);
244 })
245 .then(move |_| rx.into_stream().map(Ok).forward(frame_writer))
246 .inspect(move |res| {
247 tracing::debug!(%peer, ?res, component = "stream", "writing stream with peer finished");
248 })
249 .then(move |_| async move {
250 if cache_for_write
251 .get(&peer)
252 .is_some_and(|s| Arc::ptr_eq(&s.token, &token_write))
253 {
254 cache_for_write.invalidate(&peer);
255 }
256 }),
257 );
258
259 hopr_utils::runtime::prelude::spawn(
261 FramedRead::new(stream_rx.compat(), codec)
262 .filter_map(move |v| {
263 futures::future::ready(match v {
264 Ok(v) => {
265 tracing::trace!(%peer, "read message from peer stream");
266 Some((peer, v))
267 }
268 Err(error) => {
269 tracing::error!(%error, "Error decoding object from the underlying stream");
270 None
271 }
272 })
273 })
274 .map(Ok)
275 .forward(ingress_from_peers)
276 .inspect(move |res| match res {
277 Ok(_) => tracing::debug!(%peer, component = "stream", "incoming stream done reading"),
278 Err(error) => {
279 tracing::error!(%peer, %error, component = "stream", "incoming stream failed on reading")
280 }
281 })
282 .then(move |_| async move {
283 if cache_for_read.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
284 cache_for_read.invalidate(&peer);
285 }
286 }),
287 );
288
289 tracing::trace!(%peer, "created new io for peer");
290}
291
292pub async fn process_stream_protocol<C, V>(
293 codec: C,
294 control: V,
295 stream_cfg: crate::config::StreamProtocolConfig,
296) -> super::errors::Result<(
297 Sender<(PeerId, <C as Decoder>::Item)>, Receiver<(PeerId, <C as Decoder>::Item)>, )>
300where
301 C: Encoder<<C as Decoder>::Item> + Decoder + Send + Sync + Clone + 'static,
302 <C as Encoder<<C as Decoder>::Item>>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
303 <C as Decoder>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
304 <C as Decoder>::Item: AsRef<[u8]> + Clone + Send + Unpin + 'static,
306 V: NetworkStreamControl + Clone + Send + Sync + 'static,
307{
308 let (tx_out, mut rx_out) = channel::<(PeerId, <C as Decoder>::Item)>(100_000);
309 let (tx_in, rx_in) = channel::<(PeerId, <C as Decoder>::Item)>(100_000);
310
311 let cache_out: PeerStreamCache<<C as Decoder>::Item> = moka::sync::Cache::builder()
312 .max_capacity(2000)
313 .eviction_listener(|key: Arc<PeerId>, _, cause| {
314 tracing::trace!(peer = %key.as_ref(), ?cause, "evicting stream for peer");
315 })
316 .build();
317
318 const MAX_CONCURRENT_STREAM_OPENS: usize = 50;
320 let open_task_count = Arc::new(AtomicUsize::new(0));
321
322 let incoming = control
323 .clone()
324 .accept()
325 .map_err(|e| super::errors::ProtocolError::Logic(format!("failed to listen on protocol: {e}")))?;
326
327 let stream_open_timeout = stream_cfg.stream_open_timeout;
328 let frame_writer_backpressure_bytes = stream_cfg.frame_writer_backpressure_bytes;
329 let per_peer_channel_capacity = stream_cfg.per_peer_channel_capacity;
330 let egress_backpressure_timeout = stream_cfg.egress_backpressure_timeout;
331
332 let open_ctx = Arc::new((control, codec, tx_in));
333
334 let cache_ingress = cache_out.clone();
335 let open_ctx_ingress = open_ctx.clone();
336
337 let _ingress_process = hopr_utils::runtime::prelude::spawn(
339 incoming
340 .for_each(move |(peer, stream)| {
341 let cache = cache_ingress.clone();
342 let open_ctx = open_ctx_ingress.clone();
343
344 tracing::debug!(%peer, "received incoming peer-to-peer stream");
345 let (_control, codec, tx_in) = (&open_ctx.0, &open_ctx.1, &open_ctx.2);
346
347 let (tx, rx) = mpsc::bounded_async::<<C as Decoder>::Item>(per_peer_channel_capacity);
348 let sink = PeerSink::new(tx);
349 let token = sink.token.clone();
350 let ready = sink.ready.clone();
351 spawn_stream_pumps(
352 peer,
353 stream,
354 rx,
355 cache.clone(),
356 token,
357 codec.clone(),
358 tx_in.clone(),
359 frame_writer_backpressure_bytes,
360 egress_backpressure_timeout,
361 ready,
362 );
363 cache.insert(peer, sink);
364
365 futures::future::ready(())
366 })
367 .inspect(|_| {
368 tracing::info!(
369 task = "ingress stream processing",
370 "long-running background task finished"
371 )
372 }),
373 );
374
375 let _egress_process = hopr_utils::runtime::prelude::spawn(async move {
387 use futures::StreamExt as _;
388
389 while let Some((peer, msg)) = rx_out.next().await {
390 tracing::trace!(%peer, "trying to deliver message to peer");
391
392 let sink = if let Some(s) = cache_out.get(&peer) {
393 s
394 } else {
395 let cache2 = cache_out.clone();
396 let open_ctx2 = open_ctx.clone();
397 let open_count2 = open_task_count.clone();
398 cache_out.get_with(peer, move || {
399 let (tx, rx) = mpsc::bounded_async::<<C as Decoder>::Item>(per_peer_channel_capacity);
400 let sink = PeerSink::new(tx);
401 let token = sink.token.clone();
402 let ready = sink.ready.clone();
403
404 if open_count2.fetch_add(1, Ordering::Relaxed) < MAX_CONCURRENT_STREAM_OPENS {
405 hopr_utils::runtime::prelude::spawn(async move {
406 tracing::trace!(%peer, "peer is not in cache, opening new stream");
407 use futures_time::future::FutureExt as TimeExt;
408 let (control, codec, tx_in) = (&open_ctx2.0, &open_ctx2.1, &open_ctx2.2);
409
410 let stream = control
411 .clone()
412 .open(peer)
413 .timeout(futures_time::time::Duration::from(stream_open_timeout))
414 .await
415 .map_err(|_| anyhow::anyhow!("timeout trying to open stream to {peer}"))
416 .and_then(|s| {
417 s.map_err(|e| anyhow::anyhow!("could not open outgoing peer-to-peer stream: {e}"))
418 });
419
420 open_count2.fetch_sub(1, Ordering::Relaxed);
421
422 match stream {
423 Ok(stream) => {
424 tracing::debug!(%peer, "opening outgoing peer-to-peer stream");
425 spawn_stream_pumps(
426 peer,
427 stream,
428 rx,
429 cache2.clone(),
430 token,
431 codec.clone(),
432 tx_in.clone(),
433 frame_writer_backpressure_bytes,
434 egress_backpressure_timeout,
435 ready,
436 );
437 }
438 Err(error) => {
439 tracing::debug!(
440 %peer, %error,
441 "stream open failed/timed out; dropping buffered packets"
442 );
443 if cache2.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
444 cache2.invalidate(&peer);
445 }
446 }
447 }
448 });
449 } else {
450 open_count2.fetch_sub(1, Ordering::Relaxed);
451 tracing::debug!(%peer, "stream-open concurrency limit reached; dropping buffered packets");
452 hopr_utils::runtime::prelude::spawn(async move {
453 if cache2.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
454 cache2.invalidate(&peer);
455 }
456 });
457 }
458
459 sink
460 })
461 };
462
463 match sink.tx.try_send(msg) {
464 Ok(()) => tracing::trace!(%peer, "message queued to peer channel"),
465 Err(crossfire::TrySendError::Full(msg)) => {
466 if sink.ready.load(Ordering::Relaxed) {
467 use futures_time::future::FutureExt as _;
474 match async { sink.tx.send(msg).await }
475 .timeout(futures_time::time::Duration::from(egress_backpressure_timeout))
476 .await
477 {
478 Ok(Ok(())) => {
479 tracing::trace!(%peer, "message queued to peer channel after backpressure")
480 }
481 Ok(Err(_disconnected)) => {
482 tracing::debug!(%peer, "peer sink disconnected while awaiting space; invalidating cache");
483 if cache_out.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &sink.token)) {
484 cache_out.invalidate(&peer);
485 }
486 }
487 Err(_timeout) => {
488 #[cfg(all(feature = "telemetry", not(test)))]
489 METRIC_RING_BUFFER_DROPPED.increment();
490 tracing::debug!(
491 %peer,
492 "per-peer egress channel full past backpressure timeout; dropping newest packet"
493 );
494 }
495 }
496 } else {
497 #[cfg(all(feature = "telemetry", not(test)))]
501 METRIC_RING_BUFFER_DROPPED.increment();
502 tracing::debug!(%peer, "per-peer egress channel full during open; dropping newest packet");
503 hopr_utils::runtime::prelude::yield_now().await;
504 }
505 }
506 Err(crossfire::TrySendError::Disconnected(_)) => {
507 tracing::debug!(%peer, "peer sink disconnected; invalidating cache");
512 if cache_out.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &sink.token)) {
513 cache_out.invalidate(&peer);
514 }
515 }
516 }
517 }
518
519 tracing::info!(
520 task = "egress stream processing",
521 "long-running background task finished"
522 );
523 });
524
525 Ok((tx_out, rx_in))
526}
527
528#[cfg(test)]
529mod tests {
530 use std::{
531 pin::Pin,
532 sync::{
533 Arc,
534 atomic::{AtomicUsize, Ordering},
535 },
536 task::{Context as TaskContext, Poll, Waker},
537 };
538
539 use anyhow::Context;
540 use async_trait::async_trait;
541 use futures::{SinkExt, Stream};
542 use parking_lot::Mutex;
543 use tokio_util::{bytes::BytesMut, codec::BytesCodec};
544
545 use super::*;
546
547 #[derive(Clone, Default, Debug)]
548 struct CountingControl {
549 open_calls: Arc<AtomicUsize>,
550 }
551
552 impl CountingControl {
553 fn open_calls(&self) -> usize {
554 self.open_calls.load(Ordering::Relaxed)
555 }
556 }
557
558 #[derive(Default)]
559 struct StalledWriteIo;
560
561 impl AsyncRead for StalledWriteIo {
562 fn poll_read(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>, _buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
563 Poll::Pending
564 }
565 }
566
567 impl AsyncWrite for StalledWriteIo {
568 fn poll_write(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>, _buf: &[u8]) -> Poll<std::io::Result<usize>> {
569 Poll::Pending
570 }
571
572 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
573 Poll::Pending
574 }
575
576 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
577 Poll::Ready(Ok(()))
578 }
579 }
580
581 #[async_trait]
582 impl hopr_api::network::traits::NetworkStreamControl for CountingControl {
583 fn accept(
584 self,
585 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
586 {
587 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, StalledWriteIo)>())
588 }
589
590 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
591 self.open_calls.fetch_add(1, Ordering::Relaxed);
592 Ok::<_, std::io::Error>(StalledWriteIo)
593 }
594 }
595
596 struct AsyncBinaryStreamChannel {
597 read: async_channel_io::ChannelReader,
598 write: async_channel_io::ChannelWriter,
599 }
600
601 impl AsyncBinaryStreamChannel {
602 pub fn new() -> Self {
603 let (write, read) = async_channel_io::pipe();
604 Self { read, write }
605 }
606 }
607
608 impl AsyncRead for AsyncBinaryStreamChannel {
609 fn poll_read(
610 self: std::pin::Pin<&mut Self>,
611 cx: &mut std::task::Context<'_>,
612 buf: &mut [u8],
613 ) -> std::task::Poll<std::io::Result<usize>> {
614 let mut pinned = std::pin::pin!(&mut self.get_mut().read);
615 pinned.as_mut().poll_read(cx, buf)
616 }
617 }
618
619 impl AsyncWrite for AsyncBinaryStreamChannel {
620 fn poll_write(
621 self: std::pin::Pin<&mut Self>,
622 cx: &mut std::task::Context<'_>,
623 buf: &[u8],
624 ) -> std::task::Poll<std::io::Result<usize>> {
625 let mut pinned = std::pin::pin!(&mut self.get_mut().write);
626 pinned.as_mut().poll_write(cx, buf)
627 }
628
629 fn poll_flush(
630 self: std::pin::Pin<&mut Self>,
631 cx: &mut std::task::Context<'_>,
632 ) -> std::task::Poll<std::io::Result<()>> {
633 let pinned = std::pin::pin!(&mut self.get_mut().write);
634 pinned.poll_flush(cx)
635 }
636
637 fn poll_close(
638 self: std::pin::Pin<&mut Self>,
639 cx: &mut std::task::Context<'_>,
640 ) -> std::task::Poll<std::io::Result<()>> {
641 let pinned = std::pin::pin!(&mut self.get_mut().write);
642 pinned.poll_close(cx)
643 }
644 }
645
646 #[tokio::test]
647 async fn split_codec_should_always_produce_correct_data() -> anyhow::Result<()> {
648 let stream = AsyncBinaryStreamChannel::new();
649 let codec = tokio_util::codec::BytesCodec::new();
650
651 let expected = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8];
652 let value = tokio_util::bytes::BytesMut::from(expected.as_ref());
653
654 let (stream_rx, stream_tx) = stream.split();
655 let (mut tx, rx) = (
656 FramedWrite::new(stream_tx.compat_write(), codec),
657 FramedRead::new(stream_rx.compat(), codec),
658 );
659 tx.send(value)
660 .await
661 .map_err(|_| anyhow::anyhow!("should not fail on send"))?;
662
663 futures::pin_mut!(rx);
664
665 assert_eq!(
666 rx.next().await.context("Value must be present")??,
667 tokio_util::bytes::BytesMut::from(expected.as_ref())
668 );
669
670 Ok(())
671 }
672
673 struct DeadSignal {
678 read_dead: std::sync::atomic::AtomicBool,
679 write_dead: std::sync::atomic::AtomicBool,
680 read_waker: Mutex<Option<Waker>>,
681 write_waker: Mutex<Option<Waker>>,
682 }
683
684 impl std::fmt::Debug for DeadSignal {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 f.debug_struct("DeadSignal")
687 .field("read_dead", &self.read_dead.load(Ordering::Relaxed))
688 .field("write_dead", &self.write_dead.load(Ordering::Relaxed))
689 .finish_non_exhaustive()
690 }
691 }
692
693 impl DeadSignal {
694 fn new() -> Arc<Self> {
695 Arc::new(Self {
696 read_dead: std::sync::atomic::AtomicBool::new(false),
697 write_dead: std::sync::atomic::AtomicBool::new(false),
698 read_waker: Mutex::new(None),
699 write_waker: Mutex::new(None),
700 })
701 }
702
703 fn kill_read(self: &Arc<Self>) {
704 self.read_dead.store(true, Ordering::Release);
705 let waker = self.read_waker.lock().take();
706 if let Some(w) = waker {
707 w.wake();
708 }
709 }
710
711 fn kill_write(self: &Arc<Self>) {
712 self.write_dead.store(true, Ordering::Release);
713 let waker = self.write_waker.lock().take();
714 if let Some(w) = waker {
715 w.wake();
716 }
717 }
718 }
719
720 struct FlaggedStream {
721 signal: Arc<DeadSignal>,
722 }
723
724 impl AsyncRead for FlaggedStream {
725 fn poll_read(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, _buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
726 *self.signal.read_waker.lock() = Some(cx.waker().clone());
727 if self.signal.read_dead.load(Ordering::Acquire) {
728 return Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::ConnectionAborted)));
729 }
730 Poll::Pending
731 }
732 }
733
734 impl AsyncWrite for FlaggedStream {
735 fn poll_write(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, _buf: &[u8]) -> Poll<std::io::Result<usize>> {
736 *self.signal.write_waker.lock() = Some(cx.waker().clone());
737 if self.signal.write_dead.load(Ordering::Acquire) {
738 return Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::ConnectionAborted)));
739 }
740 Poll::Pending
741 }
742
743 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
744 Poll::Ready(Ok(()))
745 }
746
747 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
748 Poll::Ready(Ok(()))
749 }
750 }
751
752 #[derive(Clone, Debug, Default)]
753 struct ScriptedControl {
754 open_calls: Arc<AtomicUsize>,
755 signals: Arc<Mutex<Vec<Arc<DeadSignal>>>>,
756 }
757
758 impl ScriptedControl {
759 fn open_calls(&self) -> usize {
760 self.open_calls.load(Ordering::Relaxed)
761 }
762
763 fn signal(&self, index: usize) -> Option<Arc<DeadSignal>> {
764 self.signals.lock().get(index).cloned()
765 }
766 }
767
768 #[async_trait]
769 impl hopr_api::network::traits::NetworkStreamControl for ScriptedControl {
770 fn accept(
771 self,
772 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
773 {
774 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, FlaggedStream)>())
775 }
776
777 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
778 let signal = DeadSignal::new();
779 self.signals.lock().push(signal.clone());
780 self.open_calls.fetch_add(1, Ordering::Relaxed);
781 Ok::<_, std::io::Error>(FlaggedStream { signal })
782 }
783 }
784
785 async fn wait_for(secs: u64, condition: impl Fn() -> bool) -> bool {
786 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(secs);
787 while !condition() && tokio::time::Instant::now() < deadline {
788 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
789 }
790 condition()
791 }
792
793 #[tokio::test]
794 async fn dead_stream_should_be_detected_on_read_path_and_allow_next_send_to_reopen() -> anyhow::Result<()> {
795 let control = ScriptedControl::default();
796
797 let (mut tx_out, _rx_in) = process_stream_protocol(
798 BytesCodec::new(),
799 control.clone(),
800 crate::config::StreamProtocolConfig {
801 per_peer_channel_capacity: 64,
802 ..Default::default()
803 },
804 )
805 .await?;
806
807 let peer = PeerId::random();
808 let msg = BytesMut::from(&b"probe"[..]);
809
810 tx_out
811 .send((peer, msg.clone()))
812 .await
813 .context("first send should succeed")?;
814
815 assert!(
816 wait_for(2, || control.open_calls() >= 1).await,
817 "stream was never opened"
818 );
819 let signal = control.signal(0).context("signal for stream #1 must exist")?;
820
821 signal.kill_read();
822
823 tx_out
824 .send((peer, msg.clone()))
825 .await
826 .context("second send into egress queue should succeed")?;
827
828 assert!(
829 wait_for(2, || control.open_calls() >= 2).await,
830 "stream was not reopened after connection kill (open_calls={})",
831 control.open_calls()
832 );
833
834 Ok(())
835 }
836
837 #[tokio::test]
838 async fn dead_stream_should_be_detected_on_write_path_and_allow_next_send_to_reopen() -> anyhow::Result<()> {
839 let control = ScriptedControl::default();
840
841 let (mut tx_out, _rx_in) = process_stream_protocol(
842 BytesCodec::new(),
843 control.clone(),
844 crate::config::StreamProtocolConfig {
845 per_peer_channel_capacity: 128,
846 ..Default::default()
847 },
848 )
849 .await?;
850
851 let peer = PeerId::random();
852 let msg = BytesMut::from(&b"payload"[..]);
853
854 tx_out.send((peer, msg.clone())).await.context("initial send")?;
855 assert!(wait_for(2, || control.open_calls() >= 1).await, "stream not opened");
856
857 let signal = control.signal(0).context("signal #1 must exist")?;
858
859 signal.kill_write();
860
861 let mut drained = 0usize;
862 while control.open_calls() < 2 && drained < 128 {
863 tx_out
864 .send((peer, msg.clone()))
865 .await
866 .with_context(|| format!("drain send {drained} into egress queue should succeed"))?;
867 drained += 1;
868 }
869
870 assert!(
871 wait_for(3, || control.open_calls() >= 2).await,
872 "stream was not reopened after writer kill (open_calls={})",
873 control.open_calls()
874 );
875
876 Ok(())
877 }
878
879 #[tokio::test]
888 async fn stalled_peer_reopen_cadence_should_be_bounded_by_the_stall_timeout() -> anyhow::Result<()> {
889 const STALL_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
890 const WINDOW: std::time::Duration = std::time::Duration::from_millis(1_500);
891
892 let control = CountingControl::default();
893 let (mut tx_out, _rx_in) = process_stream_protocol(
894 BytesCodec::new(),
895 control.clone(),
896 crate::config::StreamProtocolConfig {
897 per_peer_channel_capacity: 4,
898 frame_writer_backpressure_bytes: 1,
899 egress_backpressure_timeout: STALL_TIMEOUT,
900 ..Default::default()
901 },
902 )
903 .await?;
904
905 let peer = PeerId::random();
906 let msg = BytesMut::from(&b"x"[..]);
907
908 let deadline = tokio::time::Instant::now() + WINDOW;
911 while tokio::time::Instant::now() < deadline {
912 let _ = tx_out.send((peer, msg.clone())).await;
913 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
914 }
915
916 let opens = control.open_calls();
917 assert!(
918 opens >= 2,
919 "a permanently stalled peer must self-heal by reopening (open_calls={opens})"
920 );
921
922 let max_expected = (WINDOW.as_millis() / STALL_TIMEOUT.as_millis()) as usize + 3;
925 assert!(
926 opens <= max_expected,
927 "reopen churn detected: {opens} opens in {WINDOW:?} exceeds the ~{max_expected} bounded by the \
928 {STALL_TIMEOUT:?} stall timeout"
929 );
930
931 Ok(())
932 }
933
934 #[tokio::test]
951 async fn stalled_write_pump_should_terminate_and_reopen_after_stall_timeout() -> anyhow::Result<()> {
952 const STALL_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
953
954 let control = CountingControl::default();
955 let (mut tx_out, _rx_in) = process_stream_protocol(
956 BytesCodec::new(),
957 control.clone(),
958 crate::config::StreamProtocolConfig {
959 per_peer_channel_capacity: 4,
960 frame_writer_backpressure_bytes: 1,
963 egress_backpressure_timeout: STALL_TIMEOUT,
965 ..Default::default()
966 },
967 )
968 .await?;
969
970 let peer = PeerId::random();
971 let msg = BytesMut::from(&b"payload"[..]);
972
973 tx_out.send((peer, msg.clone())).await.context("first send")?;
975 assert!(
976 wait_for(2, || control.open_calls() >= 1).await,
977 "stream was never opened"
978 );
979
980 tokio::time::sleep(STALL_TIMEOUT / 2).await;
982 assert_eq!(
983 control.open_calls(),
984 1,
985 "stream reopened before the stall timeout elapsed — over-eager eviction would kill merely-slow peers"
986 );
987
988 let mut sends = 0;
991 while control.open_calls() < 2 && sends < 200 {
992 let _ = tx_out.send((peer, msg.clone())).await;
993 sends += 1;
994 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
995 }
996
997 assert!(
998 wait_for(3, || control.open_calls() >= 2).await,
999 "stalled write pump never terminated: the stream was not reopened after the stall timeout (open_calls={}) \
1000 — the pump parked forever and the peer became a permanent silent black hole, exactly the field failure \
1001 mode",
1002 control.open_calls(),
1003 );
1004
1005 Ok(())
1006 }
1007
1008 #[derive(Clone, Copy)]
1016 enum Op {
1017 Ready,
1018 Pending,
1019 Err,
1020 }
1021
1022 fn apply(op: Op) -> Poll<Result<(), std::io::Error>> {
1023 match op {
1024 Op::Ready => Poll::Ready(Ok(())),
1025 Op::Pending => Poll::Pending,
1026 Op::Err => Poll::Ready(Err(std::io::Error::other("scripted sink error"))),
1027 }
1028 }
1029
1030 struct ScriptedSink {
1034 ready: Op,
1035 flush: Op,
1036 close: Op,
1037 }
1038
1039 impl Sink<u8> for ScriptedSink {
1040 type Error = std::io::Error;
1041
1042 fn poll_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1043 apply(self.ready)
1044 }
1045
1046 fn start_send(self: Pin<&mut Self>, _item: u8) -> Result<(), Self::Error> {
1047 Ok(())
1048 }
1049
1050 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1051 apply(self.flush)
1052 }
1053
1054 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1055 apply(self.close)
1056 }
1057 }
1058
1059 #[derive(Default)]
1062 struct TransientFlushSink {
1063 flushed_once: bool,
1064 }
1065
1066 impl Sink<u8> for TransientFlushSink {
1067 type Error = std::io::Error;
1068
1069 fn poll_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1070 Poll::Ready(Ok(()))
1071 }
1072
1073 fn start_send(self: Pin<&mut Self>, _item: u8) -> Result<(), Self::Error> {
1074 Ok(())
1075 }
1076
1077 fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1078 let this = self.get_mut();
1079 if this.flushed_once {
1080 Poll::Ready(Ok(()))
1081 } else {
1082 this.flushed_once = true;
1083 cx.waker().wake_by_ref();
1084 Poll::Pending
1085 }
1086 }
1087
1088 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
1089 Poll::Ready(Ok(()))
1090 }
1091 }
1092
1093 #[tokio::test]
1094 async fn stall_guard_sink_should_error_when_poll_ready_stalls_past_timeout() {
1095 let mut sink = StallGuardSink::new(
1096 ScriptedSink {
1097 ready: Op::Pending,
1098 flush: Op::Ready,
1099 close: Op::Ready,
1100 },
1101 Duration::from_millis(150),
1102 );
1103 let res = tokio::time::timeout(Duration::from_secs(2), sink.send(1u8))
1104 .await
1105 .expect("StallGuardSink must resolve on its own timer, not hang");
1106 assert!(
1107 matches!(res, Err(EgressWriteError::Stalled { .. })),
1108 "a sink that never becomes ready must fail with Stalled, got {res:?}"
1109 );
1110 }
1111
1112 #[tokio::test]
1113 async fn stall_guard_sink_should_error_when_poll_flush_stalls_past_timeout() {
1114 let mut sink = StallGuardSink::new(
1115 ScriptedSink {
1116 ready: Op::Ready,
1117 flush: Op::Pending,
1118 close: Op::Ready,
1119 },
1120 Duration::from_millis(150),
1121 );
1122 let res = tokio::time::timeout(Duration::from_secs(2), sink.send(1u8))
1123 .await
1124 .expect("StallGuardSink must resolve on its own timer, not hang");
1125 assert!(
1126 matches!(res, Err(EgressWriteError::Stalled { .. })),
1127 "a sink that accepts but never flushes must fail with Stalled, got {res:?}"
1128 );
1129 }
1130
1131 #[tokio::test]
1132 async fn stall_guard_sink_should_error_when_poll_close_stalls_past_timeout() {
1133 let mut sink = StallGuardSink::new(
1134 ScriptedSink {
1135 ready: Op::Ready,
1136 flush: Op::Ready,
1137 close: Op::Pending,
1138 },
1139 Duration::from_millis(150),
1140 );
1141 let res = tokio::time::timeout(Duration::from_secs(2), sink.close())
1142 .await
1143 .expect("StallGuardSink must resolve on its own timer, not hang");
1144 assert!(
1145 matches!(res, Err(EgressWriteError::Stalled { .. })),
1146 "a sink that never closes must fail with Stalled, got {res:?}"
1147 );
1148 }
1149
1150 #[tokio::test]
1151 async fn stall_guard_sink_should_pass_through_a_healthy_sink_without_error() {
1152 let mut sink = StallGuardSink::new(
1153 ScriptedSink {
1154 ready: Op::Ready,
1155 flush: Op::Ready,
1156 close: Op::Ready,
1157 },
1158 Duration::from_millis(50),
1159 );
1160 for i in 0..100u8 {
1161 sink.send(i)
1162 .await
1163 .expect("a healthy sink must never be failed by the stall guard");
1164 }
1165 sink.close().await.expect("closing a healthy sink must succeed");
1166 }
1167
1168 #[tokio::test]
1169 async fn stall_guard_sink_should_surface_inner_errors_verbatim_not_as_a_stall() {
1170 let mut sink = StallGuardSink::new(
1171 ScriptedSink {
1172 ready: Op::Ready,
1173 flush: Op::Err,
1174 close: Op::Ready,
1175 },
1176 Duration::from_millis(150),
1177 );
1178 let res = sink.send(1u8).await;
1179 assert!(
1180 matches!(res, Err(EgressWriteError::Sink(_))),
1181 "an inner sink error must surface as Sink(_), not be masked as Stalled, got {res:?}"
1182 );
1183 }
1184
1185 #[tokio::test]
1186 async fn stall_guard_sink_should_not_error_on_a_transient_stall_that_recovers_in_time() {
1187 let mut sink = StallGuardSink::new(TransientFlushSink::default(), Duration::from_secs(10));
1190 tokio::time::timeout(Duration::from_secs(2), sink.send(1u8))
1191 .await
1192 .expect("a sink that recovers before the timeout must not hang")
1193 .expect("a sink that recovers before the timeout must not be failed as Stalled");
1194 }
1195
1196 #[derive(Clone, Debug)]
1199 struct HeadOfLineControl {
1200 stalled_peer: PeerId,
1201 stalled_io: GatedWriteIo,
1202 healthy_io: GatedWriteIo,
1203 open_calls: Arc<AtomicUsize>,
1204 stalled_opens: Arc<AtomicUsize>,
1205 }
1206
1207 #[async_trait]
1208 impl hopr_api::network::traits::NetworkStreamControl for HeadOfLineControl {
1209 fn accept(
1210 self,
1211 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
1212 {
1213 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, GatedWriteIo)>())
1214 }
1215
1216 async fn open(self, peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
1217 self.open_calls.fetch_add(1, Ordering::Relaxed);
1218 if peer == self.stalled_peer {
1219 if self.stalled_opens.fetch_add(1, Ordering::Relaxed) == 0 {
1224 Ok::<GatedWriteIo, std::io::Error>(self.stalled_io.clone())
1225 } else {
1226 Err(std::io::Error::other("stalled peer refuses reopen"))
1227 }
1228 } else {
1229 Ok(self.healthy_io.clone())
1230 }
1231 }
1232 }
1233
1234 #[tokio::test]
1243 async fn stalled_peer_must_not_head_of_line_block_a_healthy_peer() -> anyhow::Result<()> {
1244 const STALL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
1245 const OVERFLOW: usize = 8;
1246
1247 let stalled_peer = PeerId::random();
1248 let healthy_peer = PeerId::random();
1249
1250 let stalled_io = GatedWriteIo {
1253 open: Arc::new(std::sync::atomic::AtomicBool::new(true)),
1254 ..Default::default()
1255 };
1256 let healthy_io = GatedWriteIo {
1257 open: Arc::new(std::sync::atomic::AtomicBool::new(true)),
1258 ..Default::default()
1259 };
1260
1261 let open_calls = Arc::new(AtomicUsize::new(0));
1262 let control = HeadOfLineControl {
1263 stalled_peer,
1264 stalled_io: stalled_io.clone(),
1265 healthy_io: healthy_io.clone(),
1266 open_calls: open_calls.clone(),
1267 stalled_opens: Arc::new(AtomicUsize::new(0)),
1268 };
1269
1270 let (mut tx_out, _rx_in) = process_stream_protocol(
1271 BytesCodec::new(),
1272 control,
1273 crate::config::StreamProtocolConfig {
1274 per_peer_channel_capacity: 2,
1275 frame_writer_backpressure_bytes: 1,
1277 egress_backpressure_timeout: STALL_TIMEOUT,
1278 ..Default::default()
1279 },
1280 )
1281 .await?;
1282
1283 let msg = BytesMut::from(&b"hello"[..]);
1284
1285 tx_out
1290 .send((stalled_peer, msg.clone()))
1291 .await
1292 .context("egress queue must accept priming stalled-peer packet")?;
1293 assert!(
1294 wait_for(2, || stalled_io.written() >= msg.len()).await,
1295 "priming packet was never written — stalled peer's stream/pump did not establish"
1296 );
1297 assert_eq!(
1298 open_calls.load(Ordering::Relaxed),
1299 1,
1300 "exactly one open expected during priming"
1301 );
1302
1303 stalled_io.gate_shut();
1306
1307 for _ in 0..OVERFLOW {
1311 tx_out
1312 .send((stalled_peer, msg.clone()))
1313 .await
1314 .context("egress queue must accept stalled-peer packet")?;
1315 }
1316
1317 tx_out
1318 .send((healthy_peer, msg.clone()))
1319 .await
1320 .context("egress queue must accept healthy-peer packet")?;
1321
1322 let delivered = wait_for(3, || healthy_io.written() >= msg.len()).await;
1323
1324 assert_eq!(
1325 stalled_io.written(),
1326 msg.len(),
1327 "only the priming packet should reach the stalled writer; the post-shut burst must not"
1328 );
1329
1330 assert!(
1331 delivered,
1332 "a single stalled peer head-of-line-blocked the shared egress drain: the healthy peer got {} of {} bytes \
1333 within 3s, while {OVERFLOW} overflow packets to the stalled peer each held the loop for {STALL_TIMEOUT:?}",
1334 healthy_io.written(),
1335 msg.len(),
1336 );
1337
1338 Ok(())
1339 }
1340
1341 #[derive(Clone, Debug)]
1342 struct BimodalOpenControl {
1343 slow_peer: PeerId,
1344 open_delay: std::time::Duration,
1345 slow_open_calls: Arc<AtomicUsize>,
1346 fast_open_calls: Arc<AtomicUsize>,
1347 }
1348
1349 impl BimodalOpenControl {
1350 #[allow(dead_code)]
1351 fn slow_open_calls(&self) -> usize {
1352 self.slow_open_calls.load(Ordering::Relaxed)
1353 }
1354
1355 fn fast_open_calls(&self) -> usize {
1356 self.fast_open_calls.load(Ordering::Relaxed)
1357 }
1358 }
1359
1360 #[async_trait]
1361 impl hopr_api::network::traits::NetworkStreamControl for BimodalOpenControl {
1362 fn accept(
1363 self,
1364 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
1365 {
1366 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, StalledWriteIo)>())
1367 }
1368
1369 async fn open(self, peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
1370 if peer == self.slow_peer {
1371 self.slow_open_calls.fetch_add(1, Ordering::Relaxed);
1372 tokio::time::sleep(self.open_delay).await;
1373 return Err::<AsyncBinaryStreamChannel, _>(std::io::Error::other("slow peer cannot connect"));
1374 }
1375 self.fast_open_calls.fetch_add(1, Ordering::Relaxed);
1376 Ok::<_, std::io::Error>(AsyncBinaryStreamChannel::new())
1377 }
1378 }
1379
1380 #[tokio::test]
1381 async fn egress_should_not_hol_block_fast_peer_behind_slow_opens() -> anyhow::Result<()> {
1382 let slow_peer = PeerId::random();
1383 let fast_peer = PeerId::random();
1384
1385 let control = BimodalOpenControl {
1386 slow_peer,
1387 open_delay: std::time::Duration::from_millis(5_000),
1388 slow_open_calls: Default::default(),
1389 fast_open_calls: Default::default(),
1390 };
1391
1392 let (mut tx_out, _rx_in) = process_stream_protocol(
1393 BytesCodec::new(),
1394 control.clone(),
1395 crate::config::StreamProtocolConfig {
1396 stream_open_timeout: std::time::Duration::from_millis(2_000),
1397 ..Default::default()
1398 },
1399 )
1400 .await?;
1401
1402 let msg = BytesMut::from(&b"x"[..]);
1403
1404 for _ in 0..3 {
1405 tx_out
1406 .send((slow_peer, msg.clone()))
1407 .await
1408 .context("egress queue must accept slow-peer packet")?;
1409 }
1410 tx_out
1411 .send((fast_peer, msg.clone()))
1412 .await
1413 .context("egress queue must accept fast-peer packet")?;
1414
1415 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(1_000);
1416 while control.fast_open_calls() < 1 && tokio::time::Instant::now() < deadline {
1417 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1418 }
1419
1420 assert!(
1421 control.fast_open_calls() >= 1,
1422 "fast peer's stream open was not called within 1 s — egress drain is likely head-of-line blocked by \
1423 slow-peer opens"
1424 );
1425
1426 Ok(())
1427 }
1428
1429 #[derive(Clone, Debug)]
1430 struct DelayedControl {
1431 open_delay: std::time::Duration,
1432 open_calls: Arc<AtomicUsize>,
1433 }
1434
1435 #[async_trait]
1436 impl hopr_api::network::traits::NetworkStreamControl for DelayedControl {
1437 fn accept(
1438 self,
1439 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
1440 {
1441 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, AsyncBinaryStreamChannel)>())
1442 }
1443
1444 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
1445 self.open_calls.fetch_add(1, Ordering::Relaxed);
1446 tokio::time::sleep(self.open_delay).await;
1447 Ok::<_, std::io::Error>(AsyncBinaryStreamChannel::new())
1448 }
1449 }
1450
1451 #[tokio::test]
1454 async fn egress_buffers_during_slow_open_then_drains() -> anyhow::Result<()> {
1455 let open_calls = Arc::new(AtomicUsize::new(0));
1456 let control = DelayedControl {
1457 open_delay: std::time::Duration::from_millis(100),
1458 open_calls: open_calls.clone(),
1459 };
1460
1461 let (mut tx_out, mut rx_in) = process_stream_protocol(
1462 BytesCodec::new(),
1463 control,
1464 crate::config::StreamProtocolConfig {
1465 per_peer_channel_capacity: 64,
1466 ..Default::default()
1467 },
1468 )
1469 .await?;
1470
1471 let peer = PeerId::random();
1472 let msg = BytesMut::from(&b"hello"[..]);
1473
1474 let n = 10usize;
1475 let expected_bytes = n * msg.len();
1476 for _ in 0..n {
1477 tx_out
1478 .send((peer, msg.clone()))
1479 .await
1480 .context("send into egress queue should succeed")?;
1481 }
1482
1483 assert!(
1484 wait_for(2, || open_calls.load(Ordering::Relaxed) >= 1).await,
1485 "stream was never opened"
1486 );
1487
1488 let mut received_bytes = 0usize;
1489 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
1490 while received_bytes < expected_bytes && tokio::time::Instant::now() < deadline {
1491 if let Ok(Some((_, bytes))) =
1492 tokio::time::timeout(std::time::Duration::from_millis(100), rx_in.next()).await
1493 {
1494 received_bytes += bytes.len();
1495 }
1496 }
1497
1498 assert!(
1499 received_bytes >= expected_bytes,
1500 "expected at least {expected_bytes} bytes to be delivered after stream open; got {received_bytes}"
1501 );
1502
1503 Ok(())
1504 }
1505
1506 #[derive(Clone, Default, Debug)]
1511 struct GatedWriteIo {
1512 open: Arc<std::sync::atomic::AtomicBool>,
1513 written: Arc<AtomicUsize>,
1514 waker: Arc<Mutex<Option<Waker>>>,
1515 }
1516
1517 impl GatedWriteIo {
1518 fn release(&self) {
1519 self.open.store(true, Ordering::Relaxed);
1520 if let Some(waker) = self.waker.lock().take() {
1521 waker.wake();
1522 }
1523 }
1524
1525 fn gate_shut(&self) {
1527 self.open.store(false, Ordering::Relaxed);
1528 }
1529
1530 fn written(&self) -> usize {
1531 self.written.load(Ordering::Relaxed)
1532 }
1533 }
1534
1535 impl AsyncRead for GatedWriteIo {
1536 fn poll_read(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>, _buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
1537 Poll::Pending
1538 }
1539 }
1540
1541 impl AsyncWrite for GatedWriteIo {
1542 fn poll_write(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
1543 if self.open.load(Ordering::Relaxed) {
1544 self.written.fetch_add(buf.len(), Ordering::Relaxed);
1545 Poll::Ready(Ok(buf.len()))
1546 } else {
1547 *self.waker.lock() = Some(cx.waker().clone());
1548 Poll::Pending
1549 }
1550 }
1551
1552 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
1553 Poll::Ready(Ok(()))
1554 }
1555
1556 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
1557 Poll::Ready(Ok(()))
1558 }
1559 }
1560
1561 #[derive(Clone, Debug)]
1562 struct GatedControl {
1563 io: GatedWriteIo,
1564 open_calls: Arc<AtomicUsize>,
1565 }
1566
1567 #[async_trait]
1568 impl hopr_api::network::traits::NetworkStreamControl for GatedControl {
1569 fn accept(
1570 self,
1571 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
1572 {
1573 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, GatedWriteIo)>())
1574 }
1575
1576 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
1577 self.open_calls.fetch_add(1, Ordering::Relaxed);
1578 Ok::<_, std::io::Error>(self.io.clone())
1579 }
1580 }
1581
1582 #[tokio::test]
1594 async fn egress_backpressures_when_open_channel_full() -> anyhow::Result<()> {
1595 let io = GatedWriteIo::default();
1596 let open_calls = Arc::new(AtomicUsize::new(0));
1597 let control = GatedControl {
1598 io: io.clone(),
1599 open_calls: open_calls.clone(),
1600 };
1601
1602 let (mut tx_out, _rx_in) = process_stream_protocol(
1604 BytesCodec::new(),
1605 control,
1606 crate::config::StreamProtocolConfig {
1607 per_peer_channel_capacity: 4,
1608 frame_writer_backpressure_bytes: 1,
1611 ..Default::default()
1612 },
1613 )
1614 .await?;
1615
1616 let peer = PeerId::random();
1617 let msg = BytesMut::from(&b"hello"[..]);
1618 let n = 50usize;
1619 let expected_bytes = n * msg.len();
1620
1621 for _ in 0..n {
1625 tx_out
1626 .send((peer, msg.clone()))
1627 .await
1628 .context("send into egress queue should succeed")?;
1629 }
1630 assert!(
1631 wait_for(2, || open_calls.load(Ordering::Relaxed) >= 1).await,
1632 "stream was never opened"
1633 );
1634
1635 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1638
1639 io.release();
1642
1643 let min_delivered = expected_bytes - msg.len();
1647 assert!(
1648 wait_for(3, || io.written() >= min_delivered).await,
1649 "essentially all {n} packets ({expected_bytes} bytes) must reach the wire under egress backpressure; a \
1650 regression to drop-newest on a full open channel would lose the overflow (wrote {} bytes, need >= \
1651 {min_delivered})",
1652 io.written()
1653 );
1654
1655 Ok(())
1656 }
1657}