1use std::sync::{
5 Arc,
6 atomic::{AtomicUsize, Ordering},
7};
8
9use crossfire::mpsc;
10use futures::{
11 AsyncRead, AsyncReadExt, AsyncWrite, FutureExt, StreamExt,
12 channel::mpsc::{Receiver, Sender, channel},
13};
14use hopr_api::network::NetworkStreamControl;
15use libp2p::PeerId;
16use tokio_util::{
17 codec::{Decoder, Encoder, FramedRead, FramedWrite},
18 compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt},
19};
20
21#[cfg(all(feature = "telemetry", not(test)))]
22lazy_static::lazy_static! {
23 static ref METRIC_RING_BUFFER_DROPPED: hopr_api::types::telemetry::SimpleCounter =
24 hopr_api::types::telemetry::SimpleCounter::new(
25 "hopr_egress_ring_buffer_dropped",
26 "Number of packets dropped due to per-peer egress channel overflow (drop-newest)",
27 )
28 .unwrap();
29}
30
31#[derive(Clone)]
49struct PeerSink<T: Send + 'static> {
50 tx: crossfire::MAsyncTx<mpsc::Array<T>>,
51 token: Arc<()>,
52}
53
54impl<T: Send + 'static> PeerSink<T> {
55 fn new(tx: crossfire::MAsyncTx<mpsc::Array<T>>) -> Self {
56 Self {
57 tx,
58 token: Arc::new(()),
59 }
60 }
61}
62
63type PeerStreamCache<T> = moka::sync::Cache<PeerId, PeerSink<T>>;
64
65#[allow(clippy::too_many_arguments)]
72fn spawn_stream_pumps<S, C>(
73 peer: PeerId,
74 stream: S,
75 rx: crossfire::AsyncRx<mpsc::Array<<C as Decoder>::Item>>,
76 cache: PeerStreamCache<<C as Decoder>::Item>,
77 token: Arc<()>,
78 codec: C,
79 ingress_from_peers: Sender<(PeerId, <C as Decoder>::Item)>,
80 frame_writer_backpressure_bytes: usize,
81) where
82 S: AsyncRead + AsyncWrite + Send + 'static,
83 C: Encoder<<C as Decoder>::Item> + Decoder + Send + Sync + Clone + 'static,
84 <C as Encoder<<C as Decoder>::Item>>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
85 <C as Decoder>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
86 <C as Decoder>::Item: AsRef<[u8]> + Clone + Send + 'static,
87{
88 let (stream_rx, stream_tx) = stream.split();
89 let cache_for_write = cache.clone();
90 let cache_for_read = cache.clone();
91 let token_write = token.clone();
92
93 let mut frame_writer = FramedWrite::new(stream_tx.compat_write(), codec.clone());
94
95 frame_writer.set_backpressure_boundary(frame_writer_backpressure_bytes);
101
102 hopr_utils::runtime::prelude::spawn(
104 rx.into_stream()
105 .map(Ok)
106 .forward(frame_writer)
107 .inspect(move |res| {
108 tracing::debug!(%peer, ?res, component = "stream", "writing stream with peer finished");
109 })
110 .then(move |_| async move {
111 if cache_for_write
112 .get(&peer)
113 .is_some_and(|s| Arc::ptr_eq(&s.token, &token_write))
114 {
115 cache_for_write.invalidate(&peer);
116 }
117 }),
118 );
119
120 hopr_utils::runtime::prelude::spawn(
122 FramedRead::new(stream_rx.compat(), codec)
123 .filter_map(move |v| {
124 futures::future::ready(match v {
125 Ok(v) => {
126 tracing::trace!(%peer, "read message from peer stream");
127 Some((peer, v))
128 }
129 Err(error) => {
130 tracing::error!(%error, "Error decoding object from the underlying stream");
131 None
132 }
133 })
134 })
135 .map(Ok)
136 .forward(ingress_from_peers)
137 .inspect(move |res| match res {
138 Ok(_) => tracing::debug!(%peer, component = "stream", "incoming stream done reading"),
139 Err(error) => {
140 tracing::error!(%peer, %error, component = "stream", "incoming stream failed on reading")
141 }
142 })
143 .then(move |_| async move {
144 if cache_for_read.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
145 cache_for_read.invalidate(&peer);
146 }
147 }),
148 );
149
150 tracing::trace!(%peer, "created new io for peer");
151}
152
153pub async fn process_stream_protocol<C, V>(
154 codec: C,
155 control: V,
156 stream_cfg: crate::config::StreamProtocolConfig,
157) -> super::errors::Result<(
158 Sender<(PeerId, <C as Decoder>::Item)>, Receiver<(PeerId, <C as Decoder>::Item)>, )>
161where
162 C: Encoder<<C as Decoder>::Item> + Decoder + Send + Sync + Clone + 'static,
163 <C as Encoder<<C as Decoder>::Item>>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
164 <C as Decoder>::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
165 <C as Decoder>::Item: AsRef<[u8]> + Clone + Send + 'static,
166 V: NetworkStreamControl + Clone + Send + Sync + 'static,
167{
168 let (tx_out, mut rx_out) = channel::<(PeerId, <C as Decoder>::Item)>(100_000);
169 let (tx_in, rx_in) = channel::<(PeerId, <C as Decoder>::Item)>(100_000);
170
171 let cache_out: PeerStreamCache<<C as Decoder>::Item> = moka::sync::Cache::builder()
172 .max_capacity(2000)
173 .eviction_listener(|key: Arc<PeerId>, _, cause| {
174 tracing::trace!(peer = %key.as_ref(), ?cause, "evicting stream for peer");
175 })
176 .build();
177
178 const MAX_CONCURRENT_STREAM_OPENS: usize = 50;
180 let open_task_count = Arc::new(AtomicUsize::new(0));
181
182 let incoming = control
183 .clone()
184 .accept()
185 .map_err(|e| super::errors::ProtocolError::Logic(format!("failed to listen on protocol: {e}")))?;
186
187 let stream_open_timeout = stream_cfg.stream_open_timeout;
188 let frame_writer_backpressure_bytes = stream_cfg.frame_writer_backpressure_bytes;
189 let per_peer_channel_capacity = stream_cfg.per_peer_channel_capacity;
190
191 let open_ctx = Arc::new((control, codec, tx_in));
192
193 let cache_ingress = cache_out.clone();
194 let open_ctx_ingress = open_ctx.clone();
195
196 let _ingress_process = hopr_utils::runtime::prelude::spawn(
198 incoming
199 .for_each(move |(peer, stream)| {
200 let cache = cache_ingress.clone();
201 let open_ctx = open_ctx_ingress.clone();
202
203 tracing::debug!(%peer, "received incoming peer-to-peer stream");
204 let (_control, codec, tx_in) = (&open_ctx.0, &open_ctx.1, &open_ctx.2);
205
206 let (tx, rx) = mpsc::bounded_async::<<C as Decoder>::Item>(per_peer_channel_capacity);
207 let sink = PeerSink::new(tx);
208 let token = sink.token.clone();
209 spawn_stream_pumps(
210 peer,
211 stream,
212 rx,
213 cache.clone(),
214 token,
215 codec.clone(),
216 tx_in.clone(),
217 frame_writer_backpressure_bytes,
218 );
219 cache.insert(peer, sink);
220
221 futures::future::ready(())
222 })
223 .inspect(|_| {
224 tracing::info!(
225 task = "ingress stream processing",
226 "long-running background task finished"
227 )
228 }),
229 );
230
231 let _egress_process = hopr_utils::runtime::prelude::spawn(async move {
243 use futures::StreamExt as _;
244
245 while let Some((peer, msg)) = rx_out.next().await {
246 tracing::trace!(%peer, "trying to deliver message to peer");
247
248 let sink = if let Some(s) = cache_out.get(&peer) {
249 s
250 } else {
251 let cache2 = cache_out.clone();
252 let open_ctx2 = open_ctx.clone();
253 let open_count2 = open_task_count.clone();
254 cache_out.get_with(peer, move || {
255 let (tx, rx) = mpsc::bounded_async::<<C as Decoder>::Item>(per_peer_channel_capacity);
256 let sink = PeerSink::new(tx);
257 let token = sink.token.clone();
258
259 if open_count2.fetch_add(1, Ordering::Relaxed) < MAX_CONCURRENT_STREAM_OPENS {
260 hopr_utils::runtime::prelude::spawn(async move {
261 tracing::trace!(%peer, "peer is not in cache, opening new stream");
262 use futures_time::future::FutureExt as TimeExt;
263 let (control, codec, tx_in) = (&open_ctx2.0, &open_ctx2.1, &open_ctx2.2);
264
265 let stream = control
266 .clone()
267 .open(peer)
268 .timeout(futures_time::time::Duration::from(stream_open_timeout))
269 .await
270 .map_err(|_| anyhow::anyhow!("timeout trying to open stream to {peer}"))
271 .and_then(|s| {
272 s.map_err(|e| anyhow::anyhow!("could not open outgoing peer-to-peer stream: {e}"))
273 });
274
275 open_count2.fetch_sub(1, Ordering::Relaxed);
276
277 match stream {
278 Ok(stream) => {
279 tracing::debug!(%peer, "opening outgoing peer-to-peer stream");
280 spawn_stream_pumps(
281 peer,
282 stream,
283 rx,
284 cache2.clone(),
285 token,
286 codec.clone(),
287 tx_in.clone(),
288 frame_writer_backpressure_bytes,
289 );
290 }
291 Err(error) => {
292 tracing::debug!(
293 %peer, %error,
294 "stream open failed/timed out; dropping buffered packets"
295 );
296 if cache2.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
297 cache2.invalidate(&peer);
298 }
299 }
300 }
301 });
302 } else {
303 open_count2.fetch_sub(1, Ordering::Relaxed);
304 tracing::debug!(%peer, "stream-open concurrency limit reached; dropping buffered packets");
305 hopr_utils::runtime::prelude::spawn(async move {
306 if cache2.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &token)) {
307 cache2.invalidate(&peer);
308 }
309 });
310 }
311
312 sink
313 })
314 };
315
316 match sink.tx.try_send(msg) {
317 Ok(()) => tracing::trace!(%peer, "message queued to peer channel"),
318 Err(crossfire::TrySendError::Full(_msg)) => {
319 #[cfg(all(feature = "telemetry", not(test)))]
322 METRIC_RING_BUFFER_DROPPED.increment();
323 tracing::debug!(%peer, "per-peer egress channel full; dropping newest packet");
324 hopr_utils::runtime::prelude::yield_now().await;
325 }
326 Err(crossfire::TrySendError::Disconnected(_)) => {
327 tracing::debug!(%peer, "peer sink disconnected; invalidating cache");
332 if cache_out.get(&peer).is_some_and(|s| Arc::ptr_eq(&s.token, &sink.token)) {
333 cache_out.invalidate(&peer);
334 }
335 }
336 }
337 }
338
339 tracing::info!(
340 task = "egress stream processing",
341 "long-running background task finished"
342 );
343 });
344
345 Ok((tx_out, rx_in))
346}
347
348#[cfg(test)]
349mod tests {
350 use std::{
351 pin::Pin,
352 sync::{
353 Arc,
354 atomic::{AtomicUsize, Ordering},
355 },
356 task::{Context as TaskContext, Poll, Waker},
357 };
358
359 use anyhow::Context;
360 use async_trait::async_trait;
361 use futures::{SinkExt, Stream};
362 use parking_lot::Mutex;
363 use tokio_util::{bytes::BytesMut, codec::BytesCodec};
364
365 use super::*;
366
367 #[derive(Clone, Default, Debug)]
368 struct CountingControl {
369 open_calls: Arc<AtomicUsize>,
370 }
371
372 impl CountingControl {
373 fn open_calls(&self) -> usize {
374 self.open_calls.load(Ordering::Relaxed)
375 }
376 }
377
378 #[derive(Default)]
379 struct StalledWriteIo;
380
381 impl AsyncRead for StalledWriteIo {
382 fn poll_read(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>, _buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
383 Poll::Pending
384 }
385 }
386
387 impl AsyncWrite for StalledWriteIo {
388 fn poll_write(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>, _buf: &[u8]) -> Poll<std::io::Result<usize>> {
389 Poll::Pending
390 }
391
392 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
393 Poll::Pending
394 }
395
396 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
397 Poll::Ready(Ok(()))
398 }
399 }
400
401 #[async_trait]
402 impl hopr_api::network::traits::NetworkStreamControl for CountingControl {
403 fn accept(
404 self,
405 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
406 {
407 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, StalledWriteIo)>())
408 }
409
410 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
411 self.open_calls.fetch_add(1, Ordering::Relaxed);
412 Ok::<_, std::io::Error>(StalledWriteIo)
413 }
414 }
415
416 struct AsyncBinaryStreamChannel {
417 read: async_channel_io::ChannelReader,
418 write: async_channel_io::ChannelWriter,
419 }
420
421 impl AsyncBinaryStreamChannel {
422 pub fn new() -> Self {
423 let (write, read) = async_channel_io::pipe();
424 Self { read, write }
425 }
426 }
427
428 impl AsyncRead for AsyncBinaryStreamChannel {
429 fn poll_read(
430 self: std::pin::Pin<&mut Self>,
431 cx: &mut std::task::Context<'_>,
432 buf: &mut [u8],
433 ) -> std::task::Poll<std::io::Result<usize>> {
434 let mut pinned = std::pin::pin!(&mut self.get_mut().read);
435 pinned.as_mut().poll_read(cx, buf)
436 }
437 }
438
439 impl AsyncWrite for AsyncBinaryStreamChannel {
440 fn poll_write(
441 self: std::pin::Pin<&mut Self>,
442 cx: &mut std::task::Context<'_>,
443 buf: &[u8],
444 ) -> std::task::Poll<std::io::Result<usize>> {
445 let mut pinned = std::pin::pin!(&mut self.get_mut().write);
446 pinned.as_mut().poll_write(cx, buf)
447 }
448
449 fn poll_flush(
450 self: std::pin::Pin<&mut Self>,
451 cx: &mut std::task::Context<'_>,
452 ) -> std::task::Poll<std::io::Result<()>> {
453 let pinned = std::pin::pin!(&mut self.get_mut().write);
454 pinned.poll_flush(cx)
455 }
456
457 fn poll_close(
458 self: std::pin::Pin<&mut Self>,
459 cx: &mut std::task::Context<'_>,
460 ) -> std::task::Poll<std::io::Result<()>> {
461 let pinned = std::pin::pin!(&mut self.get_mut().write);
462 pinned.poll_close(cx)
463 }
464 }
465
466 #[tokio::test]
467 async fn split_codec_should_always_produce_correct_data() -> anyhow::Result<()> {
468 let stream = AsyncBinaryStreamChannel::new();
469 let codec = tokio_util::codec::BytesCodec::new();
470
471 let expected = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8];
472 let value = tokio_util::bytes::BytesMut::from(expected.as_ref());
473
474 let (stream_rx, stream_tx) = stream.split();
475 let (mut tx, rx) = (
476 FramedWrite::new(stream_tx.compat_write(), codec),
477 FramedRead::new(stream_rx.compat(), codec),
478 );
479 tx.send(value)
480 .await
481 .map_err(|_| anyhow::anyhow!("should not fail on send"))?;
482
483 futures::pin_mut!(rx);
484
485 assert_eq!(
486 rx.next().await.context("Value must be present")??,
487 tokio_util::bytes::BytesMut::from(expected.as_ref())
488 );
489
490 Ok(())
491 }
492
493 struct DeadSignal {
498 read_dead: std::sync::atomic::AtomicBool,
499 write_dead: std::sync::atomic::AtomicBool,
500 read_waker: Mutex<Option<Waker>>,
501 write_waker: Mutex<Option<Waker>>,
502 }
503
504 impl std::fmt::Debug for DeadSignal {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 f.debug_struct("DeadSignal")
507 .field("read_dead", &self.read_dead.load(Ordering::Relaxed))
508 .field("write_dead", &self.write_dead.load(Ordering::Relaxed))
509 .finish_non_exhaustive()
510 }
511 }
512
513 impl DeadSignal {
514 fn new() -> Arc<Self> {
515 Arc::new(Self {
516 read_dead: std::sync::atomic::AtomicBool::new(false),
517 write_dead: std::sync::atomic::AtomicBool::new(false),
518 read_waker: Mutex::new(None),
519 write_waker: Mutex::new(None),
520 })
521 }
522
523 fn kill_read(self: &Arc<Self>) {
524 self.read_dead.store(true, Ordering::Release);
525 let waker = self.read_waker.lock().take();
526 if let Some(w) = waker {
527 w.wake();
528 }
529 }
530
531 fn kill_write(self: &Arc<Self>) {
532 self.write_dead.store(true, Ordering::Release);
533 let waker = self.write_waker.lock().take();
534 if let Some(w) = waker {
535 w.wake();
536 }
537 }
538 }
539
540 struct FlaggedStream {
541 signal: Arc<DeadSignal>,
542 }
543
544 impl AsyncRead for FlaggedStream {
545 fn poll_read(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, _buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
546 *self.signal.read_waker.lock() = Some(cx.waker().clone());
547 if self.signal.read_dead.load(Ordering::Acquire) {
548 return Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::ConnectionAborted)));
549 }
550 Poll::Pending
551 }
552 }
553
554 impl AsyncWrite for FlaggedStream {
555 fn poll_write(self: Pin<&mut Self>, cx: &mut TaskContext<'_>, _buf: &[u8]) -> Poll<std::io::Result<usize>> {
556 *self.signal.write_waker.lock() = Some(cx.waker().clone());
557 if self.signal.write_dead.load(Ordering::Acquire) {
558 return Poll::Ready(Err(std::io::Error::from(std::io::ErrorKind::ConnectionAborted)));
559 }
560 Poll::Pending
561 }
562
563 fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
564 Poll::Ready(Ok(()))
565 }
566
567 fn poll_close(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
568 Poll::Ready(Ok(()))
569 }
570 }
571
572 #[derive(Clone, Debug, Default)]
573 struct ScriptedControl {
574 open_calls: Arc<AtomicUsize>,
575 signals: Arc<Mutex<Vec<Arc<DeadSignal>>>>,
576 }
577
578 impl ScriptedControl {
579 fn open_calls(&self) -> usize {
580 self.open_calls.load(Ordering::Relaxed)
581 }
582
583 fn signal(&self, index: usize) -> Option<Arc<DeadSignal>> {
584 self.signals.lock().get(index).cloned()
585 }
586 }
587
588 #[async_trait]
589 impl hopr_api::network::traits::NetworkStreamControl for ScriptedControl {
590 fn accept(
591 self,
592 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
593 {
594 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, FlaggedStream)>())
595 }
596
597 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
598 let signal = DeadSignal::new();
599 self.signals.lock().push(signal.clone());
600 self.open_calls.fetch_add(1, Ordering::Relaxed);
601 Ok::<_, std::io::Error>(FlaggedStream { signal })
602 }
603 }
604
605 async fn wait_for(secs: u64, condition: impl Fn() -> bool) -> bool {
606 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(secs);
607 while !condition() && tokio::time::Instant::now() < deadline {
608 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
609 }
610 condition()
611 }
612
613 #[tokio::test]
614 async fn dead_stream_should_be_detected_on_read_path_and_allow_next_send_to_reopen() -> anyhow::Result<()> {
615 let control = ScriptedControl::default();
616
617 let (mut tx_out, _rx_in) = process_stream_protocol(
618 BytesCodec::new(),
619 control.clone(),
620 crate::config::StreamProtocolConfig {
621 per_peer_channel_capacity: 64,
622 ..Default::default()
623 },
624 )
625 .await?;
626
627 let peer = PeerId::random();
628 let msg = BytesMut::from(&b"probe"[..]);
629
630 tx_out
631 .send((peer, msg.clone()))
632 .await
633 .context("first send should succeed")?;
634
635 assert!(
636 wait_for(2, || control.open_calls() >= 1).await,
637 "stream was never opened"
638 );
639 let signal = control.signal(0).context("signal for stream #1 must exist")?;
640
641 signal.kill_read();
642
643 tx_out
644 .send((peer, msg.clone()))
645 .await
646 .context("second send into egress queue should succeed")?;
647
648 assert!(
649 wait_for(2, || control.open_calls() >= 2).await,
650 "stream was not reopened after connection kill (open_calls={})",
651 control.open_calls()
652 );
653
654 Ok(())
655 }
656
657 #[tokio::test]
658 async fn dead_stream_should_be_detected_on_write_path_and_allow_next_send_to_reopen() -> anyhow::Result<()> {
659 let control = ScriptedControl::default();
660
661 let (mut tx_out, _rx_in) = process_stream_protocol(
662 BytesCodec::new(),
663 control.clone(),
664 crate::config::StreamProtocolConfig {
665 per_peer_channel_capacity: 128,
666 ..Default::default()
667 },
668 )
669 .await?;
670
671 let peer = PeerId::random();
672 let msg = BytesMut::from(&b"payload"[..]);
673
674 tx_out.send((peer, msg.clone())).await.context("initial send")?;
675 assert!(wait_for(2, || control.open_calls() >= 1).await, "stream not opened");
676
677 let signal = control.signal(0).context("signal #1 must exist")?;
678
679 signal.kill_write();
680
681 let mut drained = 0usize;
682 while control.open_calls() < 2 && drained < 128 {
683 tx_out
684 .send((peer, msg.clone()))
685 .await
686 .with_context(|| format!("drain send {drained} into egress queue should succeed"))?;
687 drained += 1;
688 }
689
690 assert!(
691 wait_for(3, || control.open_calls() >= 2).await,
692 "stream was not reopened after writer kill (open_calls={})",
693 control.open_calls()
694 );
695
696 Ok(())
697 }
698
699 #[tokio::test]
707 async fn per_peer_stream_should_not_reopen_pathologically_on_send_failures() -> anyhow::Result<()> {
708 let control = CountingControl::default();
709 let (mut tx_out, _rx_in) = process_stream_protocol(
710 BytesCodec::new(),
711 control.clone(),
712 crate::config::StreamProtocolConfig {
713 per_peer_channel_capacity: 16,
714 ..Default::default()
715 },
716 )
717 .await?;
718
719 let peer = PeerId::random();
720 let msg = BytesMut::from(&b"x"[..]);
721
722 for _ in 0..1200 {
723 tx_out
724 .send((peer, msg.clone()))
725 .await
726 .context("egress queue should accept test packet")?;
727 }
728
729 let open_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
731 while control.open_calls() < 1 && tokio::time::Instant::now() < open_deadline {
732 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
733 }
734 assert!(
735 control.open_calls() >= 1,
736 "stream was never opened — egress task did not process any packet"
737 );
738
739 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
741 while control.open_calls() < 2 && tokio::time::Instant::now() < deadline {
742 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
743 }
744
745 assert!(
746 control.open_calls() <= 1,
747 "pathological reopen churn detected for same peer under send failures (open_calls={})",
748 control.open_calls()
749 );
750
751 Ok(())
752 }
753
754 #[derive(Clone, Debug)]
755 struct BimodalOpenControl {
756 slow_peer: PeerId,
757 open_delay: std::time::Duration,
758 slow_open_calls: Arc<AtomicUsize>,
759 fast_open_calls: Arc<AtomicUsize>,
760 }
761
762 impl BimodalOpenControl {
763 #[allow(dead_code)]
764 fn slow_open_calls(&self) -> usize {
765 self.slow_open_calls.load(Ordering::Relaxed)
766 }
767
768 fn fast_open_calls(&self) -> usize {
769 self.fast_open_calls.load(Ordering::Relaxed)
770 }
771 }
772
773 #[async_trait]
774 impl hopr_api::network::traits::NetworkStreamControl for BimodalOpenControl {
775 fn accept(
776 self,
777 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
778 {
779 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, StalledWriteIo)>())
780 }
781
782 async fn open(self, peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
783 if peer == self.slow_peer {
784 self.slow_open_calls.fetch_add(1, Ordering::Relaxed);
785 tokio::time::sleep(self.open_delay).await;
786 return Err::<AsyncBinaryStreamChannel, _>(std::io::Error::other("slow peer cannot connect"));
787 }
788 self.fast_open_calls.fetch_add(1, Ordering::Relaxed);
789 Ok::<_, std::io::Error>(AsyncBinaryStreamChannel::new())
790 }
791 }
792
793 #[tokio::test]
794 async fn egress_should_not_hol_block_fast_peer_behind_slow_opens() -> anyhow::Result<()> {
795 let slow_peer = PeerId::random();
796 let fast_peer = PeerId::random();
797
798 let control = BimodalOpenControl {
799 slow_peer,
800 open_delay: std::time::Duration::from_millis(5_000),
801 slow_open_calls: Default::default(),
802 fast_open_calls: Default::default(),
803 };
804
805 let (mut tx_out, _rx_in) = process_stream_protocol(
806 BytesCodec::new(),
807 control.clone(),
808 crate::config::StreamProtocolConfig {
809 stream_open_timeout: std::time::Duration::from_millis(2_000),
810 ..Default::default()
811 },
812 )
813 .await?;
814
815 let msg = BytesMut::from(&b"x"[..]);
816
817 for _ in 0..3 {
818 tx_out
819 .send((slow_peer, msg.clone()))
820 .await
821 .context("egress queue must accept slow-peer packet")?;
822 }
823 tx_out
824 .send((fast_peer, msg.clone()))
825 .await
826 .context("egress queue must accept fast-peer packet")?;
827
828 let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(1_000);
829 while control.fast_open_calls() < 1 && tokio::time::Instant::now() < deadline {
830 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
831 }
832
833 assert!(
834 control.fast_open_calls() >= 1,
835 "fast peer's stream open was not called within 1 s — egress drain is likely head-of-line blocked by \
836 slow-peer opens"
837 );
838
839 Ok(())
840 }
841
842 #[derive(Clone, Debug)]
843 struct DelayedControl {
844 open_delay: std::time::Duration,
845 open_calls: Arc<AtomicUsize>,
846 }
847
848 #[async_trait]
849 impl hopr_api::network::traits::NetworkStreamControl for DelayedControl {
850 fn accept(
851 self,
852 ) -> Result<impl Stream<Item = (PeerId, impl AsyncRead + AsyncWrite + Send)> + Send, impl std::error::Error>
853 {
854 Ok::<_, std::io::Error>(futures::stream::empty::<(PeerId, AsyncBinaryStreamChannel)>())
855 }
856
857 async fn open(self, _peer: PeerId) -> Result<impl AsyncRead + AsyncWrite + Send, impl std::error::Error> {
858 self.open_calls.fetch_add(1, Ordering::Relaxed);
859 tokio::time::sleep(self.open_delay).await;
860 Ok::<_, std::io::Error>(AsyncBinaryStreamChannel::new())
861 }
862 }
863
864 #[tokio::test]
867 async fn egress_buffers_during_slow_open_then_drains() -> anyhow::Result<()> {
868 let open_calls = Arc::new(AtomicUsize::new(0));
869 let control = DelayedControl {
870 open_delay: std::time::Duration::from_millis(100),
871 open_calls: open_calls.clone(),
872 };
873
874 let (mut tx_out, mut rx_in) = process_stream_protocol(
875 BytesCodec::new(),
876 control,
877 crate::config::StreamProtocolConfig {
878 per_peer_channel_capacity: 64,
879 ..Default::default()
880 },
881 )
882 .await?;
883
884 let peer = PeerId::random();
885 let msg = BytesMut::from(&b"hello"[..]);
886
887 let n = 10usize;
888 let expected_bytes = n * msg.len();
889 for _ in 0..n {
890 tx_out
891 .send((peer, msg.clone()))
892 .await
893 .context("send into egress queue should succeed")?;
894 }
895
896 assert!(
897 wait_for(2, || open_calls.load(Ordering::Relaxed) >= 1).await,
898 "stream was never opened"
899 );
900
901 let mut received_bytes = 0usize;
902 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
903 while received_bytes < expected_bytes && tokio::time::Instant::now() < deadline {
904 if let Ok(Some((_, bytes))) =
905 tokio::time::timeout(std::time::Duration::from_millis(100), rx_in.next()).await
906 {
907 received_bytes += bytes.len();
908 }
909 }
910
911 assert!(
912 received_bytes >= expected_bytes,
913 "expected at least {expected_bytes} bytes to be delivered after stream open; got {received_bytes}"
914 );
915
916 Ok(())
917 }
918}