Merge maint into master (coord cache surgical invalidation)

This commit is contained in:
Johnathan Corgan
2026-05-17 00:38:47 +00:00
3 changed files with 159 additions and 9 deletions
+131
View File
@@ -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);
}
}
+6 -2
View File
@@ -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;
+22 -7
View File
@@ -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<NodeAddr> =
@@ -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;