diff options
Diffstat (limited to 'src/state/mod.rs')
| -rw-r--r-- | src/state/mod.rs | 166 |
1 files changed, 113 insertions, 53 deletions
diff --git a/src/state/mod.rs b/src/state/mod.rs index fec164d..aebc59a 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -2,7 +2,6 @@ //! //! * `peer`: what a node knows about each peer (`PeerState`): standing, liveness, //! handshake, incidents. -//! * `session`: one Automerge sync with a peer (`SyncSession`). //! * `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`). @@ -21,13 +20,11 @@ mod host; mod peer; mod read; -mod session; mod timing; pub(crate) use peer::{HelloState, Incidents, Keepalive, Liveness, PeerState, Standing}; pub(crate) use read::{ReadSource, StaleView}; -pub(crate) use session::SyncSession; -pub(crate) use timing::Timing; +pub(crate) use timing::{SYNC_REPLY_TIMEOUT, Timing}; use std::collections::HashMap; use std::sync::Arc; @@ -35,16 +32,17 @@ use std::sync::atomic::AtomicBool; use std::time::Duration; use anyhow::{Result, bail}; -use automerge::AutoCommit; 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::locks::MutexExt; +use crate::document::store::{ChangeSet, StoreError}; +use crate::document::{Document, Transaction}; +use crate::locks::{MutexExt, RwLockExt}; use crate::remake::{Host, RemakeState}; -use crate::swarm::sync::{ChangeSet, SyncEvent}; +use crate::swarm::sync::SyncEvent; use crate::swarm::throughput::ThroughputTable; use crate::transport; use crate::transport::firewall::Firewall; @@ -58,13 +56,12 @@ pub(crate) enum NodeLifecycle { #[derive(Clone)] pub(crate) struct AppState { - /// The document and its epoch. Derefs to `AutoCommit`. + /// The document and its epoch. Derefs to `Document`. pub(crate) doc: Arc<DocumentLock>, - /// 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<Epoch>, - /// `doc`'s epoch label and heads, readable without its lock (`Replica::stamps`). + /// `doc`'s epoch label and heads, readable without its lock (`Replica::stamps`): + /// for cheap reads (handshakes, keepalives, /peers). Only ever changed while holding + /// `doc`'s write lock, together with `doc.epoch`. pub(crate) stamp: watch::Receiver<Stamp>, pub(crate) lifecycle: Arc<RwLock<NodeLifecycle>>, @@ -84,8 +81,10 @@ pub(crate) struct AppState { pub(crate) change_tx: watch::Sender<u64>, pub(crate) initial_sync_done: Arc<AtomicBool>, - /// The node is shutting down: its keepalives say it isn't ready (`peer::farewell`). - pub(crate) stopping: Arc<AtomicBool>, + /// 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<watch::Sender<bool>>, pub(crate) change_events: broadcast::Sender<Arc<ChangeSet>>, @@ -110,8 +109,20 @@ pub(crate) struct AppState { pub(crate) alias: Option<String>, /// 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, + /// What a replay does with writes a newer one superseded (`--superseded-writes`). + pub(crate) superseded_writes: crate::remake::Superseded, + /// 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<std::sync::atomic::AtomicI64>, + /// What the clock checks remember between them (`swarm::skew`). + pub(crate) clock_skew: Arc<std::sync::Mutex<crate::swarm::skew::Watch>>, /// Chunked transfers served and fetched (`swarm::blob`). pub(crate) blobs: Arc<crate::swarm::blob::Blobs>, + /// LINKS asked and answered, for a map of the swarm (`swarm::links`). + pub(crate) links: Arc<crate::swarm::links::Links>, /// HTTP API users who may write protocol data through the store API /// (`--protocol-writers`). pub(crate) protocol_writers: Arc<std::collections::HashSet<String>>, @@ -138,8 +149,16 @@ pub(crate) struct AppState { /// Our neighbours, once known (`swarm::topology`). pub(crate) neighbours: Arc<std::sync::RwLock<Option<crate::swarm::topology::NeighbourView>>>, /// Something the neighbours are computed from may have changed: the member list or a - /// peer's reachability (`swarm::topology::run_worker`). + /// peer's state (`swarm::topology::run_worker`, `swarm::topology::change_peer`). pub(crate) topology_changed: Arc<tokio::sync::Notify>, + /// Something a joining node waits for may have happened: the swarm ID learned, the + /// initial sync done, a rejoin ended, a seed found (`swarm::bootstrap`). + pub(crate) join_progress: Arc<tokio::sync::Notify>, + /// The forget records may have changed (`swarm::changes`): the peer worker acts on + /// them at once instead of at its next periodic check. + pub(crate) forget_records_changed: Arc<AtomicBool>, + /// When the peer worker is next due (`swarm::peer::Due`). + pub(crate) peers_due: Arc<crate::swarm::peer::Due>, /// While rejoining a newer epoch, the previous document, served read-only (and /// marked stale) instead of answering 503. @@ -175,6 +194,29 @@ pub(crate) struct CatchUpSource { } 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() } @@ -198,7 +240,7 @@ impl AppState { /// 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() + self.stamp.borrow().epoch } /// The local epoch and `Replica::epoch_seq`, read together, without waiting for the @@ -242,6 +284,7 @@ impl AppState { discovered_swarm_id: Some(swarm_id), }; debug!("Node transitioned from {old_lifecycle:?} {lifecycle:?}"); + self.join_progress.notify_one(); Ok(()) } NodeLifecycle::Joining { @@ -281,6 +324,8 @@ impl AppState { let old_lifecycle = lifecycle.clone(); *lifecycle = NodeLifecycle::Ready { swarm_id }; debug!("Node transitioned from {old_lifecycle:?} {lifecycle:?}"); + // Dormancy is judged once ready, and the periodic checks run at once. + self.peers_due.now(); } NodeLifecycle::Ready { swarm_id: existing } if existing == &swarm_id => { debug!("Node already ready with the same identity"); @@ -328,6 +373,7 @@ impl AppState { /// State for a peer we haven't known so far (not inserted). pub(crate) fn new_peer_state(&self, pk: &str) -> Arc<Mutex<PeerState>> { let leads = crate::swarm::pair::leads(self.local_id(), pk); + self.peers_due.now(); Arc::new(Mutex::new(PeerState::new(leads, &self.timing))) } @@ -340,7 +386,7 @@ impl AppState { /// Reads the document on the blocking pool; `finish` runs after the lock is released. pub(crate) async fn render<T, R>( &self, - read: impl FnOnce(&AutoCommit) -> T + Send + 'static, + read: impl FnOnce(&Document) -> T + Send + 'static, finish: impl FnOnce(T) -> R + Send + 'static, ) -> R where @@ -358,59 +404,73 @@ impl AppState { .expect("document read task panicked") } - /// Mutates the document on the blocking pool. - pub(crate) async fn write_doc<T>( + /// 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. While a rejoin runs, + /// the write goes into the document before it (`StaleView`), replayed when the + /// rejoin ends; on a node that keeps none, it is refused (`StoreError::Rejoining`, + /// `f` not called). + pub(crate) async fn mutate_doc<T, E>( &self, - f: impl FnOnce(&mut AutoCommit) -> T + Send + 'static, - ) -> T + f: impl FnOnce(&mut Transaction<'_>) -> Result<T, E> + Send + 'static, + ) -> Result<T, E> where T: Send + 'static, + E: From<StoreError> + Send + 'static, { - let doc = Arc::clone(&self.doc); - tokio::task::spawn_blocking(move || { - let mut replica = doc.blocking_write(); - let out = f(&mut replica.doc); - replica.refresh_heads(); - out - }) - .await - .expect("document write task panicked") + self.mutate_doc_for(None, f).await.map(|(out, _)| out) } - /// Mutates the document on the blocking pool as one Automerge change, then - /// publishes it. Write and notification run in the same blocking task, so a - /// cancelled request can't leave a committed write unannounced. - pub(crate) async fn mutate_doc<T, E>( + /// [`mutate_doc`](Self::mutate_doc) for `user`, who wrote through the API: the + /// change names them (`replica::Writer`). With the heads of the document written to + /// after the write (sorted): what a read must have to include it. + pub(crate) async fn mutate_doc_for<T, E>( &self, - f: impl FnOnce(&mut AutoCommit) -> Result<T, E> + Send + 'static, - ) -> Result<T, E> + user: Option<String>, + f: impl FnOnce(&mut Transaction<'_>) -> Result<T, E> + Send + 'static, + ) -> Result<(T, Vec<crate::document::ChangeHash>), E> where T: Send + 'static, - E: Send + 'static, + E: From<StoreError> + Send + 'static, { let state = self.clone(); tokio::task::spawn_blocking(move || { let result = { let mut replica = state.doc.blocking_write(); - let result = f(&mut replica.doc); - match &result { - // One API write = one Automerge change. - Ok(_) => { - // Stamped with our clock (milliseconds), so receivers can tell - // how long the change took to reach them (`swarm::stats`). - replica.doc.commit_with( - automerge::transaction::CommitOptions::default() - .with_time(state.clock.now_millis().as_u64() as i64), - ); - } - // Discard partial ops (e.g. old value deleted, new one not written). - Err(_) => { - replica.doc.rollback(); - } + // 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 { + // Rejoining: into the document before the rejoin, which the rejoin + // replays when it ends (`StaleView`); refused by a node that keeps + // none (it wasn't ready). + let Some((_, stale)) = state.stale_document() else { + return Err(E::from(StoreError::Rejoining)); + }; + let mut doc = stale.write_or_recover(); + let writer = crate::document::replica::Writer { + node: state.local_id(), + user: user.as_deref(), + }; + let out = crate::document::replica::transact( + &mut doc, + state.clock.now_millis(), + &writer, + f, + )?; + let mut heads = doc.get_heads(); + heads.sort_unstable(); + return Ok((out, heads)); } + // One API write = one change; on error, its partial writes (e.g. the old + // value deleted, the new one not written) are dropped. + let writer = crate::document::replica::Writer { + node: state.local_id(), + user: user.as_deref(), + }; + let result = replica.transact(state.clock.now_millis(), &writer, f); replica.refresh_heads(); - result + result.map(|out| (out, replica.heads().to_vec())) }; // lock released before notifying if result.is_ok() { |
