From b2be4c2cb7b68c0010d3076fa0eaa43c88fab2ed Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:15:25 -0400 Subject: [PATCH] Retry transport receive during UDP churn --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/transport.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec94b64..7539687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "1.0.0-rc21" +version = "1.0.0-rc22" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 4e26c23..dd88e34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "1.0.0-rc21" +version = "1.0.0-rc22" edition = "2024" license = "MIT" diff --git a/src/transport.rs b/src/transport.rs index 71e5f42..93916dd 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -19,7 +19,7 @@ use crate::protocol::{ self, CLIENT_TO_SERVER, PacketKind, ReplayWindow, SERVER_TO_CLIENT, StreamClose, StreamData, StreamOpen, StreamOpenOk, StreamOpenReject, StreamWindowAdjust, }; -use crate::udp::is_transient_udp_send_error; +use crate::udp::{is_transient_udp_error, is_transient_udp_send_error}; use anyhow::{Result, anyhow, bail}; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::net::SocketAddr; @@ -852,7 +852,11 @@ impl DoshTransport { pub async fn recv(&mut self) -> Result { let mut buf = vec![0u8; 65535]; loop { - let (n, peer) = self.socket.recv_from(&mut buf).await?; + let (n, peer) = match self.socket.recv_from(&mut buf).await { + Ok(value) => value, + Err(err) if transport_recv_error_is_retryable(&err) => continue, + Err(err) => return Err(err.into()), + }; let packet = match protocol::decode(&buf[..n]) { Ok(packet) => packet, Err(_) => continue, @@ -984,6 +988,10 @@ impl DoshTransport { } } +fn transport_recv_error_is_retryable(err: &std::io::Error) -> bool { + is_transient_udp_error(err) +} + fn initial_stream_id(role: SessionRole) -> u64 { match role { SessionRole::Client => 1, @@ -1033,6 +1041,35 @@ mod tests { assert!(service_target("../bad").is_err()); } + #[test] + fn transport_recv_retries_network_churn_errors() { + for kind in [ + std::io::ErrorKind::Interrupted, + std::io::ErrorKind::TimedOut, + std::io::ErrorKind::WouldBlock, + ] { + let err = std::io::Error::from(kind); + assert!(transport_recv_error_is_retryable(&err)); + } + assert!(!transport_recv_error_is_retryable(&std::io::Error::from( + std::io::ErrorKind::PermissionDenied, + ))); + + #[cfg(unix)] + for code in [libc::ENETUNREACH, libc::EHOSTUNREACH, libc::ECONNRESET] { + assert!(transport_recv_error_is_retryable( + &std::io::Error::from_raw_os_error(code) + )); + } + + #[cfg(windows)] + for code in [10051, 10054, 10065] { + assert!(transport_recv_error_is_retryable( + &std::io::Error::from_raw_os_error(code) + )); + } + } + #[test] fn service_registry_matches_registered_service_opens() { let mut registry = ServiceRegistry::new();