diff options
Diffstat (limited to 'src/swarm')
| -rw-r--r-- | src/swarm/links.rs | 177 | ||||
| -rw-r--r-- | src/swarm/mod.rs | 1 | ||||
| -rw-r--r-- | src/swarm/peer/inbound.rs | 6 |
3 files changed, 184 insertions, 0 deletions
diff --git a/src/swarm/links.rs b/src/swarm/links.rs new file mode 100644 index 0000000..a12a1de --- /dev/null +++ b/src/swarm/links.rs @@ -0,0 +1,177 @@ +//! How members are linked, for a map of the swarm (the dashboard, `api::dashboard`): a +//! node asks a member with LINKS, and the member answers with a LINKS_REPLY listing its +//! neighbours, each with the transport its messages go over, whether only through a +//! relay, the round trip and whether the neighbour is dormant: as the member measures +//! them, which no other node can. +//! +//! Every node answers, whatever its build: one with a dashboard asks any member, which +//! may have none. Only members (in the member list) are answered, as the links name +//! members and where they're reached; and each at most every `ANSWER_EVERY`. Nothing +//! is written to the document: asked for while someone looks, forgotten after. + +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::Duration; + +use tokio::time::Instant; +use tracing::debug; + +use crate::locks::MutexExt; +use crate::state::AppState; +use crate::swarm::{members, topology}; +use crate::transport::{Connection, Link, LinksReply, LinksRequest, ParsedMessage}; + +/// Links one reply lists at most (a member has about `--neighbours`, more if others +/// prefer it). +pub(crate) const MAX_LINKS: usize = 256; +/// A member answers each other one at most this often. +const ANSWER_EVERY: Duration = Duration::from_secs(5); +/// Replies kept from members at most (one each), and requests out. +const MAX_KEPT: usize = 4096; + +/// The requests out and the replies in, and when each asker was last answered. +#[derive(Default)] +pub(crate) struct Links { + inner: Mutex<Inner>, +} + +#[derive(Default)] +struct Inner { + /// Our LINKS out, by member: its nonce. + asked: HashMap<String, u64>, + /// The latest LINKS_REPLY of each member: when, and its links. + replies: HashMap<String, (Instant, Vec<Link>)>, + /// When we last answered each member. + answered: HashMap<String, Instant>, +} + +impl Links { + /// The latest links each member replied with, and how long ago. + #[cfg_attr(not(feature = "dashboard"), allow(dead_code))] + pub(crate) fn replies(&self) -> Vec<(String, Duration, Vec<Link>)> { + let now = Instant::now(); + self.inner + .lock_or_recover() + .replies + .iter() + .map(|(member, (at, links))| { + ( + member.clone(), + now.saturating_duration_since(*at), + links.clone(), + ) + }) + .collect() + } +} + +/// This node's links: its neighbours, as it measures them. +pub(crate) async fn own_links(state: &AppState) -> Vec<Link> { + let mut links = Vec::new(); + for (peer, peer_state) in state.peers_snapshot().await { + if !topology::is_neighbour(state, &peer) { + continue; + } + let dormant = peer_state.lock().await.liveness.is_dormant(); + let destination = state.transport.destination(&peer); + links.push(Link { + transport: destination.as_ref().and_then(|destination| { + destination + .address + .split_once(':') + .map(|(scheme, _)| scheme.to_owned()) + }), + relayed: destination + .as_ref() + .is_some_and(|destination| destination.connection == Some(Connection::Relayed)), + rtt_us: state + .transport + .rtt(&peer) + .map(|rtt| u32::try_from(rtt.as_micros()).unwrap_or(u32::MAX)), + dormant, + peer, + }); + } + links.sort_by(|a, b| a.peer.cmp(&b.peer)); + links.truncate(MAX_LINKS); + links +} + +/// Asks `member` for its links (a LINKS); its reply lands in `AppState::links`. +#[cfg_attr(not(feature = "dashboard"), allow(dead_code))] +pub(crate) async fn ask(state: &AppState, member: &str) { + let nonce = crate::transport::paths::random_nonce(); + { + let mut inner = state.links.inner.lock_or_recover(); + if inner.asked.len() >= MAX_KEPT && !inner.asked.contains_key(member) { + inner.asked.clear(); + } + inner.asked.insert(member.to_owned(), nonce); + } + let (epoch, epoch_seq) = state.epoch_stamp(); + let request = ParsedMessage::Links(LinksRequest { + epoch, + epoch_seq, + nonce, + }); + if let Err(error) = state.transport.send_message(member, &request).await { + debug!(member = %member, error = %format!("{error:#}"), "LINKS failed"); + } +} + +/// A LINKS from `peer`: answered if it's a member, and not answered lately. +pub(crate) async fn answer(state: &AppState, peer: &str, request: LinksRequest) { + let member = { + let replica = state.doc.read().await; + members::members(&replica.doc) + .iter() + .any(|member| member == peer) + }; + if !member { + debug!(peer = %peer, "Not answering LINKS from a peer that isn't a member"); + return; + } + { + let now = Instant::now(); + let mut inner = state.links.inner.lock_or_recover(); + if inner + .answered + .get(peer) + .is_some_and(|at| now.saturating_duration_since(*at) < ANSWER_EVERY) + { + return; + } + if inner.answered.len() >= MAX_KEPT { + inner + .answered + .retain(|_, at| now.saturating_duration_since(*at) < ANSWER_EVERY); + } + inner.answered.insert(peer.to_owned(), now); + } + let (epoch, epoch_seq) = state.epoch_stamp(); + let reply = ParsedMessage::LinksReply(LinksReply { + epoch, + epoch_seq, + nonce: request.nonce, + links: own_links(state).await, + }); + if let Err(error) = state.transport.send_message(peer, &reply).await { + debug!(peer = %peer, error = %format!("{error:#}"), "LINKS_REPLY failed"); + } +} + +/// A LINKS_REPLY from `peer`: kept if it answers our LINKS to it. +pub(crate) fn received(state: &AppState, peer: &str, mut reply: LinksReply) { + let mut inner = state.links.inner.lock_or_recover(); + if inner.asked.get(peer) != Some(&reply.nonce) { + return; + } + inner.asked.remove(peer); + reply.links.truncate(MAX_LINKS); + if inner.replies.len() >= MAX_KEPT && !inner.replies.contains_key(peer) { + inner.replies.clear(); + } + inner + .replies + .insert(peer.to_owned(), (Instant::now(), reply.links)); +} diff --git a/src/swarm/mod.rs b/src/swarm/mod.rs index d424529..7fb8e16 100644 --- a/src/swarm/mod.rs +++ b/src/swarm/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod auth; pub(crate) mod blob; pub(crate) mod bootstrap; +pub(crate) mod links; pub(crate) mod members; pub(crate) mod pair; pub(crate) mod peer; diff --git a/src/swarm/peer/inbound.rs b/src/swarm/peer/inbound.rs index 6a149a2..d4df6c7 100644 --- a/src/swarm/peer/inbound.rs +++ b/src/swarm/peer/inbound.rs @@ -160,6 +160,12 @@ pub(crate) async fn handle_message( transport::ParsedMessage::StatusReply(remote) => { on_status(state, &peer, &remote, reactivated).await; } + transport::ParsedMessage::Links(request) => { + crate::swarm::links::answer(state, &peer, request).await; + } + transport::ParsedMessage::LinksReply(reply) => { + crate::swarm::links::received(state, &peer, reply); + } transport::ParsedMessage::Who(_) | transport::ParsedMessage::Me(_) | transport::ParsedMessage::Ping(_) |
