From 91c91a4cee04bb15eeeefb78223c7c72694dd2e2 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 5 Nov 2026 21:35:33 +0100 Subject: slow link a hop more --- docs/peers.md | 4 ++- src/swarm/tree.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/docs/peers.md b/docs/peers.md index eefb0cc..29512a5 100644 --- a/docs/peers.md +++ b/docs/peers.md @@ -351,7 +351,9 @@ built on the graph the member list gives (as if every member reached every other on whom each member says it reaches. Members are ordered by their distance from the writer over that graph, then whether they reach a member a hop closer, then by key; a member's parent is the neighbour it reaches that comes before it, the closest to the -writer first, then one that reaches closer, then the one it said it has the shortest +writer first (a link at least two round-trip buckets slower than its fastest to one +counting a hop more: a slow circuit to the writer is passed over for a fast link to a +member as close), then one that reaches closer, then the one it said it has the shortest round trip to, then by pair score. Coming before rules out cycles; taking only one it reaches routes around links that don't work: where a writer behind NAT reaches a single member, that one comes first among those as far from it, diff --git a/src/swarm/tree.rs b/src/swarm/tree.rs index 29186d8..0aef94c 100644 --- a/src/swarm/tree.rs +++ b/src/swarm/tree.rs @@ -7,8 +7,9 @@ //! (`swarm::reach`). Every member's distance from the writer over that graph orders the //! members: by distance, then whether it reaches a member a hop closer, then pair key; //! a member's parent is the neighbour it reaches that comes before it, the closest to -//! the writer first, then one that reaches closer, then the one it has the shortest -//! round trip to (as it said, a bucket: `rtt::bucket`), then by pair score +//! the writer first (a link much slower than its fastest to one counting a hop more), +//! then one that reaches closer, then the one it has the shortest round trip to (as it +//! said, a bucket: `rtt::bucket`), then by pair score //! (`topology::pair_score`). Coming before rules out cycles, and taking only a neighbour //! it reaches routes around links that don't work: where a writer behind NAT reaches a //! single member, that one comes first and the others take it. @@ -95,7 +96,8 @@ fn distances(state: &AppState, writer: u64) -> Option<(Arc, Arc( graph: &'g Graph, distance: &HashMap, @@ -140,7 +150,7 @@ fn parent<'g>( member: &str, ) -> Option<&'g str> { let its_rank = rank(graph, distance, reached, member)?; - graph + let candidates: Vec<&str> = graph .get(member) .into_iter() .flatten() @@ -149,15 +159,22 @@ fn parent<'g>( rank(graph, distance, reached, candidate).is_some_and(|r| r < its_rank) && reached(member, candidate) }) - .min_by_key(|candidate| { - let (d, far, _) = rank(graph, distance, reached, candidate).expect("ranked"); - ( - d, - far, - round_trip(member, candidate), - topology::pair_score(candidate, member), - ) - }) + .collect(); + let fastest = candidates + .iter() + .map(|candidate| round_trip(member, candidate)) + .min()?; + candidates.into_iter().min_by_key(|candidate| { + let (d, far, _) = rank(graph, distance, reached, candidate).expect("ranked"); + let bucket = round_trip(member, candidate); + let slow = u32::from(bucket >= fastest.saturating_add(SLOW_BY_BUCKETS)); + ( + d + slow, + far, + bucket, + topology::pair_score(candidate, member), + ) + }) } /// `writer`'s whole tree, as far as `reached` knows whom each member reaches and @@ -643,6 +660,45 @@ mod tests { } } + /// A member whose link to the writer is much slower than to another member as close + /// to the writer, one that comes before it, takes that one; one bucket slower, it + /// keeps the writer. + #[test] + fn a_slow_link_counts_a_hop_more() { + let mut graph = Graph::new(); + for (a, b) in [("w", "a"), ("w", "x"), ("a", "x")] { + graph.entry(a.into()).or_default().insert(b.into()); + graph.entry(b.into()).or_default().insert(a.into()); + } + let distance: HashMap = [("w", 0), ("a", 1), ("x", 1)] + .into_iter() + .map(|(member, d)| (member.to_owned(), d)) + .collect(); + let reached = |_: &str, _: &str| true; + // Whichever of the two comes first (pair key) may be the other's parent. + let (first, second) = if topology::pair_key("a") < topology::pair_key("x") { + ("a", "x") + } else { + ("x", "a") + }; + for (to_writer, expected) in [(6, first), (5, "w")] { + let round_trip = |by: &str, member: &str| match (by, member) { + (by, "w") if by == second => to_writer, + (by, m) if by == second && m == first => 4, + _ => rtt::UNKNOWN_BUCKET, + }; + assert_eq!( + parent(&graph, &distance, &reached, &round_trip, second), + Some(expected), + "{to_writer}" + ); + assert_eq!( + parent(&graph, &distance, &reached, &round_trip, first), + Some("w") + ); + } + } + /// Hops along parents from us; members without a parent, below one without, or /// not connected count as off the tree. #[test] -- cgit v1.3.1