From 5581992d5603322d7f02bd76625334be3f8a2b18 Mon Sep 17 00:00:00 2001 From: tv Date: Tue, 3 Nov 2026 04:44:49 +0100 Subject: measure 2-hop neighbourhood --- src/swarm/reach.rs | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/swarm/reach.rs (limited to 'src/swarm/reach.rs') diff --git a/src/swarm/reach.rs b/src/swarm/reach.rs new file mode 100644 index 0000000..17b189d --- /dev/null +++ b/src/swarm/reach.rs @@ -0,0 +1,119 @@ +//! Whom each neighbour reaches: a node's 2-hop neighbourhood, as measured, not as the +//! member list says. +//! +//! A node knows its own links, and from them whom it reaches: the members it has an +//! active session with (handshake done, neither dormant nor unreachable). It tells its +//! neighbours so in its STATUS (`Status::reaches`, each member by its pair key, +//! `topology::pair_key`, 8 bytes instead of a node ID): in every keepalive STATUS and +//! STATUS_REPLY, and soon (the peer worker looks every 5 s) to every neighbour when it +//! changed, +//! but not in announcements of heads, which go often. So every node knows whom its +//! neighbours reach, as of their latest STATUS: for as long as that is fresh +//! (`held_for`), then not, until the next. Nothing goes into the document, and nobody +//! knows more than two hops out. +//! +//! What it is for: passing changes on around links that don't work, and to exactly the +//! members the sender doesn't reach; a member a neighbour reaches is one worth trying, +//! and a way to it. Not for choosing multipoint relays (as OLSR does): over the member +//! list's graph, which is close to random, neighbourhoods hardly overlap, and nearly +//! every neighbour would be one (13.7 of 13.7 at 250 members and more), saving next to +//! nothing but in a full mesh. + +use std::collections::HashMap; +use std::time::Duration; + +use tokio::time::Instant; +use tracing::debug; + +use crate::locks::MutexExt; +use crate::state::{AppState, HelloState, Standing}; +use crate::swarm::peer::announce_heads; +use crate::swarm::{members, topology}; + +/// Whom we reach, as last told our neighbours. +#[derive(Default)] +pub(crate) struct Reach { + published: Option>, +} + +/// The pair keys of the members we reach now, sorted: those with an active session. +async fn measure(state: &AppState) -> Vec { + let mut reached = Vec::new(); + for (peer, peer_state) in state.peers_snapshot().await { + let peer_state = peer_state.lock().await; + if peer_state.hello_state == HelloState::Established + && !peer_state.liveness.is_dormant() + && !peer_state.is_unreachable() + && matches!(peer_state.standing, Standing::Member { .. }) + { + reached.push(topology::pair_key(&peer)); + } + } + reached.sort_unstable(); + reached +} + +/// Whom we reach, as we tell it (`Status::reaches`); empty until first measured. +pub(crate) fn published(state: &AppState) -> Vec { + state + .reach + .lock_or_recover() + .published + .clone() + .unwrap_or_default() +} + +/// With the peer worker's periodic pass (every 5 s): whom we reach now; if it changed, +/// our neighbours are told at once (a STATUS each, unanswered if the heads are the +/// same). A change that is undone within a pass isn't told. +pub(crate) async fn check(state: &AppState) { + let now = measure(state).await; + { + let mut reach = state.reach.lock_or_recover(); + if reach.published.as_ref() == Some(&now) { + return; + } + reach.published = Some(now.clone()); + } + debug!( + reached = now.len(), + "Whom we reach changed; telling our neighbours" + ); + for (peer, peer_state) in state.peers_snapshot().await { + let established = peer_state.lock().await.hello_state == HelloState::Established; + if !established || !topology::is_neighbour(state, &peer) { + continue; + } + if let Err(error) = tell(state, &peer).await { + debug!(peer = %peer, error = %format!("{error:#}"), "Telling whom we reach failed"); + } + } +} + +/// A STATUS to `peer` with whom we reach: as an announcement (unanswered if the heads +/// are the same), but with `reaches`. +async fn tell(state: &AppState, peer: &str) -> anyhow::Result<()> { + announce_heads(state, peer, true).await +} + +/// How long what a neighbour said it reaches holds: past two of its regular STATUSes, +/// each of which says it again. +pub(crate) fn held_for(state: &AppState) -> Duration { + state.timing.status_refresh * 2 + state.timing.keepalive +} + +/// The members `peer` reaches, as it last said, while that holds (`held_for`); by +/// pair key. +pub(crate) fn of(state: &AppState, said: Option<&(Instant, Vec)>) -> Option> { + said.filter(|(at, _)| at.elapsed() < held_for(state)) + .map(|(_, reaches)| reaches.clone()) +} + +/// The members of our member list by pair key, to name what neighbours say they reach. +pub(crate) async fn names(state: &AppState) -> HashMap { + let replica = state.doc.read().await; + members::members(&replica.doc) + .into_iter() + .map(|member| (topology::pair_key(&member), member)) + .collect() +} -- cgit v1.3.1