aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-06 19:53:27 +0100
committertv <tv@krebsco.de>2026-11-06 19:53:27 +0100
commit7c0c318dca3054be6c7bacf5a4b3f5a4e47cc949 (patch)
tree8baa20fcef0b22f21f05d91c282b3613c546d98f /src
parentdbff95968b859b2f879442dfc5c4b5bbf916ad96 (diff)
topology neighbours and optional graph
Diffstat (limited to 'src')
-rw-r--r--src/swarm/topology/graph.rs17
-rw-r--r--src/swarm/topology/mod.rs161
2 files changed, 120 insertions, 58 deletions
diff --git a/src/swarm/topology/graph.rs b/src/swarm/topology/graph.rs
index 92240e9..d8b2921 100644
--- a/src/swarm/topology/graph.rs
+++ b/src/swarm/topology/graph.rs
@@ -34,8 +34,8 @@ pub(crate) fn schemes(addresses: &[String]) -> BTreeSet<String> {
/// The neighbours of `local` among `members` (by the transports each is on), on each of
/// `local_transports`: on each, its neighbours among the members on it
/// (`neighbours`), all together.
-pub(crate) fn neighbours_across(
- topology: &dyn Topology,
+pub(crate) fn neighbours_across<T: Topology + ?Sized>(
+ topology: &T,
members: &BTreeMap<String, BTreeSet<String>>,
local: &str,
local_transports: &BTreeSet<String>,
@@ -56,9 +56,10 @@ pub(crate) fn neighbours_across(
}
/// The neighbours of `local` among `members` (which may or may not contain `local`),
-/// skipping members for which `unreachable` holds.
-pub(crate) fn neighbours(
- topology: &dyn Topology,
+/// skipping members for which `unreachable` holds; none by a topology without a shared
+/// graph.
+pub(crate) fn neighbours<T: Topology + ?Sized>(
+ topology: &T,
members: &[String],
local: &str,
k: usize,
@@ -69,7 +70,7 @@ pub(crate) fn neighbours(
all.push(local.to_owned());
topology
.graph(&all, k, &|m: &str| m != local && unreachable(m), stable)
- .remove(local)
+ .and_then(|mut graph| graph.remove(local))
.unwrap_or_default()
}
@@ -88,8 +89,8 @@ impl Topology for RingAndPairs {
k: usize,
unreachable: &dyn Fn(&str) -> bool,
stable: &dyn Fn(&str) -> bool,
- ) -> BTreeMap<String, BTreeSet<String>> {
- graph(members, k, unreachable, stable)
+ ) -> Option<BTreeMap<String, BTreeSet<String>>> {
+ Some(graph(members, k, unreachable, stable))
}
}
diff --git a/src/swarm/topology/mod.rs b/src/swarm/topology/mod.rs
index b5da4d8..d9f361a 100644
--- a/src/swarm/topology/mod.rs
+++ b/src/swarm/topology/mod.rs
@@ -41,26 +41,73 @@ pub(crate) use graph::{RingAndPairs, pair_key, pair_score};
/// How members' neighbours are chosen: the graph above (`RingAndPairs`) is one way;
/// others can be tried against it in the benchmark (`docs/future-work.md`, Swappable
-/// topology and dissemination). One per swarm, as every node must compute the same
-/// graph: the writers' trees are built on it (`member_list_graph`, `swarm::tree`).
+/// topology and dissemination). One per swarm: a node's neighbours must count it as
+/// theirs.
///
-/// A topology each node keeps from its own messages instead (HyParView's views) has no
-/// graph every node shares; it takes more than this (its state, its messages, and a
-/// dissemination that needs no shared graph), and comes with one.
+/// A topology either computes a graph every node shares (`graph`: the same inputs give
+/// every node the same), or keeps its own state from its own messages (HyParView's
+/// views), and has none. What needs the shared graph does nothing without one: writers'
+/// trees (`member_list_graph`, `swarm::tree`), the links members record where it is
+/// wrong (`working_graph`, `swarm::working_links`), indirect probes through a member's
+/// neighbours, the dashboard's map of the intended links (`intended_graph`). A
+/// dissemination that needs none goes with such a topology (PlumTree).
+#[async_trait::async_trait]
pub(crate) trait Topology: Send + Sync + 'static {
/// Its name (in logs, and once chosen by the swarm, in the document).
fn name(&self) -> &'static str;
/// Every reachable member's neighbours among `members` (those on one transport),
/// each keeping about `k`, skipping those `unreachable`, preferring those `stable`
- /// as it does. Pure: the same inputs give every node the same graph.
+ /// as it does. Pure: the same inputs give every node the same graph. None for a
+ /// topology without a shared graph.
fn graph(
&self,
members: &[String],
k: usize,
unreachable: &dyn Fn(&str) -> bool,
stable: &dyn Fn(&str) -> bool,
- ) -> BTreeMap<String, BTreeSet<String>>;
+ ) -> Option<BTreeMap<String, BTreeSet<String>>>;
+
+ /// Our neighbours, from what they are chosen from (`Inputs`), each time those may
+ /// have changed (`refresh`). By default our part of the shared graph, on each of our
+ /// transports (`neighbours_across`); a topology with state of its own keeps them
+ /// there.
+ fn neighbours(&self, local: &str, inputs: &Inputs<'_>) -> BTreeSet<String> {
+ neighbours_across(
+ self,
+ inputs.transports,
+ local,
+ inputs.local_transports,
+ inputs.k,
+ inputs.unreachable,
+ inputs.stable,
+ )
+ }
+
+ /// With every recomputation (`refresh`, at least every 5 s), before `neighbours`:
+ /// what a topology with state of its own does about it (asking members to be
+ /// neighbours, giving up on those that don't answer). By default nothing.
+ async fn maintain(&self, state: &AppState, inputs: &Inputs<'_>) {
+ let _ = (state, inputs);
+ }
+
+ /// Changes whenever the topology's own state changed what `neighbours` gives: the
+ /// neighbours are recomputed then, even though nothing they are chosen from did. By
+ /// default always the same.
+ fn version(&self) -> u64 {
+ 0
+ }
+}
+
+/// What a node's neighbours are chosen from (`Topology::neighbours`): the members, by
+/// the transports each is on (as the member list gives their addresses), ours, the
+/// neighbours each keeps, whom we judge unreachable, who is stable.
+pub(crate) struct Inputs<'a> {
+ pub(crate) transports: &'a BTreeMap<String, BTreeSet<String>>,
+ pub(crate) local_transports: &'a BTreeSet<String>,
+ pub(crate) k: usize,
+ pub(crate) unreachable: &'a (dyn Fn(&str) -> bool + Sync),
+ pub(crate) stable: &'a (dyn Fn(&str) -> bool + Sync),
}
/// This node's current neighbours, and the member list they were computed from.
@@ -81,6 +128,8 @@ pub(crate) struct NeighbourView {
/// they say they sync with us (`refresh`).
outbound: BTreeSet<String>,
neighbours: BTreeSet<String>,
+ /// The topology's version they were computed with (`Topology::version`).
+ version: u64,
/// Every member not judged unreachable is a neighbour (the tests wait for it,
/// `is_full_mesh`). Not the swarm being a full mesh: members on a transport we are
/// not on, or that we never reached, don't count against it.
@@ -88,66 +137,71 @@ pub(crate) struct NeighbourView {
full_mesh: bool,
/// The graph the member list gives, as if every member reached every other
/// (`member_list_graph`): computed when first asked for, kept while the member list
- /// stays the same.
- whole: std::sync::OnceLock<Arc<BTreeMap<String, BTreeSet<String>>>>,
+ /// stays the same. None for a topology without a shared graph.
+ whole: std::sync::OnceLock<Option<Arc<BTreeMap<String, BTreeSet<String>>>>>,
/// Where members recorded their links differ from that graph (`swarm::working_links`),
/// those with a record.
recorded: BTreeMap<String, members::RecordedLinks>,
/// That graph with the links members recorded (`working_graph`): computed when first
/// asked for, kept while the member list and the records stay the same.
- working: std::sync::OnceLock<Arc<BTreeMap<String, BTreeSet<String>>>>,
+ working: std::sync::OnceLock<Option<Arc<BTreeMap<String, BTreeSet<String>>>>>,
}
/// Every member's neighbours as the member list gives them, as if every member reached
/// every other: the same on every node that knows the same member list, whatever each
/// reaches (`swarm::tree` builds writers' trees on it). On each transport among the
-/// members on it, all together. None until the view is known.
+/// members on it, all together. None until the view is known, and for a topology
+/// without a shared graph (`Topology::graph`).
pub(crate) fn member_list_graph(
state: &AppState,
) -> Option<Arc<BTreeMap<String, BTreeSet<String>>>> {
let view = state.neighbours.read_or_recover();
let view = view.as_ref()?;
- Some(Arc::clone(view.whole.get_or_init(|| {
- let mut whole: BTreeMap<String, BTreeSet<String>> = view
- .members
- .iter()
- .map(|member| (member.clone(), BTreeSet::new()))
- .collect();
- let transports: BTreeSet<&String> = view.transports.values().flatten().collect();
- for transport in transports {
- let on_it: Vec<String> = view
- .transports
+ view.whole
+ .get_or_init(|| {
+ let mut whole: BTreeMap<String, BTreeSet<String>> = view
+ .members
.iter()
- .filter(|(_, on)| on.contains(transport))
- .map(|(member, _)| member.clone())
+ .map(|member| (member.clone(), BTreeSet::new()))
.collect();
- let graph = state
- .topology
- .graph(&on_it, view.k, &|_| false, &|m| view.stable.contains(m));
- for (member, neighbours) in graph {
- whole.entry(member).or_default().extend(neighbours);
+ let transports: BTreeSet<&String> = view.transports.values().flatten().collect();
+ for transport in transports {
+ let on_it: Vec<String> = view
+ .transports
+ .iter()
+ .filter(|(_, on)| on.contains(transport))
+ .map(|(member, _)| member.clone())
+ .collect();
+ let graph = state
+ .topology
+ .graph(&on_it, view.k, &|_| false, &|m| view.stable.contains(m))?;
+ for (member, neighbours) in graph {
+ whole.entry(member).or_default().extend(neighbours);
+ }
}
- }
- Arc::new(whole)
- })))
+ Some(Arc::new(whole))
+ })
+ .clone()
}
/// The member list's graph (`member_list_graph`) with the links members recorded
/// (`swarm::working_links`): without those either end recorded as unreached, with those
/// one recorded as reached (`with_recorded`). The same on every node that has the same
/// document; what the writers' trees are built on (`swarm::tree`). None until the view
-/// is known.
+/// is known, and for a topology without a shared graph.
pub(crate) fn working_graph(state: &AppState) -> Option<Arc<BTreeMap<String, BTreeSet<String>>>> {
let whole = member_list_graph(state)?;
let view = state.neighbours.read_or_recover();
let view = view.as_ref()?;
- Some(Arc::clone(view.working.get_or_init(|| {
- if view.recorded.is_empty() {
- whole
- } else {
- Arc::new(with_recorded(&whole, &view.recorded))
- }
- })))
+ view.working
+ .get_or_init(|| {
+ Some(if view.recorded.is_empty() {
+ whole
+ } else {
+ Arc::new(with_recorded(&whole, &view.recorded))
+ })
+ })
+ .clone()
}
/// `graph` with the links members recorded (`recorded`): those one recorded as reached
@@ -303,9 +357,11 @@ pub(crate) fn is_neighbour(state: &AppState, peer: &str) -> bool {
/// they are (with members that only connect out, or aren't listed yet). For a map of
/// the swarm (the dashboard); None until the view is known. A link exists if either end
/// chose it: draw one if either lists the other. About `members²` work: compute it when
-/// the member list may have changed, not on every look.
+/// the member list may have changed, not on every look. None too for a topology without
+/// a shared graph.
#[cfg_attr(not(feature = "dashboard"), allow(dead_code))]
pub(crate) fn intended_graph(state: &AppState) -> Option<BTreeMap<String, BTreeSet<String>>> {
+ member_list_graph(state)?;
let view = state.neighbours.read_or_recover();
let view = view.as_ref()?;
let graph = view
@@ -581,10 +637,22 @@ pub(crate) async fn refresh(state: &AppState) -> Option<NeighbourChange> {
}
}
}
+ let unreachable_of = |m: &str| unreachable.contains(m);
+ let stable_of = |m: &str| stable.contains(m);
+ let inputs = Inputs {
+ transports: &transports,
+ local_transports: &local_transports,
+ k,
+ unreachable: &unreachable_of,
+ stable: &stable_of,
+ };
+ state.topology.maintain(state, &inputs).await;
+ let version = state.topology.version();
let previous = {
let view = state.neighbours.read_or_recover();
if view.as_ref().is_some_and(|view| {
- view.members == current
+ view.version == version
+ && view.members == current
&& view.unlisted == unlisted
&& view.outbound == outbound
&& view.k == k
@@ -626,15 +694,7 @@ pub(crate) async fn refresh(state: &AppState) -> Option<NeighbourChange> {
Some(previous) => previous,
None => state.peer_states.read().await.keys().cloned().collect(),
};
- let mut computed = neighbours_across(
- &*state.topology,
- &transports,
- state.local_id(),
- &local_transports,
- k,
- &|m| unreachable.contains(m),
- &|m| stable.contains(m),
- );
+ let mut computed = state.topology.neighbours(state.local_id(), &inputs);
computed.extend(outbound.iter().cloned());
#[cfg(test)]
let listed_neighbours = computed.len();
@@ -680,6 +740,7 @@ pub(crate) async fn refresh(state: &AppState) -> Option<NeighbourChange> {
unlisted,
outbound,
neighbours: computed,
+ version,
whole,
recorded,
working,