1use std::{
2 convert::Into,
3 fmt::{Debug, Formatter},
4 pin::Pin,
5 sync::Arc,
6 task::{Context, Poll},
7 time::Duration,
8};
9
10use futures::{SinkExt, StreamExt, TryStreamExt};
11use hopr_api::types::{internal::routing::DestinationRouting, primitive::errors::GeneralError};
12use hopr_protocol_app::prelude::{ApplicationData, ApplicationDataIn, ApplicationDataOut, ReservedTag, Tag};
13#[cfg(feature = "telemetry")]
14use hopr_protocol_session::NoopTracker;
15use hopr_protocol_session::{
16 AcknowledgementMode, AcknowledgementState, AcknowledgementStateConfig, ReliableSocket, SessionSocketConfig,
17 UnreliableSocket,
18 flow_control::{DeliveryClock, DeliveryMeter, DeliveryTap, FlowControlConfig},
19};
20use hopr_protocol_start::StartProtocol;
21use hopr_utils::network_types::utils::{AsyncWriteSink, DuplexIO};
22use tracing::{debug, instrument};
23
24use crate::{
25 Capabilities, Capability,
26 balancer::BalancerStateValues,
27 errors::TransportSessionError,
28 flow_control::{PacedWriter, SurbSupply},
29};
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct ByteCapabilities(pub Capabilities);
34
35impl TryFrom<u8> for ByteCapabilities {
36 type Error = GeneralError;
37
38 fn try_from(value: u8) -> Result<Self, Self::Error> {
39 Capabilities::new(value)
40 .map(Self)
41 .map_err(|_| GeneralError::ParseError("capabilities".into()))
42 }
43}
44
45impl From<ByteCapabilities> for u8 {
46 fn from(value: ByteCapabilities) -> Self {
47 *value.0.as_ref()
48 }
49}
50
51impl From<ByteCapabilities> for Capabilities {
52 fn from(value: ByteCapabilities) -> Self {
53 value.0
54 }
55}
56
57impl From<Capabilities> for ByteCapabilities {
58 fn from(value: Capabilities) -> Self {
59 Self(value)
60 }
61}
62
63impl AsRef<Capabilities> for ByteCapabilities {
64 fn as_ref(&self) -> &Capabilities {
65 &self.0
66 }
67}
68
69pub type HoprStartProtocol = StartProtocol<SessionId, SessionTarget, ByteCapabilities>;
71
72pub const SESSION_APPLICATION_TAG: Tag = Tag::Reserved(ReservedTag::Session as u64);
75
76pub use hopr_utils::network_types::types::{ServiceId, SessionId, SessionTarget};
84
85pub(crate) fn caps_to_ack_mode(caps: Capabilities) -> AcknowledgementMode {
86 if caps.contains(Capability::RetransmissionAck | Capability::RetransmissionNack) {
87 AcknowledgementMode::Both
88 } else if caps.contains(Capability::RetransmissionAck) {
89 AcknowledgementMode::Full
90 } else {
91 AcknowledgementMode::Partial
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
97pub enum ClosureReason {
98 WriteClosed,
100 EmptyRead,
102 Eviction,
104}
105
106trait AsyncReadWrite: futures::AsyncWrite + futures::AsyncRead + Send + Unpin {}
108impl<T: futures::AsyncWrite + futures::AsyncRead + Send + Unpin> AsyncReadWrite for T {}
109
110pub type IncomingSession = hopr_api::node::IncomingSession<HoprSession>;
116
117#[derive(Copy, Clone, Debug, PartialEq, Eq, smart_default::SmartDefault, serde::Serialize)]
119pub struct HoprSessionConfig {
120 #[default(Capabilities::empty())]
124 pub capabilities: Capabilities,
125 #[default(1500)]
129 pub frame_mtu: usize,
130 #[default(Duration::from_millis(800))]
134 #[serde(with = "humantime_serde")]
135 pub frame_timeout: Duration,
136 #[default(0)]
141 pub max_buffered_segments: usize,
142 pub max_frames_behind_gap: Option<usize>,
148}
149
150#[pin_project::pin_project]
155pub struct HoprSession {
156 id: SessionId,
157 #[pin]
158 inner: Box<dyn AsyncReadWrite>,
159 routing: DestinationRouting,
160 cfg: HoprSessionConfig,
161 on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
162}
163
164pub(crate) const SESSION_SOCKET_CAPACITY: usize = 16384;
165
166impl HoprSession {
167 #[tracing::instrument(skip_all, fields(id, routing, cfg, session_id = %id))]
175 pub fn new<Tx, Rx>(
176 id: SessionId,
177 routing: DestinationRouting,
178 cfg: HoprSessionConfig,
179 hopr: (Tx, Rx),
180 on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
181 ) -> Result<Self, TransportSessionError>
182 where
183 Tx: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Send + Unpin + 'static,
184 Rx: futures::Stream<Item = ApplicationDataIn> + Send + Unpin + 'static,
185 Tx::Error: std::error::Error + Send + Sync,
186 {
187 Self::new_with_surb_state(id, routing, cfg, hopr, on_close, None, None)
188 }
189
190 #[allow(clippy::too_many_arguments)]
195 #[tracing::instrument(skip_all, fields(id, routing, cfg, session_id = %id))]
196 pub fn new_with_surb_state<Tx, Rx>(
197 id: SessionId,
198 routing: DestinationRouting,
199 cfg: HoprSessionConfig,
200 hopr: (Tx, Rx),
201 on_close: Option<Box<dyn FnOnce(SessionId, ClosureReason) + Send + Sync>>,
202 surb_mgmt: Option<Arc<BalancerStateValues>>,
203 flow_control: Option<FlowControlConfig>,
204 ) -> Result<Self, TransportSessionError>
205 where
206 Tx: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Send + Unpin + 'static,
207 Rx: futures::Stream<Item = ApplicationDataIn> + Send + Unpin + 'static,
208 Tx::Error: std::error::Error + Send + Sync,
209 {
210 let routing_clone = routing.clone();
211
212 #[cfg(feature = "telemetry")]
213 let (session_id_write, session_id_read) = (id, id);
214
215 let transport = DuplexIO(
217 AsyncWriteSink::<{ ApplicationData::PAYLOAD_SIZE }, _>(hopr.0.sink_map_err(std::io::Error::other).with(
218 move |buf: Box<[u8]>| {
219 #[cfg(feature = "telemetry")]
220 crate::telemetry::record_session_write(&session_id_write, buf.len());
221 futures::future::ready(
224 ApplicationData::new(SESSION_APPLICATION_TAG, buf.into_vec())
225 .map(|data| (routing_clone.clone(), ApplicationDataOut::with_no_packet_info(data)))
226 .map_err(std::io::Error::other),
227 )
228 },
229 )),
230 hopr.1
233 .map(move |data| {
234 #[cfg(feature = "telemetry")]
235 crate::telemetry::record_session_read(&session_id_read, data.data.plain_text.len());
236 Ok::<_, std::io::Error>(data.data.plain_text)
237 })
238 .into_async_read(),
239 );
240
241 let inner: Box<dyn AsyncReadWrite> = if cfg.capabilities.contains(Capability::Segmentation) {
243 let socket_cfg = SessionSocketConfig {
244 frame_size: cfg.frame_mtu,
245 frame_timeout: cfg.frame_timeout,
246 capacity: SESSION_SOCKET_CAPACITY,
247 flush_immediately: cfg.capabilities.contains(Capability::NoDelay),
248 datagram: false,
253 max_buffered_segments: cfg.max_buffered_segments,
254 max_frame_age: flow_control.and_then(|c| c.max_frame_age),
257 max_frames_behind_gap: cfg.max_frames_behind_gap,
264 ..Default::default()
265 };
266
267 if cfg.capabilities.contains(Capability::RetransmissionAck)
270 || cfg.capabilities.contains(Capability::RetransmissionNack)
271 {
272 let fc = flow_control;
273
274 let ack_cfg = AcknowledgementStateConfig {
276 expected_packet_latency: Duration::from_millis(200),
281 mode: caps_to_ack_mode(cfg.capabilities),
282 backoff_base: 0.2,
283 max_incoming_frame_retries: 1,
284 max_outgoing_frame_retries: fc.map(|c| c.frame_retries.max(1) as usize).unwrap_or(2),
292 max_frame_age: fc.and_then(|c| c.max_frame_age),
295 ..Default::default()
296 };
297
298 debug!(
299 ?socket_cfg,
300 ?ack_cfg,
301 flow_control = fc.is_some(),
302 "opening new stateful session socket"
303 );
304
305 let (ack_state, flow_control) = match fc {
308 Some(fc_cfg) => {
309 let meter = DeliveryMeter::default();
310 let ack_state = AcknowledgementState::<{ ApplicationData::PAYLOAD_SIZE }>::new(id, ack_cfg)
311 .with_delivery_tap(DeliveryTap::new(meter.clone(), cfg.frame_mtu));
312 let clock = DeliveryClock::new(meter, Some(ack_cfg.expected_packet_latency));
313 (ack_state, Some((fc_cfg, clock)))
314 }
315 None => (
316 AcknowledgementState::<{ ApplicationData::PAYLOAD_SIZE }>::new(id, ack_cfg),
317 None,
318 ),
319 };
320
321 let socket = ReliableSocket::new(
322 transport,
323 ack_state,
324 socket_cfg,
325 #[cfg(feature = "telemetry")]
326 NoopTracker,
327 )?;
328
329 match flow_control {
330 Some((fc_cfg, clock)) => {
331 let surb_state = surb_mgmt
332 .clone()
333 .unwrap_or_else(|| Arc::new(BalancerStateValues::default()));
334 let supply = SurbSupply::new(surb_state, cfg.frame_mtu);
335 debug!(?fc_cfg, "wrapping session socket with paced flow-control writer");
336 Box::new(PacedWriter::new(socket, fc_cfg, clock, supply))
337 }
338 None => Box::new(socket),
339 }
340 } else {
341 let socket_cfg = SessionSocketConfig {
344 datagram: cfg.capabilities.contains(Capability::NoDelay),
345 ..socket_cfg
346 };
347 debug!(?socket_cfg, "opening new stateless session socket");
348
349 Box::new(UnreliableSocket::<{ ApplicationData::PAYLOAD_SIZE }>::new_stateless(
350 id,
351 transport,
352 socket_cfg,
353 #[cfg(feature = "telemetry")]
354 NoopTracker,
355 )?)
356 }
357 } else {
358 debug!("opening raw session socket");
359 Box::new(transport)
360 };
361
362 Ok(Self {
363 id,
364 inner,
365 routing,
366 cfg,
367 on_close,
368 })
369 }
370
371 pub fn id(&self) -> &SessionId {
373 &self.id
374 }
375
376 pub fn routing(&self) -> &DestinationRouting {
378 &self.routing
379 }
380
381 pub fn config(&self) -> &HoprSessionConfig {
383 &self.cfg
384 }
385}
386
387impl std::fmt::Debug for HoprSession {
388 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
389 f.debug_struct("Session")
390 .field("id", &self.id)
391 .field("routing", &self.routing)
392 .finish_non_exhaustive()
393 }
394}
395
396impl futures::AsyncRead for HoprSession {
397 #[instrument(name = "Session::poll_read", level = "trace", skip_all, fields(session_id = %self.id), ret)]
398 fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<std::io::Result<usize>> {
399 let this = self.project();
400 let read = futures::ready!(this.inner.poll_read(cx, buf))?;
401 if read == 0 {
402 tracing::trace!("hopr session empty read");
403 if let Some(notifier) = this.on_close.take() {
405 tracing::trace!("notifying read half closure of session");
406 notifier(*this.id, ClosureReason::EmptyRead);
407 }
408 }
409 Poll::Ready(Ok(read))
410 }
411}
412
413impl futures::AsyncWrite for HoprSession {
414 #[instrument(name = "Session::poll_write", level = "trace", skip_all, fields(session_id = %self.id), ret)]
415 fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
416 self.project().inner.poll_write(cx, buf)
417 }
418
419 #[instrument(name = "Session::poll_flush", level = "trace", skip_all, fields(session_id = %self.id), ret)]
420 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
421 self.project().inner.poll_flush(cx)
422 }
423
424 #[instrument(name = "Session::poll_close", level = "trace", skip_all, fields(session_id = %self.id), ret)]
425 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
426 let this = self.project();
427 futures::ready!(this.inner.poll_close(cx))?;
428 tracing::trace!("hopr session closed");
429
430 #[cfg(feature = "telemetry")]
431 crate::telemetry::set_session_state(this.id, crate::telemetry::SessionLifecycleState::Closing);
432
433 if let Some(notifier) = this.on_close.take() {
434 tracing::trace!("notifying write half closure of session");
435 notifier(*this.id, ClosureReason::WriteClosed);
436 }
437
438 Poll::Ready(Ok(()))
439 }
440}
441
442#[cfg(feature = "runtime-tokio")]
443impl tokio::io::AsyncRead for HoprSession {
444 fn poll_read(
445 mut self: Pin<&mut Self>,
446 cx: &mut Context<'_>,
447 buf: &mut tokio::io::ReadBuf<'_>,
448 ) -> Poll<std::io::Result<()>> {
449 let slice = buf.initialize_unfilled();
450 let n = std::task::ready!(futures::AsyncRead::poll_read(self.as_mut(), cx, slice))?;
451 buf.advance(n);
452 Poll::Ready(Ok(()))
453 }
454}
455
456#[cfg(feature = "runtime-tokio")]
457impl tokio::io::AsyncWrite for HoprSession {
458 fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, std::io::Error>> {
459 futures::AsyncWrite::poll_write(self.as_mut(), cx, buf)
460 }
461
462 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
463 futures::AsyncWrite::poll_flush(self.as_mut(), cx)
464 }
465
466 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
467 futures::AsyncWrite::poll_close(self.as_mut(), cx)
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use anyhow::Context;
474 use futures::{AsyncReadExt, AsyncWriteExt};
475 use hopr_api::types::{
476 crypto::prelude::*,
477 crypto_random::Randomizable,
478 internal::{prelude::HoprPseudonym, routing::RoutingOptions},
479 primitive::prelude::*,
480 };
481 use hopr_utils::network_types::prelude::SealedHost;
482
483 use super::*;
484
485 #[test]
488 fn byte_capabilities_roundtrip_via_u8() -> anyhow::Result<()> {
489 let flags: Capabilities = Capability::Segmentation.into();
490 let caps = ByteCapabilities::from(flags);
491 let byte_val: u8 = caps.into();
492 let restored = ByteCapabilities::try_from(byte_val)?;
493 assert_eq!(caps, restored);
494 Ok(())
495 }
496
497 #[test]
498 fn byte_capabilities_invalid_bits_are_rejected() {
499 assert!(ByteCapabilities::try_from(0xFF_u8).is_err());
501 }
502
503 #[test]
504 fn byte_capabilities_empty_is_zero() {
505 let caps = ByteCapabilities::from(Capabilities::empty());
506 let byte_val: u8 = caps.into();
507 assert_eq!(byte_val, 0);
508 }
509
510 #[test]
511 fn byte_capabilities_combined_flags() -> anyhow::Result<()> {
512 let caps: Capabilities = Capability::Segmentation | Capability::NoRateControl;
513 let byte_caps = ByteCapabilities::from(caps);
514 let byte_val: u8 = byte_caps.into();
515 let restored = ByteCapabilities::try_from(byte_val)?;
516 assert_eq!(*restored.as_ref(), caps);
517 Ok(())
518 }
519
520 #[test]
523 fn caps_to_ack_mode_both_when_ack_and_nack() {
524 let caps: Capabilities = Capability::RetransmissionAck | Capability::RetransmissionNack;
525 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Both);
526 }
527
528 #[test]
529 fn caps_to_ack_mode_full_when_only_ack() {
530 let caps: Capabilities = Capability::RetransmissionAck.into();
531 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Full);
532 }
533
534 #[test]
535 fn caps_to_ack_mode_partial_when_no_retransmission() {
536 let caps: Capabilities = Capability::Segmentation.into();
537 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
538 }
539
540 #[test]
541 fn caps_to_ack_mode_partial_when_empty() {
542 assert_eq!(caps_to_ack_mode(Capabilities::empty()), AcknowledgementMode::Partial);
543 }
544
545 #[test]
546 fn caps_to_ack_mode_should_be_partial_when_only_nack() {
547 let caps: Capabilities = Capability::RetransmissionNack.into();
548 assert_eq!(caps_to_ack_mode(caps), AcknowledgementMode::Partial);
549 }
550
551 #[test]
554 fn closure_reason_display_values_are_stable() {
555 let reasons = [
556 ClosureReason::WriteClosed,
557 ClosureReason::EmptyRead,
558 ClosureReason::Eviction,
559 ];
560 insta::assert_debug_snapshot!(reasons);
561 }
562
563 #[test]
566 fn hopr_session_config_default_snapshot() {
567 let cfg = HoprSessionConfig::default();
568 insta::assert_yaml_snapshot!(cfg);
569 }
570
571 #[test]
574 fn session_target_variants_debug_snapshot() -> anyhow::Result<()> {
575 let targets: Vec<SessionTarget> = vec![
576 SessionTarget::UdpStream(SealedHost::Plain(
577 "127.0.0.1:8080".parse().context("parsing UDP target")?,
578 )),
579 SessionTarget::TcpStream(SealedHost::Plain("10.0.0.1:443".parse().context("parsing TCP target")?)),
580 SessionTarget::ExitNode(42),
581 ];
582 insta::assert_debug_snapshot!(targets);
583 Ok(())
584 }
585
586 #[test]
589 fn session_id_display_and_debug_should_be_identical() {
590 let id = HoprPseudonym::random();
591 assert_eq!(format!("{id}"), format!("{id:?}"));
592 }
593
594 #[test]
595 fn session_id_hash_eq_consistency() {
596 use std::collections::HashSet;
597 let pseudonym = HoprPseudonym::random();
598 let id1: SessionId = pseudonym;
599 let id2: SessionId = pseudonym;
600 let id3: SessionId = HoprPseudonym::random();
601
602 let mut set = HashSet::new();
603 set.insert(id1);
604 assert!(set.contains(&id2));
605 assert!(!set.contains(&id3), "different pseudonym should not be in the set");
606 }
607
608 #[test_log::test(tokio::test)]
611 async fn test_session_bidirectional_flow_without_segmentation() -> anyhow::Result<()> {
612 let dst: Address = (&ChainKeypair::random()).into();
613 let id: SessionId = HoprPseudonym::random();
614 const DATA_LEN: usize = 5000;
615
616 let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
617 let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
618
619 let mut alice_session = HoprSession::new(
620 id,
621 DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
622 Default::default(),
623 (
624 alice_tx,
625 alice_rx
626 .map(|(_, data)| ApplicationDataIn {
627 data: data.data,
628 packet_info: Default::default(),
629 })
630 .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
631 ),
632 None,
633 )?;
634
635 let mut bob_session = HoprSession::new(
636 id,
637 DestinationRouting::Return(id.into()),
638 Default::default(),
639 (
640 bob_tx,
641 bob_rx
642 .map(|(_, data)| ApplicationDataIn {
643 data: data.data,
644 packet_info: Default::default(),
645 })
646 .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
647 ),
648 None,
649 )?;
650
651 let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
652 let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
653
654 let mut bob_recv = [0u8; DATA_LEN];
655 let mut alice_recv = [0u8; DATA_LEN];
656
657 tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
658 .await
659 .context("alice write failed")?
660 .context("alice write timed out")?;
661 alice_session.flush().await?;
662
663 tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
664 .await
665 .context("bob write failed")?
666 .context("bob write timed out")?;
667 bob_session.flush().await?;
668
669 tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
670 .await
671 .context("bob read failed")?
672 .context("bob read timed out")?;
673
674 tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
675 .await
676 .context("alice read failed")?
677 .context("alice read timed out")?;
678
679 assert_eq!(&alice_sent, bob_recv.as_slice());
680 assert_eq!(bob_sent, alice_recv);
681
682 Ok(())
683 }
684
685 #[test_log::test(tokio::test)]
686 async fn test_session_bidirectional_flow_with_segmentation() -> anyhow::Result<()> {
687 let dst: Address = (&ChainKeypair::random()).into();
688 let id: SessionId = HoprPseudonym::random();
689 const DATA_LEN: usize = 5000;
690
691 let (alice_tx, bob_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
692 let (bob_tx, alice_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
693
694 let mut alice_session = HoprSession::new(
695 id,
696 DestinationRouting::forward_only(dst, RoutingOptions::Hops(0.try_into()?)),
697 HoprSessionConfig {
698 capabilities: Capability::Segmentation.into(),
699 ..Default::default()
700 },
701 (
702 alice_tx,
703 alice_rx
704 .map(|(_, data)| ApplicationDataIn {
705 data: data.data,
706 packet_info: Default::default(),
707 })
708 .inspect(|d| debug!("alice rcvd: {}", d.data.total_len())),
709 ),
710 None,
711 )?;
712
713 let mut bob_session = HoprSession::new(
714 id,
715 DestinationRouting::Return(id.into()),
716 HoprSessionConfig {
717 capabilities: Capability::Segmentation.into(),
718 ..Default::default()
719 },
720 (
721 bob_tx,
722 bob_rx
723 .map(|(_, data)| ApplicationDataIn {
724 data: data.data,
725 packet_info: Default::default(),
726 })
727 .inspect(|d| debug!("bob rcvd: {}", d.data.total_len())),
728 ),
729 None,
730 )?;
731
732 let alice_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
733 let bob_sent = hopr_api::types::crypto_random::random_bytes::<DATA_LEN>();
734
735 let mut bob_recv = [0u8; DATA_LEN];
736 let mut alice_recv = [0u8; DATA_LEN];
737
738 tokio::time::timeout(Duration::from_secs(1), alice_session.write_all(&alice_sent))
739 .await
740 .context("alice write failed")?
741 .context("alice write timed out")?;
742 alice_session.flush().await?;
743
744 tokio::time::timeout(Duration::from_secs(1), bob_session.write_all(&bob_sent))
745 .await
746 .context("bob write failed")?
747 .context("bob write timed out")?;
748 bob_session.flush().await?;
749
750 tokio::time::timeout(Duration::from_secs(1), bob_session.read_exact(&mut bob_recv))
751 .await
752 .context("bob read failed")?
753 .context("bob read timed out")?;
754
755 tokio::time::timeout(Duration::from_secs(1), alice_session.read_exact(&mut alice_recv))
756 .await
757 .context("alice read failed")?
758 .context("alice read timed out")?;
759
760 assert_eq!(alice_sent, bob_recv);
761 assert_eq!(bob_sent, alice_recv);
762
763 Ok(())
764 }
765}