node: periodically re-broadcast TreeAnnounce on no-change in check_periodic_parent_reeval

Closes the eventually-consistent gap in spanning-tree state
distribution. Every existing send_tree_announce_to_all call site
gates on a local state-change event (parent switch, self-root
promotion, ancestry change, peer promotion, parent loss). Once a
partition latches — for example a parent-switch announce stranded
in the brief cross-init handshake swap window, where the announce
arrives on a session-index whose decrypt-worker entry has been
unregistered — neither side's state changes again, so neither
side ever re-broadcasts. The existing 60 s check_periodic_parent_reeval
was a re-evaluation, not a re-broadcast: it short-circuited
silently on no-change. Production-side healing depended on
incidental link churn; lab harnesses with stable docker-bridge
links had no equivalent path.

Add a final else branch that fires send_tree_announce_to_all
unconditionally on the no-change path, alongside the existing
switch and self-promote arms. Receivers coalesce by sequence
comparison (ParentDeclaration::is_fresher_than) and short-circuit
at the `if !updated` gate in handle_tree_announce; same-sequence
repeats drop silently with no cascade. The per-peer 500 ms
rate-limiter is well below this 60 s cadence and does not suppress
the heartbeat broadcast.

The fix is a general protocol-robustness improvement: it addresses
any in-flight TreeAnnounce loss class, not only the specific
cross-init swap-window drop site.

testing/static/scripts/rekey-test.sh BASELINE_CONVERGENCE_TIMEOUT
60 -> 65 so a partition healed by the periodic broadcast at T+60
lands inside the convergence window. wait_for_full_baseline
early-exits on PASS, so successful reps see no extra wall-clock.
This commit is contained in:
Johnathan Corgan
2026-05-26 02:50:51 +00:00
parent 4d5380604a
commit ffd78440a8
3 changed files with 50 additions and 2 deletions
+16
View File
@@ -495,6 +495,22 @@ impl Node {
self.send_tree_announce_to_all().await;
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
self.bloom_state.mark_all_updates_needed(all_peers);
} else {
// Periodic re-broadcast on no-change: makes TreeAnnounce
// distribution eventually-consistent. Receivers coalesce
// by sequence via ParentDeclaration::is_fresher_than and
// short-circuit at the `if !updated` gate in
// handle_tree_announce; the per-peer 500 ms rate-limiter
// never blocks at this 60 s cadence. Closes the cross-init
// in-flight loss recovery gap where the swap window can
// strand one side's announce on a session-index the other
// side cannot decrypt.
trace!(
seq = self.tree_state.my_declaration().sequence(),
root = %self.tree_state.root(),
"Periodic TreeAnnounce re-broadcast (no state change)"
);
self.send_tree_announce_to_all().await;
}
}