diff --git a/src/cache/coord_cache.rs b/src/cache/coord_cache.rs index 84fda86..a0c914a 100644 --- a/src/cache/coord_cache.rs +++ b/src/cache/coord_cache.rs @@ -205,6 +205,40 @@ impl CoordCache { self.entries.clear(); } + /// Drop entries whose cached destination ancestry contains the given + /// `NodeAddr`. + /// + /// Used at parent-position-change sites: when our own position in the + /// tree changes, destinations downstream of us (whose cached coordinates + /// embed our previous prefix) have stale path information and must be + /// re-learned. Entries whose ancestry does not include `node_addr` are + /// unaffected by the local position change and are retained. + /// + /// Returns the count of entries removed. + pub fn invalidate_via_node(&mut self, node_addr: &NodeAddr) -> usize { + let len_before = self.entries.len(); + self.entries + .retain(|_, entry| !entry.coords().contains(node_addr)); + len_before - self.entries.len() + } + + /// Drop entries whose cached destination `root_id` differs from + /// `current_root`. + /// + /// Used at root-change sites (become_root, root handover via + /// TreeAnnounce). `find_next_hop` returns `None` for any destination + /// whose root does not match the local root, so entries from a stale + /// root cannot route and would otherwise occupy cache slots until + /// TTL expiry. + /// + /// Returns the count of entries removed. + pub fn invalidate_other_roots(&mut self, current_root: &NodeAddr) -> usize { + let len_before = self.entries.len(); + self.entries + .retain(|_, entry| entry.coords().root_id() == current_root); + len_before - self.entries.len() + } + /// Evict one entry (expired first, then LRU). fn evict_one(&mut self, current_time_ms: u64) { // First try to evict an expired entry @@ -507,4 +541,101 @@ mod tests { assert_eq!(stats.expired, 0); assert_eq!(stats.avg_age_ms, 0); } + + // ===== Surgical invalidation tests ===== + + #[test] + fn test_invalidate_via_node_at_self_depth() { + // Entry whose own NodeAddr (depth 0) is the invalidation target. + let mut cache = CoordCache::new(100, 1000); + let target = make_node_addr(1); + + cache.insert(target, make_coords(&[1, 0]), 0); + assert_eq!(cache.len(), 1); + + let removed = cache.invalidate_via_node(&target); + assert_eq!(removed, 1); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_invalidate_via_node_interior() { + // Entry whose ancestry contains the target in the interior of the path. + let mut cache = CoordCache::new(100, 1000); + let dest = make_node_addr(5); + // Path: 5 -> 3 -> 1 -> 0 (root). Target 3 appears at depth 1. + cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); + + let removed = cache.invalidate_via_node(&make_node_addr(3)); + assert_eq!(removed, 1); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_invalidate_via_node_absent() { + // Entry whose ancestry does NOT contain the target must be retained. + let mut cache = CoordCache::new(100, 1000); + let dest = make_node_addr(5); + cache.insert(dest, make_coords(&[5, 3, 1, 0]), 0); + + let removed = cache.invalidate_via_node(&make_node_addr(99)); + assert_eq!(removed, 0); + assert_eq!(cache.len(), 1); + assert!(cache.contains(&dest, 0)); + } + + #[test] + fn test_invalidate_via_node_empty_cache() { + let mut cache = CoordCache::new(100, 1000); + let removed = cache.invalidate_via_node(&make_node_addr(1)); + assert_eq!(removed, 0); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_invalidate_other_roots_current_root_kept() { + let mut cache = CoordCache::new(100, 1000); + // Entries rooted at addr(0) + cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + + let removed = cache.invalidate_other_roots(&make_node_addr(0)); + assert_eq!(removed, 0); + assert_eq!(cache.len(), 2); + } + + #[test] + fn test_invalidate_other_roots_different_root_dropped() { + let mut cache = CoordCache::new(100, 1000); + // Three entries rooted at addr(0), one rooted at addr(9) + cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + cache.insert(make_node_addr(3), make_coords(&[3, 0]), 0); + cache.insert(make_node_addr(4), make_coords(&[4, 9]), 0); + + let removed = cache.invalidate_other_roots(&make_node_addr(0)); + assert_eq!(removed, 1); + assert_eq!(cache.len(), 3); + assert!(!cache.contains(&make_node_addr(4), 0)); + assert!(cache.contains(&make_node_addr(1), 0)); + } + + #[test] + fn test_invalidate_other_roots_all_match() { + let mut cache = CoordCache::new(100, 1000); + cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); + cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0); + + let removed = cache.invalidate_other_roots(&make_node_addr(0)); + assert_eq!(removed, 0); + assert_eq!(cache.len(), 2); + } + + #[test] + fn test_invalidate_other_roots_empty_cache() { + let mut cache = CoordCache::new(100, 1000); + let removed = cache.invalidate_other_roots(&make_node_addr(0)); + assert_eq!(removed, 0); + assert_eq!(cache.len(), 0); + } } diff --git a/src/node/handlers/mmp.rs b/src/node/handlers/mmp.rs index c421c3c..b505b4e 100644 --- a/src/node/handlers/mmp.rs +++ b/src/node/handlers/mmp.rs @@ -153,7 +153,9 @@ impl Node { warn!(error = %e, "Failed to sign declaration after first-RTT parent eval"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_via_node doc. + self.coord_cache + .invalidate_via_node(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; self.stats_mut().tree.parent_switches += 1; @@ -178,7 +180,9 @@ impl Node { warn!(error = %e, "Failed to sign self-root declaration after first-RTT"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_other_roots doc. + self.coord_cache + .invalidate_other_roots(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; self.stats_mut().tree.parent_switches += 1; diff --git a/src/node/tree.rs b/src/node/tree.rs index 1d69a5c..336c8b0 100644 --- a/src/node/tree.rs +++ b/src/node/tree.rs @@ -250,7 +250,9 @@ impl Node { warn!(error = %e, "Failed to sign declaration after parent switch"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_via_node doc. + self.coord_cache + .invalidate_via_node(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; @@ -261,7 +263,7 @@ impl Node { new_seq = new_seq, new_root = %self.tree_state.root(), depth = self.tree_state.my_coords().depth(), - "Parent switched, flushed coord cache, announcing to all peers" + "Parent switched, invalidated downstream coord cache entries, announcing to all peers" ); if flap_dampened { self.stats_mut().tree.flap_dampened += 1; @@ -281,7 +283,9 @@ impl Node { warn!(error = %e, "Failed to sign self-root declaration"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_other_roots doc. + self.coord_cache + .invalidate_other_roots(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; self.stats_mut().tree.parent_switches += 1; @@ -315,7 +319,12 @@ impl Node { warn!(error = %e, "Failed to sign declaration after loop detection"); return; } - self.coord_cache.clear(); + // handle_parent_lost may promote to root OR find new parent; + // cover both invalidation classes. + self.coord_cache + .invalidate_via_node(self.identity.node_addr()); + self.coord_cache + .invalidate_other_roots(self.tree_state.root()); self.reset_discovery_backoff(); self.send_tree_announce_to_all().await; } @@ -350,7 +359,9 @@ impl Node { warn!(error = %e, "Failed to sign declaration after parent update"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_via_node doc. + self.coord_cache + .invalidate_via_node(self.identity.node_addr()); self.reset_discovery_backoff(); let new_addrs: Vec = @@ -439,7 +450,9 @@ impl Node { warn!(error = %e, "Failed to sign declaration after periodic parent re-eval"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_via_node doc. + self.coord_cache + .invalidate_via_node(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; @@ -468,7 +481,9 @@ impl Node { warn!(error = %e, "Failed to sign self-root declaration in periodic reeval"); return; } - self.coord_cache.clear(); + // Surgical invalidation — see CoordCache::invalidate_other_roots doc. + self.coord_cache + .invalidate_other_roots(self.identity.node_addr()); self.reset_discovery_backoff(); self.stats_mut().tree.parent_switched += 1; self.stats_mut().tree.parent_switches += 1;