mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Implement cost-based parent selection with periodic re-evaluation
Cost-based parent selection: - Replace depth-only parent selection with effective_depth = depth + link_cost - link_cost computed from locally measured MMP metrics: etx * (1.0 + srtt_ms / 100.0) - Prevents bottleneck subtrees in heterogeneous networks where a LoRa link at depth 1 would otherwise always beat fiber at depth 2 - Configurable hysteresis (default 0.2) prevents marginal parent switches - Configurable hold-down timer (default 30s) suppresses re-evaluation after parent switch - Mandatory switches (parent lost, root change) bypass both safeguards - Link costs passed as HashMap parameter to keep TreeState pure Periodic re-evaluation: - evaluate_parent() was only called on TreeAnnounce receipt or parent loss; after tree stabilization, link degradation went undetected - Added timer-based re-evaluation (reeval_interval_secs, default 60s) that calls evaluate_parent() from the tick handler with current MMP link costs - Respects existing hold-down and hysteresis safeguards - Short-circuits when disabled or <2 peers Design documentation: - Update 7 design docs to reflect cost-based parent selection - Replace depth-only algorithm descriptions with effective_depth model - Replace rejected cumulative path cost spec with local-only design rationale - Rewrite Example 2 (heterogeneous links) for local-only cost model - Update config docs: parent_switch_threshold replaced by parent_hysteresis, hold_down_secs, reeval_interval_secs Chaos simulation enhancements: - fips_overrides with deep merge for per-scenario FIPS config customization - Explicit topology algorithm for deterministic test graphs - Control socket querying via fipsctl for tree/MMP snapshot collection - Edge existence validation in netem manager - Per-link netem policy overrides - 9 new chaos scenarios covering cost avoidance, depth-vs-cost tradeoffs, stability, mixed topologies, periodic re-evaluation, and bottleneck parent 12 new unit tests, 667 total passing, clippy clean.
This commit is contained in:
+10
-2
@@ -337,6 +337,10 @@ pub struct Node {
|
||||
/// are exhausted.
|
||||
retry_pending: HashMap<NodeAddr, retry::RetryState>,
|
||||
|
||||
// === Periodic Parent Re-evaluation ===
|
||||
/// Timestamp of last periodic parent re-evaluation (for pacing).
|
||||
last_parent_reeval: Option<std::time::Instant>,
|
||||
|
||||
// === Display Names ===
|
||||
/// Human-readable names for configured peers (alias or short npub).
|
||||
/// Populated at startup from peer config.
|
||||
@@ -368,7 +372,8 @@ impl Node {
|
||||
|
||||
// Initialize tree state with signed self-declaration
|
||||
let mut tree_state = TreeState::new(node_addr);
|
||||
tree_state.set_parent_switch_threshold(config.node.tree.parent_switch_threshold);
|
||||
tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
|
||||
tree_state.set_hold_down(config.node.tree.hold_down_secs);
|
||||
tree_state
|
||||
.sign_declaration(&identity)
|
||||
.expect("signing own declaration should never fail");
|
||||
@@ -432,6 +437,7 @@ impl Node {
|
||||
std::time::Duration::from_millis(coords_response_interval_ms),
|
||||
),
|
||||
retry_pending: HashMap::new(),
|
||||
last_parent_reeval: None,
|
||||
peer_aliases: HashMap::new(),
|
||||
})
|
||||
}
|
||||
@@ -451,7 +457,8 @@ impl Node {
|
||||
|
||||
// Initialize tree state with signed self-declaration
|
||||
let mut tree_state = TreeState::new(node_addr);
|
||||
tree_state.set_parent_switch_threshold(config.node.tree.parent_switch_threshold);
|
||||
tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
|
||||
tree_state.set_hold_down(config.node.tree.hold_down_secs);
|
||||
tree_state
|
||||
.sign_declaration(&identity)
|
||||
.expect("signing own declaration should never fail");
|
||||
@@ -518,6 +525,7 @@ impl Node {
|
||||
std::time::Duration::from_millis(coords_response_interval_ms),
|
||||
),
|
||||
retry_pending: HashMap::new(),
|
||||
last_parent_reeval: None,
|
||||
peer_aliases: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+76
-4
@@ -3,6 +3,8 @@
|
||||
//! Handles building, sending, and receiving TreeAnnounce messages,
|
||||
//! including periodic root refresh and rate-limited propagation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::protocol::TreeAnnounce;
|
||||
use crate::NodeAddr;
|
||||
|
||||
@@ -194,8 +196,11 @@ impl Node {
|
||||
self.bloom_state.mark_update_needed(*from);
|
||||
}
|
||||
|
||||
// Re-evaluate parent selection
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent() {
|
||||
// Re-evaluate parent selection with current link costs
|
||||
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
|
||||
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
||||
.collect();
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
||||
let new_seq = self.tree_state.my_declaration().sequence() + 1;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -268,9 +273,73 @@ impl Node {
|
||||
|
||||
/// Periodic tree maintenance, called from the tick handler.
|
||||
///
|
||||
/// Sends pending rate-limited announces.
|
||||
/// Sends pending rate-limited announces and checks for periodic
|
||||
/// parent re-evaluation based on current MMP link costs.
|
||||
pub(super) async fn check_tree_state(&mut self) {
|
||||
self.send_pending_tree_announces().await;
|
||||
self.check_periodic_parent_reeval().await;
|
||||
}
|
||||
|
||||
/// Periodic parent re-evaluation based on current MMP link costs.
|
||||
///
|
||||
/// Self-paces using `last_parent_reeval` and the configured
|
||||
/// `reeval_interval_secs`. When a better parent is found, follows
|
||||
/// the same switch flow as TreeAnnounce-triggered switches.
|
||||
async fn check_periodic_parent_reeval(&mut self) {
|
||||
let interval_secs = self.config.node.tree.reeval_interval_secs;
|
||||
if interval_secs == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Need at least 2 peers for a meaningful comparison
|
||||
if self.peers.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
let interval = std::time::Duration::from_secs(interval_secs);
|
||||
|
||||
if let Some(last) = self.last_parent_reeval {
|
||||
if now.duration_since(last) < interval {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.last_parent_reeval = Some(now);
|
||||
|
||||
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
|
||||
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
||||
.collect();
|
||||
|
||||
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
||||
let new_seq = self.tree_state.my_declaration().sequence() + 1;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
warn!(error = %e, "Failed to sign declaration after periodic parent re-eval");
|
||||
return;
|
||||
}
|
||||
self.tree_state.recompute_coords();
|
||||
self.coord_cache.clear();
|
||||
|
||||
info!(
|
||||
new_parent = %self.peer_display_name(&new_parent),
|
||||
new_seq = new_seq,
|
||||
new_root = %self.tree_state.root(),
|
||||
depth = self.tree_state.my_coords().depth(),
|
||||
trigger = "periodic",
|
||||
"Parent switched via periodic cost re-evaluation"
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle tree state cleanup when a peer is removed.
|
||||
@@ -286,7 +355,10 @@ impl Node {
|
||||
self.tree_state.remove_peer(node_addr);
|
||||
|
||||
if was_parent {
|
||||
let changed = self.tree_state.handle_parent_lost();
|
||||
let peer_costs: HashMap<NodeAddr, f64> = self.peers.iter()
|
||||
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
||||
.collect();
|
||||
let changed = self.tree_state.handle_parent_lost(&peer_costs);
|
||||
if changed {
|
||||
// Re-sign the new declaration
|
||||
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
|
||||
|
||||
Reference in New Issue
Block a user