//! 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) that answer //! (`PeerState::answer_overdue`: none left a keepalive or sync message unanswered for //! `Timing::unreached_after`, asked again halfway, with nothing else heard from it; a //! member gone shows within seconds, not after `dormant_after`). 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 to every neighbour when it changed (the peer worker looks //! every 5 s, and at once when a member we reach stops answering), 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 the session lasts (a //! neighbour that goes dormant, or starts anew, says it again). Not for a set time: //! while a pair exchanges changes it sends no keepalives, and so no STATUS, for as //! long as the writes go on. 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 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 /// that answer. async fn measure(state: &AppState) -> Vec { let now = Instant::now(); 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() && !peer_state.answer_overdue(now) && 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), and at once when a member we said /// we reach stops answering (`reached_stops_answering`): whom we reach now; if it changed, /// our neighbours are told at once (a STATUS each, unanswered unless they lack some of /// our heads). 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"); } } } /// Whether `peer`, whose answer is overdue (`PeerState::answer_overdue`), is one we /// said we reach: then the peer worker checks at once (`check`). pub(crate) fn reached_stops_answering(state: &AppState, peer: &str) -> bool { state .reach .lock_or_recover() .published .as_ref() .is_some_and(|keys| keys.contains(&topology::pair_key(peer))) } /// A STATUS to `peer` with whom we reach: as an announcement (unanswered unless it lacks /// some of our heads), but with `reaches`. async fn tell(state: &AppState, peer: &str) -> anyhow::Result<()> { announce_heads(state, peer, true).await } /// The members a peer reaches, as it last said in this session (`said`, kept with its /// state, cleared when the session ends: `PeerState::reset_sync`); by pair key. pub(crate) fn of(said: Option<&(Instant, Vec)>) -> Option> { said.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() }