node: re-push TreeAnnounce when a peer advertises a worse root

A node with a single tree peer has its periodic parent re-evaluation
disabled (it needs at least two peers for a meaningful comparison), so
it depends entirely on its peer pushing a TreeAnnounce for it to attach.
That push happens once at promotion time plus on the parent's slow
periodic no-change re-broadcast. If the one-shot attaching announce is
lost, the single-uplink node falls back to self-root and cannot recover
until the next periodic re-broadcast (reeval_interval_secs later),
stranding it out of the tree and unreachable end-to-end in the interim.

Make tree-position exchange self-healing on the receive path: when an
accepted TreeAnnounce advertises a root strictly worse (higher NodeAddr,
since election is smallest-wins) than our own, echo our current
declaration back to that peer. A stranded self-root node's announce now
provokes its better-rooted peer to re-push its real position immediately,
so the node re-attaches within a round-trip instead of waiting for the
periodic cadence.

Echo only in that one direction. If the peer's root is lower (better)
than ours, we are the stale side: the peer would ignore our worse root
anyway and we converge via the parent re-evaluation that follows, so
echoing back is pure waste and would double announce traffic in the
learning direction during a root change or partition merge. Equal roots
are already converged. The echo is bounded by the existing per-peer
500 ms tree-announce rate limiter and is a no-op once the peer adopts our
root, so it adds no traffic in a converged mesh.

Add a spanning-tree unit test that drives a converged child back to
self-root with its peer-ancestry view of the root cleared (modelling the
lost attaching announce), and asserts the root re-pushes on the
resulting root disagreement and the child re-attaches.
This commit is contained in:
Johnathan Corgan
2026-06-08 14:36:13 +00:00
parent 1b7528ce89
commit d364933ca5
2 changed files with 135 additions and 0 deletions
+35
View File
@@ -216,6 +216,41 @@ impl Node {
"Processed TreeAnnounce"
);
// Re-push our current position when the announcing peer advertises a
// strictly worse (higher NodeAddr) root than ours. Root election is
// smallest-NodeAddr-wins, so a peer on a higher root has a stale or
// pre-attachment view and can attach to (or re-attach through) us;
// reply with our current declaration so it does so without waiting
// for the next periodic re-broadcast cadence.
//
// Only the better-rooted side echoes. If the peer's root is lower
// (better) than ours, WE are the stale side: the peer would ignore
// our worse root anyway, and we converge via the parent re-evaluation
// below, so echoing back is pure waste — and during a root change or
// partition merge it would double announce traffic in the learning
// direction. Equal roots are already converged. Restricting to `>`
// keeps the echo to the one direction that helps.
//
// This closes a convergence wedge on a single-uplink node: its only
// peer pushes the attaching announce once at promotion time, and if
// that datagram is lost the single-uplink node cannot self-correct
// (its own periodic parent re-evaluation is disabled below two peers)
// and is stranded as a self-root until the parent's next periodic
// re-broadcast (~reeval_interval_secs later). Echoing on root
// disagreement makes tree-position exchange self-healing on the
// receive path and is naturally bounded by the per-peer 500 ms
// tree-announce rate limiter, so it does not storm during normal
// convergence (it stops as soon as the peer adopts our root).
if *announce.ancestry.root_id() > *self.tree_state.root()
&& let Err(e) = self.send_tree_announce_to_peer(from).await
{
debug!(
peer = %self.peer_display_name(from),
error = %e,
"Failed to re-push TreeAnnounce on root disagreement"
);
}
// Bloom filter exchange initiation is handled at handshake completion
// ([handshake.rs] mark_update_needed on the new peer) and on actual
// content changes via [bloom.rs::handle_filter_announce]'s