mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
cache: scope coord cache invalidation to entries actually affected by topology change
Replaces the unconditional `CoordCache::clear()` calls at parent-switch, become-root, and loop-detection sites with two targeted invalidation methods scoped to what actually makes an entry stale: - `invalidate_via_node(node_addr)`: drop entries whose cached destination ancestry contains `node_addr`. Used at parent-position- change sites — our prefix changed, so destinations downstream of us have stale-prefix coords. - `invalidate_other_roots(current_root)`: drop entries rooted under a different root than the current one. Used at root-change sites. Under the previous global flush, parent switches blanked the cache across the board, leaving `find_next_hop` returning `None` for every non-direct-peer destination until the cache passively re-warmed via incoming TreeAnnounces / SessionSetup. Surgical invalidation preserves entries that remain correct after the topology change. The cached coord describes a destination's tree position; that position only goes stale relative to our own routing decisions when our own prefix changes (entries we are downstream of) or the root changes (entries in a different tree). Peer removal does not invalidate cached coords: `Node::find_next_hop` recomputes the next-hop decision on every call against the current peer set, bloom filters, and tree state, and Discovery already triggers on `no route to destination` errors when a destination becomes unroutable through us. The peer-removal site retains the original "no cache invalidation" behavior. Each method returns the count of entries removed for observability. Unit tests cover each method against the cases enumerated in the acceptance criterion.
This commit is contained in:
Vendored
+131
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user