aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 21:35:33 +0100
committertv <tv@krebsco.de>2026-11-05 21:35:33 +0100
commit91c91a4cee04bb15eeeefb78223c7c72694dd2e2 (patch)
tree970e4cc253434be7875f837a0f40da3949218fb9 /src
parentb0ddfeccbc04a39196717e81daa5dc3aa0f42c69 (diff)
slow link a hop more
Diffstat (limited to 'src')
-rw-r--r--src/swarm/tree.rs90
1 files changed, 73 insertions, 17 deletions
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<Graph>, Arc<HashMap<S
/// Members are ordered by distance from the writer, then whether they reach a member a
/// hop closer (as they say: else, while it hasn't the change from its own side, it is
/// as good as farther), then pair key; a member's parent is the neighbour it reaches
-/// that comes before it, by the same, then round trip, then pair score. So where the writer reaches few
+/// that comes before it, by the same (a much slower link counting a hop more), then
+/// round trip, then pair score. So where the writer reaches few
/// (behind NAT), those it reaches come first among the members as far from it, and the
/// others take them as parents. What a member reaches we know of ourselves and our
/// neighbours, as they said; of others, the graph is taken as right (if we're wrong,
@@ -127,11 +129,19 @@ fn rank(
Some((d, !closer, topology::pair_key(member)))
}
+/// A link to a possible parent this many round-trip buckets slower than the member's
+/// fastest to one (`rtt::bucket`; with doubling bounds, at least twice the round trip)
+/// counts as a hop more (`parent`): a member whose link to the writer is a slow circuit
+/// takes one as close to the writer as itself, if it has a fast link to one, rather than
+/// lag behind the others with every change.
+const SLOW_BY_BUCKETS: u8 = 2;
+
/// `member`'s parent in the writer's tree (`rank`): 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 (`round_trip(member, candidate)`,
-/// a bucket: `rtt::bucket`), then by pair score; None if none does (or it is the
-/// writer).
+/// before it, the closest to the writer first (a link `SLOW_BY_BUCKETS` 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 (`round_trip(member, candidate)`, a
+/// bucket: `rtt::bucket`), then by pair score; None if none does (or it is the writer).
+/// Its round trips are what its neighbours know too, so they compute the same parent.
fn parent<'g>(
graph: &'g Graph,
distance: &HashMap<String, u32>,
@@ -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<String, u32> = [("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]