commit 4cff87560ba238ba6eebd16b1465c0ebae2f6ac2 from: murilo ijanc date: Wed Mar 25 19:52:03 2026 UTC Remove stale peers without replacement from routing table When a peer exceeds STALE_THRESHOLD failures and the replacement cache is empty, remove it outright instead of leaving it in the bucket indefinitely. Prevents phantom peer accumulation in small clusters where the cache rarely fills. commit - c4076f54c9e66afb73081fd33b4176ba4407a8a5 commit + 4cff87560ba238ba6eebd16b1465c0ebae2f6ac2 blob - f4c5b2c0a88778c8c2f8ce7ab7cebe831cc5cb4b blob + 574aca329b91f08b7b7a6c58f4b7d42bfb190799 --- src/handlers.rs +++ src/handlers.rs @@ -849,8 +849,7 @@ impl Node { .closest .iter() .find(|p| { - q.queried.contains(&p.id) - && p.id != sender_id + q.queried.contains(&p.id) && p.id != sender_id }) .cloned(); @@ -866,9 +865,7 @@ impl Node { is_unique: false, }; if let Err(e) = self.send_store(&peer, &store_msg) { - log::debug!( - "Republish-on-access failed: {e}" - ); + log::debug!("Republish-on-access failed: {e}"); } else { log::debug!( "Republished value to {:?} (nearest without)", blob - f956f9821695e00eff5516b1ab427b2fb9fa3b29 blob + 1c663d67f6ae60a6f30c58e43e29a63f2ef4f798 --- src/lib.rs +++ src/lib.rs @@ -124,5 +124,5 @@ pub use id::NodeId; pub use nat::NatState; // Re-export sha2 for downstream crates. -pub use sha2; pub use node::Node; +pub use sha2; blob - a9b618d1fe724850b91c421c559f138e0d8c3cf8 blob + dad9bb5c17ac4ada87843841407c567e31aaa5cd --- src/routing.rs +++ src/routing.rs @@ -140,10 +140,8 @@ impl KBucket { /// Add a contact to the replacement cache. fn add_to_cache(&mut self, peer: PeerInfo) { // Update if already in cache - if let Some(pos) = self - .replacements - .iter() - .position(|r| r.id == peer.id) + if let Some(pos) = + self.replacements.iter().position(|r| r.id == peer.id) { self.replacements.remove(pos); self.replacements.push(peer); @@ -368,7 +366,9 @@ impl RoutingTable { /// Record a communication failure for a peer. /// If the peer becomes stale (exceeds threshold), /// tries to replace it with a cached contact. - /// Returns the evicted NodeId if replacement happened. + /// If no replacement is available, removes the stale + /// peer outright to avoid phantom entries. + /// Returns the evicted NodeId if removal happened. pub fn record_failure(&mut self, id: &NodeId) -> Option { // Never mark pinned nodes as stale if self.pinned.contains(id) { @@ -377,7 +377,14 @@ impl RoutingTable { let idx = self.bucket_index(id)?; let became_stale = self.buckets[idx].record_failure(id); if became_stale { - self.buckets[idx].try_replace_stale(id) + // Try replacement cache first; if empty, just + // remove the dead peer so it doesn't linger. + let evicted = self.buckets[idx].try_replace_stale(id); + if evicted.is_none() { + self.remove(id); + return Some(*id); + } + evicted } else { None }