From d1c24bb50e397622706a85855aebae908e54d2fb Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Sat, 4 Apr 2026 17:57:55 +0200 Subject: [PATCH 1/5] perf: tighten network timeouts for faster connection and DHT operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce timeouts based on RTT analysis (300ms worst-case cross-globe RTT). Previous values caused excessive delays when peers were unreachable. Changes: - DIAL_TIMEOUT: 90s → 25s (derived from strategy stage sum + margin) - DEFAULT_CONNECTION_TIMEOUT_SECS: 90s → 25s (matches DIAL_TIMEOUT) - SEND_TIMEOUT: 15s → 10s (3x margin over 4MB at 10Mbps) - REQUEST_TIMEOUT: 30s → 10s (aligned with libp2p/BEP 5 practice) - IDENTITY_EXCHANGE_TIMEOUT: 10s → 5s (1-2 RTTs + margin) - BOOTSTRAP_IDENTITY_TIMEOUT_SECS: 10s → 5s (matches identity exchange) Companion to saorsa-transport timeout tightening. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dht_network_manager.rs | 6 +++--- src/network.rs | 10 +++++----- src/transport/saorsa_transport_adapter.rs | 21 ++++++++++----------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/dht_network_manager.rs b/src/dht_network_manager.rs index 92b5e63..c56de1c 100644 --- a/src/dht_network_manager.rs +++ b/src/dht_network_manager.rs @@ -51,10 +51,10 @@ const MAX_CANDIDATE_NODES: usize = 200; /// Messages larger than this are rejected before deserialization const MAX_MESSAGE_SIZE: usize = 64 * 1024; -/// Request timeout for DHT message handlers (30 seconds) +/// Request timeout for DHT message handlers (10 seconds) /// Prevents long-running handlers from starving the semaphore permit pool /// SEC-001: DoS mitigation via timeout enforcement on concurrent operations -const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); +const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); /// Reliability score assigned to the local node in K-closest results. /// The local node is always considered fully reliable for its own lookups. @@ -62,7 +62,7 @@ const SELF_RELIABILITY_SCORE: f64 = 1.0; /// Maximum time to wait for the identity-exchange handshake after dialling /// a peer. The actual timeout is `min(request_timeout, this)`. -const IDENTITY_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(10); +const IDENTITY_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(5); /// Maximum time to wait for a stale peer's ping response during admission contention. const STALE_REVALIDATION_TIMEOUT: Duration = Duration::from_secs(1); diff --git a/src/network.rs b/src/network.rs index b0151ad..115fc77 100644 --- a/src/network.rs +++ b/src/network.rs @@ -124,16 +124,16 @@ const DEFAULT_MAX_CONNECTIONS: usize = 10_000; /// Default connection timeout in seconds. /// -/// Must accommodate the full NAT traversal flow: direct (5s) → hole-punch -/// (15s) → relay (30s) = ~50s. With 90s we have headroom for retries and -/// slow handshakes. -const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 90; +/// Derived from the sum of connection strategy stages: direct (2s) + +/// 2 × hole-punch rounds (3s + 1s retry each) + relay (10s) = ~20s. +/// 25s provides margin for handshake jitter. +const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 25; /// Number of cached bootstrap peers to retrieve. const BOOTSTRAP_PEER_BATCH_SIZE: usize = 20; /// Timeout in seconds for waiting on a bootstrap peer's identity exchange. -const BOOTSTRAP_IDENTITY_TIMEOUT_SECS: u64 = 10; +const BOOTSTRAP_IDENTITY_TIMEOUT_SECS: u64 = 5; /// Serde helper — returns `true`. const fn default_true() -> bool { diff --git a/src/transport/saorsa_transport_adapter.rs b/src/transport/saorsa_transport_adapter.rs index d8ceb6d..7a39df6 100644 --- a/src/transport/saorsa_transport_adapter.rs +++ b/src/transport/saorsa_transport_adapter.rs @@ -437,11 +437,10 @@ impl P2PNetworkNode { /// Connect to a peer by address pub async fn connect_to_peer(&self, peer_addr: SocketAddr) -> Result { - // The full NAT traversal flow is: direct (5s) → hole-punch (15s) → - // relay (30s). The outer timeout must accommodate the entire flow, - // otherwise the connection attempt is killed mid-hole-punch and the - // NAT'd peer is never reached. - const DIAL_TIMEOUT: Duration = Duration::from_secs(90); + // The full NAT traversal flow is: direct (2s) + 2 × hole-punch + // rounds (3s + 1s retry each) + relay (10s) = ~20s. 25s provides + // margin for handshake jitter. + const DIAL_TIMEOUT: Duration = Duration::from_secs(25); let conn = tokio::time::timeout( DIAL_TIMEOUT, @@ -519,14 +518,14 @@ impl P2PNetworkNode { /// Dials the peer by address, opens a typed unidirectional stream, /// writes the data, and finishes the stream. pub async fn send_to_peer_raw(&self, addr: &SocketAddr, data: &[u8]) -> Result<()> { - // Wrap the entire send path in a 15-second timeout. + // Wrap the entire send path in a 10-second timeout. // // For chunk storage, the client should already have the correct - // address (relay or direct) from the DHT. Direct connections - // complete in <5s, so 15s is generous. A longer timeout (e.g. 60s) - // causes ~60s delays per unreachable peer when the DHT has stale - // NATted addresses, dominating upload time. - const SEND_TIMEOUT: Duration = Duration::from_secs(15); + // address (relay or direct) from the DHT. A 4MB chunk at 10 Mbps + // takes ~3.2s; 10s provides 3x margin for slow-start and jitter. + // Longer timeouts cause excessive delays per unreachable peer when + // the DHT has stale NATted addresses, dominating upload time. + const SEND_TIMEOUT: Duration = Duration::from_secs(10); tokio::time::timeout(SEND_TIMEOUT, async { let conn = self From 0ba19fbafe1eb9f954567286ee8a122f08abf6d3 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Sat, 4 Apr 2026 18:21:38 +0200 Subject: [PATCH 2/5] fix: update test assertion to match new 25s connection timeout default Co-Authored-By: Claude Opus 4.6 (1M context) --- src/network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network.rs b/src/network.rs index 115fc77..434e336 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2271,7 +2271,7 @@ mod tests { assert_eq!(config.listen_addrs().len(), 2); // IPv4 + IPv6 assert_eq!(config.max_connections, 10000); - assert_eq!(config.connection_timeout, Duration::from_secs(90)); + assert_eq!(config.connection_timeout, Duration::from_secs(25)); } #[tokio::test] From d8bda2cc3ae6a690ce520e9b3ae3ab812df925c3 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Sat, 4 Apr 2026 18:23:32 +0200 Subject: [PATCH 3/5] fix: align SEND_TIMEOUT with DIAL_TIMEOUT to cover full NAT traversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_to_peer_raw calls the same dial_addr path as connect_to_peer but had only 10s — less than the relay fallback alone. Bump to 25s to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transport/saorsa_transport_adapter.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/transport/saorsa_transport_adapter.rs b/src/transport/saorsa_transport_adapter.rs index 7a39df6..0c81ae5 100644 --- a/src/transport/saorsa_transport_adapter.rs +++ b/src/transport/saorsa_transport_adapter.rs @@ -518,14 +518,9 @@ impl P2PNetworkNode { /// Dials the peer by address, opens a typed unidirectional stream, /// writes the data, and finishes the stream. pub async fn send_to_peer_raw(&self, addr: &SocketAddr, data: &[u8]) -> Result<()> { - // Wrap the entire send path in a 10-second timeout. - // - // For chunk storage, the client should already have the correct - // address (relay or direct) from the DHT. A 4MB chunk at 10 Mbps - // takes ~3.2s; 10s provides 3x margin for slow-start and jitter. - // Longer timeouts cause excessive delays per unreachable peer when - // the DHT has stale NATted addresses, dominating upload time. - const SEND_TIMEOUT: Duration = Duration::from_secs(10); + // Budget must cover dial (up to ~20s for full NAT traversal cascade) + // plus the data transfer. Matches DIAL_TIMEOUT in connect_to_peer. + const SEND_TIMEOUT: Duration = Duration::from_secs(25); tokio::time::timeout(SEND_TIMEOUT, async { let conn = self From 75baafdac573119a12ac68a04d0ccb8e16f3f78c Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Sat, 4 Apr 2026 18:29:53 +0200 Subject: [PATCH 4/5] perf: reduce DEFAULT_REQUEST_TIMEOUT_SECS from 30s to 15s Aligns with the tighter timeout profile while staying above the relay stage (~10s) so dial_candidate's NAT traversal cascade is not truncated. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dht_network_manager.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dht_network_manager.rs b/src/dht_network_manager.rs index c56de1c..c8d340d 100644 --- a/src/dht_network_manager.rs +++ b/src/dht_network_manager.rs @@ -2536,8 +2536,13 @@ impl DhtNetworkManager { } } -/// Default request timeout for DHT operations (seconds) -const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30; +/// Default request timeout for outbound DHT operations (seconds). +/// +/// Governs `wait_for_response` and the upper bound of `dial_candidate`'s +/// dial timeout (`min(connection_timeout, request_timeout)`). Must stay +/// above the relay stage (~10s) so it never truncates the NAT traversal +/// cascade. +const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 15; /// Default maximum concurrent DHT operations const DEFAULT_MAX_CONCURRENT_OPS: usize = 100; From 183732134d7c2e5e16e3744b6a6889235f444867 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Sat, 4 Apr 2026 17:53:06 +0100 Subject: [PATCH 5/5] build: switch saorsa-transport to rc-2026.4.1 branch Point saorsa-transport dependency at the saorsa-labs Git repo on the rc-2026.4.1 release-candidate branch instead of crates.io. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f650389..120f260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "saorsa-core" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "async-trait", @@ -2481,8 +2481,7 @@ dependencies = [ [[package]] name = "saorsa-transport" version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e33412e61b0cb630410981a629f967a858b1663be8f71b75eadb66c9588ef26" +source = "git+https://github.com/saorsa-labs/saorsa-transport.git?branch=rc-2026.4.1#24bec697562a972bfac01a2b730a3694e5fc5fd7" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 195f469..8664912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ parking_lot = "0.12" once_cell = "1.21" # Networking -saorsa-transport = "0.31.0" +saorsa-transport = { git = "https://github.com/saorsa-labs/saorsa-transport.git", branch = "rc-2026.4.1" } # Core-specific dependencies dirs = "6.0"