mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Implement greedy routing with bloom filter priority
Add the full next-hop routing algorithm to Node::find_next_hop(): - Local delivery, direct peer, bloom filter candidates, greedy tree routing fallback, with (link_cost, tree_distance, node_addr) ordering - select_best_candidate() scores by peer→dest distance (not us→peer) with self-distance check to prevent routing loops - TreeState::find_next_hop() for greedy tree routing with progress guarantee - ActivePeer::link_cost() placeholder (constant 1.0) for future link quality metrics Add routing tests including 100-node all-pairs reachability simulation (9900/9900 delivered, 0 loops, avg 4.0 hops, max 8). Update fips-routing.md to reflect bloom filter routing as the primary forwarding mechanism, with greedy tree routing as fallback during convergence windows.
This commit is contained in:
+114
-6
@@ -706,14 +706,122 @@ impl Node {
|
||||
self.peers.values().filter(|p| p.can_send()).count()
|
||||
}
|
||||
|
||||
// === Routing (stubs) ===
|
||||
// === Routing ===
|
||||
|
||||
/// Find next hop for a destination (stub).
|
||||
/// Find next hop for a destination node address.
|
||||
///
|
||||
/// Returns the peer that minimizes tree distance to the destination.
|
||||
pub fn find_next_hop(&self, _dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
|
||||
// Stub: would implement greedy tree routing
|
||||
None
|
||||
/// Routing priority:
|
||||
/// 1. Destination is self → `None` (local delivery)
|
||||
/// 2. Destination is a direct peer → that peer
|
||||
/// 3. Bloom filter candidates + greedy tree routing → among peers whose
|
||||
/// bloom filter contains the destination, pick the one that minimizes
|
||||
/// tree distance to the destination (if dest coords are cached), with
|
||||
/// `(link_cost, tree_distance_to_dest, node_addr)` tie-breaking.
|
||||
/// Falls back to greedy tree routing if no bloom filter hits.
|
||||
/// 4. No route → `None`
|
||||
///
|
||||
/// The self-distance check from greedy routing also applies to bloom
|
||||
/// filter candidates: a peer is only selected if it is strictly closer
|
||||
/// to the destination than we are (prevents routing loops).
|
||||
pub fn find_next_hop(&self, dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
|
||||
// 1. Local delivery
|
||||
if dest_node_addr == self.node_addr() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Direct peer
|
||||
if let Some(peer) = self.peers.get(dest_node_addr) {
|
||||
if peer.can_send() {
|
||||
return Some(peer);
|
||||
}
|
||||
}
|
||||
|
||||
// Look up destination coords (used by both bloom and tree paths)
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let dest_coords = self.coord_cache.get(dest_node_addr, now_ms);
|
||||
|
||||
// 3. Bloom filter candidates, scored by tree distance to dest
|
||||
let candidates: Vec<&ActivePeer> = self.destination_in_filters(dest_node_addr);
|
||||
if !candidates.is_empty() {
|
||||
return self.select_best_candidate(&candidates, dest_coords);
|
||||
}
|
||||
|
||||
// 4. Greedy tree routing fallback
|
||||
let dest_coords = dest_coords?;
|
||||
let next_hop_id = self.tree_state.find_next_hop(dest_coords)?;
|
||||
|
||||
self.peers.get(&next_hop_id).filter(|p| p.can_send())
|
||||
}
|
||||
|
||||
/// Select the best peer from a set of bloom filter candidates.
|
||||
///
|
||||
/// When dest_coords are available, uses distance from each candidate's
|
||||
/// coordinates to the destination as the primary metric (after link_cost).
|
||||
/// Only selects peers that are strictly closer to the destination than
|
||||
/// we are (self-distance check prevents loops).
|
||||
///
|
||||
/// When dest_coords are not available, falls back to distance from us
|
||||
/// to the candidate peer (a weaker heuristic — prefers closer peers on
|
||||
/// the theory that shorter paths are better).
|
||||
///
|
||||
/// Ordering: `(link_cost, distance_to_dest, node_addr)`.
|
||||
fn select_best_candidate<'a>(
|
||||
&'a self,
|
||||
candidates: &[&'a ActivePeer],
|
||||
dest_coords: Option<&crate::tree::TreeCoordinate>,
|
||||
) -> Option<&'a ActivePeer> {
|
||||
let my_distance = dest_coords.map(|dc| self.tree_state.my_coords().distance_to(dc));
|
||||
|
||||
let mut best: Option<(&ActivePeer, f64, usize)> = None;
|
||||
|
||||
for &candidate in candidates {
|
||||
if !candidate.can_send() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = candidate.link_cost();
|
||||
|
||||
// Compute distance: peer→dest if coords available, else us→peer
|
||||
let dist = match dest_coords {
|
||||
Some(dc) => self
|
||||
.tree_state
|
||||
.peer_coords(candidate.node_addr())
|
||||
.map(|pc| pc.distance_to(dc))
|
||||
.unwrap_or(usize::MAX),
|
||||
None => self
|
||||
.tree_state
|
||||
.distance_to_peer(candidate.node_addr())
|
||||
.unwrap_or(usize::MAX),
|
||||
};
|
||||
|
||||
// Self-distance check: when dest coords are available,
|
||||
// only consider peers that are strictly closer than us
|
||||
if let Some(my_dist) = my_distance {
|
||||
if dist >= my_dist {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let dominated = match &best {
|
||||
None => true,
|
||||
Some((_, best_cost, best_dist)) => {
|
||||
cost < *best_cost
|
||||
|| (cost == *best_cost && dist < *best_dist)
|
||||
|| (cost == *best_cost
|
||||
&& dist == *best_dist
|
||||
&& candidate.node_addr() < best.as_ref().unwrap().0.node_addr())
|
||||
}
|
||||
};
|
||||
|
||||
if dominated {
|
||||
best = Some((candidate, cost, dist));
|
||||
}
|
||||
}
|
||||
|
||||
best.map(|(peer, _, _)| peer)
|
||||
}
|
||||
|
||||
/// Check if a destination is in any peer's bloom filter.
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
|
||||
mod bloom;
|
||||
mod handshake;
|
||||
mod routing;
|
||||
mod spanning_tree;
|
||||
mod unit;
|
||||
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
//! Routing integration tests.
|
||||
//!
|
||||
//! Tests the full Node::find_next_hop() routing logic including bloom
|
||||
//! filter priority, greedy tree routing, and tie-breaking.
|
||||
|
||||
use super::*;
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::tree::{ParentDeclaration, TreeCoordinate};
|
||||
use spanning_tree::{
|
||||
cleanup_nodes, drain_all_packets, generate_random_edges, initiate_handshake, make_test_node,
|
||||
run_tree_test, verify_tree_convergence, TestNode,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// === Local delivery ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_local_delivery() {
|
||||
let node = make_node();
|
||||
let my_addr = *node.node_addr();
|
||||
assert!(node.find_next_hop(&my_addr).is_none());
|
||||
}
|
||||
|
||||
// === Direct peer ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_direct_peer() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let link_id = LinkId::new(1);
|
||||
|
||||
let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *identity.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, identity, 2000).unwrap();
|
||||
|
||||
let result = node.find_next_hop(&peer_addr);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().node_addr(), &peer_addr);
|
||||
}
|
||||
|
||||
// === No route ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_unknown_destination() {
|
||||
let node = make_node();
|
||||
let unknown = make_node_addr(99);
|
||||
assert!(node.find_next_hop(&unknown).is_none());
|
||||
}
|
||||
|
||||
// === Bloom filter priority ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_bloom_filter_hit() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
// Create two peers
|
||||
let link_id1 = LinkId::new(1);
|
||||
let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000);
|
||||
let peer1_addr = *id1.node_addr();
|
||||
node.add_connection(conn1).unwrap();
|
||||
node.promote_connection(link_id1, id1, 2000).unwrap();
|
||||
|
||||
let link_id2 = LinkId::new(2);
|
||||
let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000);
|
||||
let peer2_addr = *id2.node_addr();
|
||||
node.add_connection(conn2).unwrap();
|
||||
node.promote_connection(link_id2, id2, 2000).unwrap();
|
||||
|
||||
// Destination not directly connected
|
||||
let dest = make_node_addr(99);
|
||||
|
||||
// Add dest to peer1's bloom filter only
|
||||
let peer1 = node.get_peer_mut(&peer1_addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer1.update_filter(filter, 1, 3000);
|
||||
|
||||
// Should route through peer1 (bloom filter hit)
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().node_addr(), &peer1_addr);
|
||||
|
||||
// Peer2 should NOT be selected (no filter hit)
|
||||
assert_ne!(result.unwrap().node_addr(), &peer2_addr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routing_bloom_filter_multiple_hits_tiebreak() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
// Create three peers
|
||||
let mut peer_addrs = Vec::new();
|
||||
for i in 1..=3 {
|
||||
let link_id = LinkId::new(i);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let addr = *id.node_addr();
|
||||
peer_addrs.push(addr);
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
}
|
||||
|
||||
let dest = make_node_addr(99);
|
||||
|
||||
// Add dest to ALL peers' bloom filters
|
||||
for &addr in &peer_addrs {
|
||||
let peer = node.get_peer_mut(&addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer.update_filter(filter, 1, 3000);
|
||||
}
|
||||
|
||||
// All peers have equal link_cost (1.0) and no tree coords set,
|
||||
// so tree distance is usize::MAX for all. Tie-break by smallest node_addr.
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
|
||||
let smallest_addr = peer_addrs.iter().min().unwrap();
|
||||
assert_eq!(result.unwrap().node_addr(), smallest_addr);
|
||||
}
|
||||
|
||||
// === Greedy tree routing ===
|
||||
|
||||
#[test]
|
||||
fn test_routing_tree_fallback() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
let my_addr = *node.node_addr();
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
let peer_addr = *id.node_addr();
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Set up tree state through the public API.
|
||||
// We're root, peer is our child. The peer has a subtree below it.
|
||||
// TreeState::new() already makes us the root with coords [my_addr].
|
||||
// Add peer as child of us.
|
||||
let peer_coords = TreeCoordinate::from_addrs(vec![peer_addr, my_addr]).unwrap();
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer_addr, my_addr, 1, 1000),
|
||||
peer_coords,
|
||||
);
|
||||
|
||||
// Destination: a node under our peer in the tree
|
||||
let dest = make_node_addr(99);
|
||||
let dest_coords =
|
||||
TreeCoordinate::from_addrs(vec![dest, peer_addr, my_addr]).unwrap();
|
||||
|
||||
// Put dest coords in the cache
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
node.coord_cache_mut().insert(dest, dest_coords, now_ms);
|
||||
|
||||
// No bloom filter hit — should fall back to tree routing.
|
||||
// Our distance to dest: 2 (root → peer → dest)
|
||||
// Peer's distance to dest: 1 (peer → dest)
|
||||
// Peer is closer, so it's the next hop.
|
||||
let result = node.find_next_hop(&dest);
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap().node_addr(), &peer_addr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routing_tree_no_coords_in_cache() {
|
||||
let mut node = make_node();
|
||||
let transport_id = TransportId::new(1);
|
||||
|
||||
// Create a peer
|
||||
let link_id = LinkId::new(1);
|
||||
let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000);
|
||||
node.add_connection(conn).unwrap();
|
||||
node.promote_connection(link_id, id, 2000).unwrap();
|
||||
|
||||
// Destination not in bloom filters and not in coord cache
|
||||
let dest = make_node_addr(99);
|
||||
assert!(node.find_next_hop(&dest).is_none());
|
||||
}
|
||||
|
||||
// === Integration: converged network ===
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_routing_chain_topology() {
|
||||
// Build a 4-node chain: 0 -- 1 -- 2 -- 3
|
||||
let mut nodes = vec![
|
||||
make_test_node().await,
|
||||
make_test_node().await,
|
||||
make_test_node().await,
|
||||
make_test_node().await,
|
||||
];
|
||||
|
||||
// Connect the chain
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
initiate_handshake(&mut nodes, 1, 2).await;
|
||||
initiate_handshake(&mut nodes, 2, 3).await;
|
||||
|
||||
// Converge tree and bloom filters
|
||||
drain_all_packets(&mut nodes, false).await;
|
||||
|
||||
// Verify tree convergence
|
||||
let root = nodes.iter().map(|n| *n.node.node_addr()).min().unwrap();
|
||||
for tn in &nodes {
|
||||
assert_eq!(
|
||||
*tn.node.tree_state().root(),
|
||||
root,
|
||||
"Tree not converged"
|
||||
);
|
||||
}
|
||||
|
||||
// Populate coord caches: each node caches the far-end node's coords
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let node3_addr = *nodes[3].node.node_addr();
|
||||
let node3_coords = nodes[3].node.tree_state().my_coords().clone();
|
||||
nodes[0]
|
||||
.node
|
||||
.coord_cache_mut()
|
||||
.insert(node3_addr, node3_coords, now_ms);
|
||||
|
||||
let node0_addr = *nodes[0].node.node_addr();
|
||||
let node0_coords = nodes[0].node.tree_state().my_coords().clone();
|
||||
nodes[3]
|
||||
.node
|
||||
.coord_cache_mut()
|
||||
.insert(node0_addr, node0_coords, now_ms);
|
||||
|
||||
// Node 0 should be able to route toward node 3.
|
||||
// The next hop should be node 1 (only peer of node 0).
|
||||
let hop = nodes[0].node.find_next_hop(&node3_addr);
|
||||
assert!(hop.is_some(), "Node 0 should find route to node 3");
|
||||
let node1_addr = *nodes[1].node.node_addr();
|
||||
assert_eq!(
|
||||
hop.unwrap().node_addr(),
|
||||
&node1_addr,
|
||||
"Node 0's next hop to node 3 should be node 1"
|
||||
);
|
||||
|
||||
// Node 3 should route toward node 0 via node 2.
|
||||
let hop = nodes[3].node.find_next_hop(&node0_addr);
|
||||
assert!(hop.is_some(), "Node 3 should find route to node 0");
|
||||
let node2_addr = *nodes[2].node.node_addr();
|
||||
assert_eq!(
|
||||
hop.unwrap().node_addr(),
|
||||
&node2_addr,
|
||||
"Node 3's next hop to node 0 should be node 2"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_routing_bloom_preferred_over_tree() {
|
||||
// Build a 3-node triangle: 0 -- 1, 0 -- 2, 1 -- 2
|
||||
let mut nodes = vec![
|
||||
make_test_node().await,
|
||||
make_test_node().await,
|
||||
make_test_node().await,
|
||||
];
|
||||
|
||||
initiate_handshake(&mut nodes, 0, 1).await;
|
||||
initiate_handshake(&mut nodes, 0, 2).await;
|
||||
initiate_handshake(&mut nodes, 1, 2).await;
|
||||
|
||||
drain_all_packets(&mut nodes, false).await;
|
||||
|
||||
// Create a destination beyond the network
|
||||
let dest = make_node_addr(99);
|
||||
|
||||
// Add dest to peer 2's bloom filter (from node 0's perspective)
|
||||
let peer2_addr = *nodes[2].node.node_addr();
|
||||
let peer2 = nodes[0].node.get_peer_mut(&peer2_addr).unwrap();
|
||||
let mut filter = BloomFilter::new();
|
||||
filter.insert(&dest);
|
||||
peer2.update_filter(filter, 100, 50000);
|
||||
|
||||
// Even though we could use tree routing (if coords were cached),
|
||||
// the bloom filter hit should be preferred.
|
||||
let hop = nodes[0].node.find_next_hop(&dest);
|
||||
assert!(hop.is_some(), "Should route via bloom filter");
|
||||
assert_eq!(
|
||||
hop.unwrap().node_addr(),
|
||||
&peer2_addr,
|
||||
"Should pick peer with bloom filter hit"
|
||||
);
|
||||
}
|
||||
|
||||
// === Multi-hop forwarding simulation ===
|
||||
|
||||
/// Result of simulating multi-hop packet forwarding.
|
||||
#[derive(Debug)]
|
||||
enum ForwardResult {
|
||||
/// Packet reached the destination in the given number of hops.
|
||||
Delivered(usize),
|
||||
/// Routing returned None at the given node index (no route).
|
||||
NoRoute { at_node: usize, hops: usize },
|
||||
/// Routing loop detected (visited the same node twice).
|
||||
Loop { at_node: usize, hops: usize },
|
||||
}
|
||||
|
||||
/// Build a NodeAddr → node index lookup table.
|
||||
fn build_addr_index(nodes: &[TestNode]) -> std::collections::HashMap<NodeAddr, usize> {
|
||||
nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tn)| (*tn.node.node_addr(), i))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Simulate multi-hop forwarding from source to destination.
|
||||
///
|
||||
/// At each hop, calls `find_next_hop` on the current node and follows
|
||||
/// the result to the next node. Terminates on delivery, routing failure,
|
||||
/// or loop detection.
|
||||
fn simulate_forwarding(
|
||||
nodes: &[TestNode],
|
||||
addr_index: &std::collections::HashMap<NodeAddr, usize>,
|
||||
src: usize,
|
||||
dst: usize,
|
||||
) -> ForwardResult {
|
||||
let dest_addr = *nodes[dst].node.node_addr();
|
||||
let max_hops = nodes.len(); // can't take more hops than nodes
|
||||
|
||||
let mut current = src;
|
||||
let mut visited = HashSet::new();
|
||||
visited.insert(current);
|
||||
|
||||
for hop in 0..max_hops {
|
||||
let next = nodes[current].node.find_next_hop(&dest_addr);
|
||||
|
||||
match next {
|
||||
None => {
|
||||
// find_next_hop returns None for local delivery (dest == self)
|
||||
if *nodes[current].node.node_addr() == dest_addr {
|
||||
return ForwardResult::Delivered(hop);
|
||||
}
|
||||
return ForwardResult::NoRoute {
|
||||
at_node: current,
|
||||
hops: hop,
|
||||
};
|
||||
}
|
||||
Some(peer) => {
|
||||
let next_addr = *peer.node_addr();
|
||||
|
||||
// Is next hop the destination?
|
||||
if next_addr == dest_addr {
|
||||
return ForwardResult::Delivered(hop + 1);
|
||||
}
|
||||
|
||||
// Find the node index for the next hop
|
||||
let next_idx = match addr_index.get(&next_addr) {
|
||||
Some(&idx) => idx,
|
||||
None => {
|
||||
return ForwardResult::NoRoute {
|
||||
at_node: current,
|
||||
hops: hop,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Loop detection
|
||||
if visited.contains(&next_idx) {
|
||||
return ForwardResult::Loop {
|
||||
at_node: next_idx,
|
||||
hops: hop + 1,
|
||||
};
|
||||
}
|
||||
|
||||
visited.insert(next_idx);
|
||||
current = next_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForwardResult::NoRoute {
|
||||
at_node: current,
|
||||
hops: max_hops,
|
||||
}
|
||||
}
|
||||
|
||||
/// 100-node random graph: verify all-pairs routing reachability.
|
||||
///
|
||||
/// After tree and bloom filter convergence, simulates multi-hop packet
|
||||
/// forwarding between every pair of nodes. Every packet must be delivered
|
||||
/// without loops.
|
||||
#[tokio::test]
|
||||
async fn test_routing_reachability_100_nodes() {
|
||||
const NUM_NODES: usize = 100;
|
||||
const TARGET_EDGES: usize = 250;
|
||||
const SEED: u64 = 42;
|
||||
|
||||
let edges = generate_random_edges(NUM_NODES, TARGET_EDGES, SEED);
|
||||
let mut nodes = run_tree_test(NUM_NODES, &edges, false).await;
|
||||
verify_tree_convergence(&nodes);
|
||||
|
||||
// Populate coord caches: every node learns every other node's coordinates.
|
||||
// In production this happens via SessionSetup/LookupResponse; here we
|
||||
// inject them directly so routing can make progress-based decisions.
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Collect all (addr, coords) pairs first to avoid borrow issues
|
||||
let all_coords: Vec<(NodeAddr, TreeCoordinate)> = nodes
|
||||
.iter()
|
||||
.map(|tn| (*tn.node.node_addr(), tn.node.tree_state().my_coords().clone()))
|
||||
.collect();
|
||||
|
||||
for node in &mut nodes {
|
||||
for &(ref addr, ref coords) in &all_coords {
|
||||
if addr != node.node.node_addr() {
|
||||
node.node.coord_cache_mut().insert(*addr, coords.clone(), now_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let addr_index = build_addr_index(&nodes);
|
||||
|
||||
let mut total_pairs = 0;
|
||||
let mut total_hops = 0usize;
|
||||
let mut max_hops = 0usize;
|
||||
let mut failures = Vec::new();
|
||||
let mut loops = Vec::new();
|
||||
|
||||
// Test all pairs
|
||||
for src in 0..NUM_NODES {
|
||||
for dst in 0..NUM_NODES {
|
||||
if src == dst {
|
||||
continue;
|
||||
}
|
||||
|
||||
total_pairs += 1;
|
||||
|
||||
match simulate_forwarding(&nodes, &addr_index, src, dst) {
|
||||
ForwardResult::Delivered(hops) => {
|
||||
total_hops += hops;
|
||||
if hops > max_hops {
|
||||
max_hops = hops;
|
||||
}
|
||||
}
|
||||
ForwardResult::NoRoute { at_node, hops } => {
|
||||
failures.push((src, dst, at_node, hops));
|
||||
}
|
||||
ForwardResult::Loop { at_node, hops } => {
|
||||
loops.push((src, dst, at_node, hops));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let delivered = total_pairs - failures.len() - loops.len();
|
||||
let avg_hops = if delivered > 0 {
|
||||
total_hops as f64 / delivered as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"\n === Routing Reachability ({} nodes) ===",
|
||||
NUM_NODES
|
||||
);
|
||||
eprintln!(
|
||||
" Pairs tested: {} | Delivered: {} | Failed: {} | Loops: {}",
|
||||
total_pairs,
|
||||
delivered,
|
||||
failures.len(),
|
||||
loops.len()
|
||||
);
|
||||
eprintln!(
|
||||
" Hops: avg={:.1} max={}",
|
||||
avg_hops, max_hops
|
||||
);
|
||||
|
||||
if !failures.is_empty() {
|
||||
let show = failures.len().min(10);
|
||||
eprintln!(" First {} failures:", show);
|
||||
for &(src, dst, at_node, hops) in &failures[..show] {
|
||||
eprintln!(
|
||||
" {} -> {}: stuck at node {} after {} hops",
|
||||
src, dst, at_node, hops
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !loops.is_empty() {
|
||||
let show = loops.len().min(10);
|
||||
eprintln!(" First {} loops:", show);
|
||||
for &(src, dst, at_node, hops) in &loops[..show] {
|
||||
eprintln!(
|
||||
" {} -> {}: loop at node {} after {} hops",
|
||||
src, dst, at_node, hops
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
loops.is_empty(),
|
||||
"Detected {} routing loops out of {} pairs",
|
||||
loops.len(),
|
||||
total_pairs
|
||||
);
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"Detected {} routing failures out of {} pairs",
|
||||
failures.len(),
|
||||
total_pairs
|
||||
);
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user