aboutsummaryrefslogtreecommitdiffstats
path: root/src/swarm/reach.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/swarm/reach.rs')
-rw-r--r--src/swarm/reach.rs119
1 files changed, 119 insertions, 0 deletions
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<Vec<u64>>,
+}
+
+/// The pair keys of the members we reach now, sorted: those with an active session.
+async fn measure(state: &AppState) -> Vec<u64> {
+ 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<u64> {
+ 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<u64>)>) -> Option<Vec<u64>> {
+ 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<u64, String> {
+ let replica = state.doc.read().await;
+ members::members(&replica.doc)
+ .into_iter()
+ .map(|member| (topology::pair_key(&member), member))
+ .collect()
+}