From 64cc30df1214c4062550107d47d29e746c0bc3f6 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Fri, 8 May 2026 19:53:53 +0300 Subject: [PATCH] Schedule retry on startup peer-init failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When initiate_peer_connections() runs at boot, address resolution can fail for an entire peer (no operational transport for the configured transport types, all addresses unreachable, NAT rebind invalidated cached endpoints, etc.). Before this change the failure was logged and silently forgotten — the peer entry stayed in a dead state forever, accepting incoming pings but unable to answer them, until the daemon was manually restarted. The retry plumbing (schedule_retry / process_pending_retries with exponential backoff) already exists and is wired into the post-handshake failure paths (BootstrapEvent::Failed, MMP dead-link timeout, handshake timeout). The startup loop just wasn't calling it. Mirror the BootstrapEvent::Failed path: on a startup peer-init error, parse the peer's npub and call schedule_retry so the peer recovers without operator intervention. Includes a regression test that asserts retry_pending is populated when initiate_peer_connections() fails for a peer with no operational transport. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/node/lifecycle.rs | 6 ++++++ src/node/tests/unit.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index 5b2435f..d4ec4f5 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -73,6 +73,12 @@ impl Node { error = %e, "Failed to initiate peer connection" ); + // Schedule a retry so transient address-resolution failures + // (e.g. cached endpoints stale, NAT rebinds, all addresses + // currently unreachable) recover without a daemon restart. + if let Ok(peer_identity) = PeerIdentity::from_npub(&peer_config.npub) { + self.schedule_retry(*peer_identity.node_addr(), Self::now_ms()); + } } } } diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index 43d5944..03f9558 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -952,6 +952,38 @@ fn test_promote_clears_retry_pending() { ); } +/// Initial peer-init failure at startup must enqueue a retry. Otherwise a peer +/// whose addresses cannot be dialed at boot (no operational transport for the +/// configured transport types, all addresses unreachable, NAT rebind, etc.) +/// stays dead forever — pings arrive but cannot be answered until the daemon +/// is manually restarted. +#[tokio::test] +async fn test_initiate_peer_connections_schedules_retry_on_no_transport() { + let peer_identity = Identity::generate(); + let peer_npub = peer_identity.npub(); + let peer_node_addr = *PeerIdentity::from_npub(&peer_npub).unwrap().node_addr(); + + let mut config = Config::new(); + // udp address but no UDP transport registered on the node — every dial + // attempt resolves to NodeError::NoTransportForType. + config.peers.push(crate::config::PeerConfig::new( + peer_npub, + "udp", + "10.0.0.2:2121", + )); + + let mut node = Node::new(config).unwrap(); + assert!(node.retry_pending.is_empty()); + + node.initiate_peer_connections().await; + + assert!( + node.retry_pending.contains_key(&peer_node_addr), + "startup peer-init failure must enqueue a retry so the peer can recover \ + without a daemon restart" + ); +} + // ============================================================================ // transport_mtu() — ISSUE-2026-0011 regression coverage // ============================================================================