//! Shared node state: the document, lifecycle, per-peer state and timing. //! //! * `peer`: what a node knows about each peer (`PeerState`): standing, liveness, //! handshake, incidents. //! * `timing`: the intervals the protocol runs on (`Timing`). //! * `read`: serving reads, live or from the pre-rejoin copy. //! * `host`: the node as the remake module sees it (`remake::Host`). //! //! **Lock order.** Where several of these locks are held at once, they are taken in this //! order: `doc` (the replica), `peer_states` (the map), a peer's `PeerState`, then //! `lifecycle`. Sync generation and apply take doc → peer; an epoch switch doc → map → //! each peer; establishing a peer peer → lifecycle. Waiting for a peer while holding the //! map makes everything that needs the map wait for that peer, so apart from the switch //! (which holds the map only for reading), code that walks the peers copies the map //! first (`peers_snapshot`), and the one place that holds the map while looking at a //! peer only tries its lock (`swarm::peer::forget_unless_alive`). The std mutexes //! (statistics, round-trip times and the like) are held only briefly, never across an //! await. mod host; mod peer; mod read; mod timing; pub(crate) use peer::{HelloState, Incidents, Keepalive, Liveness, PeerState, Standing}; pub(crate) use read::{ReadSource, StaleView}; pub(crate) use timing::Timing; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::time::Duration; use anyhow::{Result, bail}; use tokio::sync::{Mutex, RwLock, broadcast, mpsc, watch}; use tokio::time::Instant; use tracing::{debug, error, warn}; use crate::document::lock::DocumentLock; use crate::document::replica::{Epoch, Stamp}; use crate::document::store::{ChangeSet, StoreError}; use crate::document::{Document, Transaction}; use crate::locks::MutexExt; use crate::remake::{Host, RemakeState}; use crate::swarm::sync::SyncEvent; use crate::swarm::throughput::ThroughputTable; use crate::transport; use crate::transport::firewall::Firewall; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum NodeLifecycle { // TODO maybe rename discovered_swarm_id to swarm_id if that doesn't impair readability. Joining { discovered_swarm_id: Option }, Ready { swarm_id: String }, } #[derive(Clone)] pub(crate) struct AppState { /// The document and its epoch. Derefs to `Document`. pub(crate) doc: Arc, /// Copy of `doc.epoch` for cheap, lock-free reads (handshakes, keepalives, /peers). /// Only ever changed while holding `doc`'s write lock, together with `doc.epoch`. pub(crate) epoch_tx: watch::Sender, /// `doc`'s epoch label and heads, readable without its lock (`Replica::stamps`). pub(crate) stamp: watch::Receiver, pub(crate) lifecycle: Arc>, pub(crate) peer_states: Arc>>>>, pub(crate) transport: Arc, pub(crate) firewall: Arc, /// Messages and bytes exchanged, per peer (`transport::traffic`). pub(crate) traffic: Arc, /// Node-wide figures for /admin/stats (`swarm::stats`). pub(crate) stats: Arc, pub(crate) sync_events: mpsc::UnboundedSender, /// The document's version: bumped after every change (`notify_store_changed`). The /// change publisher (`swarm::sync`) waits on it; a measurement compares it. pub(crate) change_tx: watch::Sender, pub(crate) initial_sync_done: Arc, /// True once the node shuts down (a signal, a failed join, a transport gone): the /// HTTP API drains and its long-lived responses end, and keepalives say the node /// isn't ready and is leaving (`peer::farewell`). pub(crate) closing: Arc>, pub(crate) change_events: broadcast::Sender>, pub(crate) timing: Timing, /// Per-node remake state (prebuilt snapshots, switch status, archive). pub(crate) remake: Arc, /// Pre-shared key every member must prove in the handshake; None admits any node /// that reaches us. pub(crate) swarm_key: Option>, /// Random per run of this node; our HELLOs carry it and replies must echo it. pub(crate) run_id: transport::RunId, /// Handshakes this node started in this run, with any peer (`HelloRequest::handshake`). pub(crate) handshakes: Arc, /// Target number of neighbours (`--neighbours`); up to this many + 1 members the /// swarm is a full mesh. pub(crate) neighbours_k: usize, /// Our alias (`--alias`), published in the member list (`swarm::members`). pub(crate) alias: Option, /// Members alive within this window vote on remakes (`--electorate-window`). pub(crate) electorate_window: Duration, /// This node's clock is off when it differs from most of its peers' by more than /// this (`--clock-skew-warn`, `swarm::skew`); 0: never. pub(crate) clock_skew_warn: Duration, /// How far this node's clock is ahead of its peers' (their median, `swarm::skew`), as /// last measured; 0 before. The propagation figures take it off. pub(crate) clock_ahead_ms: Arc, /// What the clock checks remember between them (`swarm::skew`). pub(crate) clock_skew: Arc>, /// Chunked transfers served and fetched (`swarm::blob`). pub(crate) blobs: Arc, /// LINKS asked and answered, for a map of the swarm (`swarm::links`). pub(crate) links: Arc, /// HTTP API users who may write protocol data through the store API /// (`--protocol-writers`). pub(crate) protocol_writers: Arc>, /// This node's own files, and how much it read and wrote them. pub(crate) files: Arc, /// This node's document on disk (`--document-file`), if configured. pub(crate) document_file: Option>, /// What this node remembers across restarts (`--state-file`), if configured. pub(crate) state_file: Option>, /// The effective configuration with sources, imported values and secrets left out /// (`config::Loaded::report`), for `GET /admin/config`. pub(crate) config: Arc, /// Our own connectivity record and stability tier (`swarm::stability`). pub(crate) stability: Arc>, /// Round-trip times to peers (`swarm::rtt`). pub(crate) rtt: Arc>, /// Sync throughput with peers (`swarm::throughput`). pub(crate) throughput: Arc>, /// Pulls of changes from neighbours (`swarm::relay`). pub(crate) pulling: Arc>, /// An announcement round is scheduled, for changes from these peers /// (`swarm::relay`). pub(crate) announce_pending: Arc>>>, /// Our neighbours, once known (`swarm::topology`). pub(crate) neighbours: Arc>>, /// Something the neighbours are computed from may have changed: the member list or a /// peer's state (`swarm::topology::run_worker`, `swarm::topology::change_peer`). pub(crate) topology_changed: Arc, /// While rejoining a newer epoch, the previous document, served read-only (and /// marked stale) instead of answering 503. pub(crate) stale: Arc>>, /// While the initial sync isn't done: the one peer we catch up from. pub(crate) catch_up_source: Arc>>, /// Since when a closer peer has had a head start to become the source. pub(crate) catch_up_preference: Arc>>, /// A catch-up source given up for not answering our sync messages, and until when it /// isn't chosen again (`swarm::source`). pub(crate) catch_up_avoided: Arc>>, /// The node's wall clock (`clock`). pub(crate) clock: Arc, } /// The peer a catching-up node syncs with first (`swarm::source`). #[derive(Clone, Debug)] pub(crate) struct CatchUpSource { pub(crate) peer: String, /// When it was chosen or last sent us anything. pub(crate) last_heard: Instant, /// When it was chosen or last sent us a sync message: progress, not just liveness. pub(crate) last_sync: Instant, /// Our sync messages to it in a row whose answer didn't come in time. pub(crate) reply_timeouts: u32, /// When we last warned that catching up from it stalls. pub(crate) last_warned: Option, /// Our last probe STATUS to it. pub(crate) last_probe: Option, /// Its keepalives in a row saying it wouldn't sync with us. pub(crate) refusals: u32, } impl AppState { /// Asks the sync worker to look at `peer` (`SyncEvent::PeerNeedsSync`): whether to /// send it a sync message now. Nothing if the worker is gone (the node stops). pub(crate) fn request_sync(&self, peer: &str) { let _ = self .sync_events .send(SyncEvent::PeerNeedsSync(peer.to_owned())); } /// `request_sync`, after `delay`. pub(crate) fn request_sync_after(&self, peer: &str, delay: std::time::Duration) { let events = self.sync_events.clone(); let peer = peer.to_owned(); tokio::spawn(async move { tokio::time::sleep(delay).await; let _ = events.send(SyncEvent::PeerNeedsSync(peer)); }); } /// Whether the node shuts down (`closing`). pub(crate) fn is_closing(&self) -> bool { *self.closing.borrow() } pub(crate) fn local_id(&self) -> &str { self.transport.local_id() } /// Peers that can serve our epoch's data: active, ready members on our epoch /// (`PeerSummary::serves_epoch`). pub(crate) async fn active_peers_on_local_epoch(&self) -> Vec { let epoch = self.local_epoch(); self.peer_summaries() .await .into_iter() .filter(|peer| peer.serves_epoch(epoch)) .map(|peer| peer.id) .collect() } pub(crate) fn lock_rtt(&self) -> std::sync::MutexGuard<'_, crate::swarm::rtt::RttTable> { self.rtt.lock_or_recover() } /// The local epoch. Code that labels or applies sync messages must use the epoch /// of the locked `Replica` instead, so label and document can't disagree. pub(crate) fn local_epoch(&self) -> Epoch { *self.epoch_tx.borrow() } /// The local epoch and `Replica::epoch_seq`, read together, without waiting for the /// document's lock. pub(crate) fn epoch_stamp(&self) -> (Epoch, u32) { let stamp = self.stamp.borrow(); (stamp.epoch, stamp.epoch_seq) } pub(crate) async fn is_ready(&self) -> bool { matches!(*self.lifecycle.read().await, NodeLifecycle::Ready { .. }) } pub(crate) async fn swarm_id(&self) -> Option { match &*self.lifecycle.read().await { NodeLifecycle::Ready { swarm_id } => Some(swarm_id.clone()), NodeLifecycle::Joining { discovered_swarm_id, } => discovered_swarm_id.clone(), } } // Record the expected swarm without making the node Ready. Bootstrap owns readiness. pub(crate) async fn set_expected_swarm_id(&self, swarm_id: String) -> Result<()> { let mut lifecycle = self.lifecycle.write().await; match &*lifecycle { NodeLifecycle::Joining { discovered_swarm_id: Some(existing), } if existing != &swarm_id => { bail!( "Conflicting expected swarm IDs: expected {}, received {}", existing, swarm_id ); } NodeLifecycle::Joining { discovered_swarm_id: None, } => { let old_lifecycle = lifecycle.clone(); *lifecycle = NodeLifecycle::Joining { discovered_swarm_id: Some(swarm_id), }; debug!("Node transitioned from {old_lifecycle:?} {lifecycle:?}"); Ok(()) } NodeLifecycle::Joining { discovered_swarm_id: Some(_), } => { warn!("Node already Joining with the same identity"); Ok(()) } NodeLifecycle::Ready { swarm_id: existing } if existing == &swarm_id => { warn!("Node already Ready with the same identity"); Ok(()) } NodeLifecycle::Ready { swarm_id: existing } => { bail!( "Cannot change established swarm ID: existing {}, received {}", existing, swarm_id ); } } } // Only bootstrap should call this, after validating document metadata. pub(crate) async fn mark_ready(&self, swarm_id: String) -> Result<()> { let mut lifecycle = self.lifecycle.write().await; match &*lifecycle { NodeLifecycle::Joining { discovered_swarm_id: Some(expected), } if expected != &swarm_id => { bail!( "Cannot mark node ready: expected swarm {}, received {}", expected, swarm_id ); } NodeLifecycle::Joining { .. } => { let old_lifecycle = lifecycle.clone(); *lifecycle = NodeLifecycle::Ready { swarm_id }; debug!("Node transitioned from {old_lifecycle:?} {lifecycle:?}"); } NodeLifecycle::Ready { swarm_id: existing } if existing == &swarm_id => { debug!("Node already ready with the same identity"); } NodeLifecycle::Ready { swarm_id: existing } => { bail!( "Cannot change established swarm ID: existing {}, received {}", existing, swarm_id ); } } Ok(()) } /// Every known peer with its state, copied out of the peer map. Lock a peer's state /// only from such a copy, never while holding the map's lock: the map would then /// wait for the peer, and with it everything that only needs the map (a lookup, a /// new peer). pub(crate) async fn peers_snapshot(&self) -> Vec<(String, Arc>)> { self.peer_states .read() .await .iter() .map(|(peer, state)| (peer.clone(), Arc::clone(state))) .collect() } /// Looks up a peer without creating it. Background work (sync, retries, /// keepalives) must use this so a forgotten peer isn't resurrected. pub(crate) async fn get_peer_state(&self, pk: &str) -> Option>> { self.peer_states.read().await.get(pk).cloned() } pub(crate) async fn get_or_create_peer_state(&self, pk: &str) -> Arc> { let mut peers = self.peer_states.write().await; Arc::clone( peers .entry(pk.to_owned()) .or_insert_with(|| self.new_peer_state(pk)), ) } /// State for a peer we haven't known so far (not inserted). pub(crate) fn new_peer_state(&self, pk: &str) -> Arc> { let leads = crate::swarm::pair::leads(self.local_id(), pk); Arc::new(Mutex::new(PeerState::new(leads, &self.timing))) } pub(crate) fn notify_store_changed(&self) { self.change_tx.send_modify(|version| { *version = version.wrapping_add(1); }); } /// Reads the document on the blocking pool; `finish` runs after the lock is released. pub(crate) async fn render( &self, read: impl FnOnce(&Document) -> T + Send + 'static, finish: impl FnOnce(T) -> R + Send + 'static, ) -> R where R: Send + 'static, { let doc = Arc::clone(&self.doc); tokio::task::spawn_blocking(move || { let data = { let guard = doc.blocking_read(); read(&guard.doc) }; finish(data) }) .await .expect("document read task panicked") } /// Mutates the document on the blocking pool as one change, then /// publishes it. Write and notification run in the same blocking task, so a /// cancelled request can't leave a committed write unannounced. Refused /// (`StoreError::Rejoining`, `f` not called) while the document isn't writable. pub(crate) async fn mutate_doc( &self, f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, ) -> Result where T: Send + 'static, E: From + Send + 'static, { let state = self.clone(); tokio::task::spawn_blocking(move || { let result = { let mut replica = state.doc.blocking_write(); // Checked here, under the lock: the caller's readiness check (if any) // was before a rejoin could swap in a fresh document. if !replica.writable { return Err(E::from(StoreError::Rejoining)); } // One API write = one change; on error, its partial writes (e.g. the old // value deleted, the new one not written) are dropped. let result = replica.transact(state.clock.now_millis(), f); replica.refresh_heads(); result }; // lock released before notifying if result.is_ok() { state.publish_change(); } result }) .await .expect("document write task panicked") } pub(crate) fn publish_change(&self) { self.notify_store_changed(); if let Err(error) = self.sync_events.send(SyncEvent::DocumentChanged) { // The write is committed; failing the request would misreport it. error!(error = %error, "Sync worker is gone; change will not be replicated"); } } }