aboutsummaryrefslogtreecommitdiffstats
path: root/src/swarm
diff options
context:
space:
mode:
Diffstat (limited to 'src/swarm')
-rw-r--r--src/swarm/links.rs177
-rw-r--r--src/swarm/mod.rs1
-rw-r--r--src/swarm/peer/inbound.rs6
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(_)