diff options
Diffstat (limited to 'src/swarm/bootstrap.rs')
| -rw-r--r-- | src/swarm/bootstrap.rs | 167 |
1 files changed, 145 insertions, 22 deletions
diff --git a/src/swarm/bootstrap.rs b/src/swarm/bootstrap.rs index f41a00b..bd15a34 100644 --- a/src/swarm/bootstrap.rs +++ b/src/swarm/bootstrap.rs @@ -24,9 +24,11 @@ use tokio::time::Instant; use tracing::{debug, info, warn}; use uuid::Uuid; +use crate::document; use crate::document::replica::Epoch; use crate::document::store; use crate::files::{FileKind, Files}; +use crate::force::Force; use crate::remake::snapshot::{self, META_ROOT, SWARM}; use crate::state::AppState; use crate::swarm::peer::{remember_peer, send_hello_once}; @@ -45,16 +47,21 @@ pub(crate) enum Start { Join, /// Start a swarm, empty (`--genesis`). Genesis, - /// Start a swarm from a backup already loaded as the document (`--recover-from`). - Recovered, + /// Start on a document already loaded as the replica (`StartingDocument`). + FromDocument(Origin), } pub(crate) async fn bootstrap(state: &AppState, seeds: &[String], start: Start) -> Result<()> { - if start == Start::Recovered { + if let Start::FromDocument(origin) = start { let Some(swarm_id) = state.swarm_id().await else { - bail!("recovering without a swarm ID"); + bail!("starting on a document without a swarm ID"); }; - info!(swarm_id = %swarm_id, epoch = %state.local_epoch(), "Recovered the swarm from a backup"); + info!( + swarm_id = %swarm_id, + epoch = %state.local_epoch(), + "Starting on {}, ready at once", + origin.describe() + ); state.initial_sync_done.store(true, Ordering::Release); state.mark_ready(swarm_id).await?; // Members of the swarm still running, if any: they merge with this node, or it @@ -143,38 +150,154 @@ fn read_swarm_id(doc: &AutoCommit) -> Option<String> { } } -/// A backup to recover from (`--recover-from`). -pub(crate) struct Backup { +/// Where a document to start from came from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Origin { + /// A backup to recover from (`--recover-from`). + Backup, + /// This node's document file (`--document-file`). + DocumentFile, +} + +impl Origin { + fn describe(self) -> &'static str { + match self { + Origin::Backup => "the backup", + Origin::DocumentFile => "the document file", + } + } +} + +/// A document the node starts from, ready at once: on the swarm and epoch it names. +pub(crate) struct StartingDocument { /// Under a fresh actor. pub(crate) doc: AutoCommit, pub(crate) epoch: Epoch, pub(crate) swarm_id: String, + pub(crate) origin: Origin, + /// The actors this node wrote with before (from the document file). + pub(crate) earlier_actors: Vec<ActorId>, } -/// Loads a backup to recover from, as `GET /admin/document` saves it. -pub(crate) fn load_backup(path: &Path, files: &Files) -> Result<Backup> { - let bytes = files - .read(FileKind::Backup, path) - .with_context(|| format!("Reading the backup {}", path.display()))? - .with_context(|| format!("The backup {} doesn't exist", path.display()))?; - let mut doc = AutoCommit::load(&bytes) - .with_context(|| format!("Loading the backup {}", path.display()))? - .with_actor(ActorId::random()); - let Some(swarm_id) = read_swarm_id(&doc) else { +/// What `--recover-from` and `--document-file` say about where to start. +pub(crate) struct StartOptions<'a> { + pub(crate) recover_from: Option<&'a Path>, + pub(crate) document_file: Option<&'a Path>, + pub(crate) genesis: bool, + /// The swarm the state file remembers. + pub(crate) remembered: Option<&'a str>, +} + +/// The document to start from, if any, checked against the other options. Conflicts +/// are errors unless forced (`force`): +/// * an unreadable document file (`badDocumentFile`: moved aside, start without it); +/// * a backup to recover from while the document file holds a document +/// (`replaceDocumentFile`: the backup wins, and the file is rewritten from it); +/// * a document of another swarm than the state file remembers (`swarmId`). +/// +/// A document file holding a document with `--genesis` is an error: move it away. +pub(crate) fn starting_document( + options: &StartOptions<'_>, + files: &Files, + force: &Force, +) -> Result<Option<StartingDocument>> { + let from_file = match options.document_file { + None => None, + Some(path) => match document::file::load(path, files) { + Ok(loaded) => loaded, + Err(error) if force.allows("badDocumentFile") => { + warn!(error = %format!("{error:#}"), "Unusable document file; moving it aside and starting without it (--force badDocumentFile)"); + let aside = files.move_aside(path, ".bad").with_context(|| { + format!("Moving the document file {} aside", path.display()) + })?; + warn!(moved_to = %aside.display(), "Moved the document file aside"); + None + } + Err(error) => { + bail!( + "{error:#}; add --force badDocumentFile to move it aside and start without it" + ) + } + }, + }; + let backup = match options.recover_from { + None => None, + Some(path) => Some(load_backup(path, files)?), + }; + + let (doc, origin, earlier_actors) = match (backup, from_file) { + (Some(_), Some(_)) if !force.allows("replaceDocumentFile") => { + bail!( + "recover-from would replace the document in the document file; add --force replaceDocumentFile to recover the backup anyway" + ); + } + (Some(backup), from_file) => { + if from_file.is_some() { + warn!( + "Recovering the backup instead of the document file's document (--force replaceDocumentFile)" + ); + } + (backup, Origin::Backup, Vec::new()) + } + (None, Some(loaded)) => (loaded.doc, Origin::DocumentFile, loaded.actors), + (None, None) => return Ok(None), + }; + if options.genesis && origin == Origin::DocumentFile { bail!( - "The backup {} names no swarm (no _continuum/swarm); it predates swarm IDs in documents", - path.display() + "The document file holds a document of a swarm, and genesis would start a new one; move the file away to start a new swarm" + ); + } + let starting = identify(doc, origin, earlier_actors)?; + + if let Some(remembered) = options.remembered.filter(|id| *id != starting.swarm_id) { + let what = origin.describe(); + if !force.allows("swarmId") { + bail!( + "{what} is of swarm {}, but the state file remembers swarm {remembered}; add --force swarmId to start on {what}'s swarm anyway", + starting.swarm_id + ); + } + warn!( + document = %starting.swarm_id, + remembered = %remembered, + "Starting on {what}'s swarm, not the remembered one (--force swarmId)" ); + } + Ok(Some(starting)) +} + +/// The swarm and epoch `doc` is on. +fn identify( + mut doc: AutoCommit, + origin: Origin, + earlier_actors: Vec<ActorId>, +) -> Result<StartingDocument> { + let what = origin.describe(); + let Some(swarm_id) = read_swarm_id(&doc) else { + bail!("{what} names no swarm (no _continuum/swarm); it predates swarm IDs in documents"); }; - let epoch = snapshot::epoch_of(&mut doc) - .map_err(|reason| anyhow::anyhow!("The backup {}: {reason}", path.display()))?; - Ok(Backup { + let epoch = + snapshot::epoch_of(&mut doc).map_err(|reason| anyhow::anyhow!("{what}: {reason}"))?; + Ok(StartingDocument { doc, epoch, swarm_id, + origin, + earlier_actors, }) } +/// A backup to recover from, as `GET /admin/document` saves it, under a fresh actor. +fn load_backup(path: &Path, files: &Files) -> Result<AutoCommit> { + let bytes = files + .read(FileKind::Backup, path) + .with_context(|| format!("Reading the backup {}", path.display()))? + .with_context(|| format!("The backup {} doesn't exist", path.display()))?; + Ok(AutoCommit::load(&bytes) + .with_context(|| format!("Loading the backup {}", path.display()))? + .with_actor(ActorId::random())) +} + /// Resolves the seeds in `unresolved`, says HELLO to each that resolves, and keeps the /// rest for later. Warns once per seed that fails (`failing` remembers which), and says /// when such a seed resolves after all. |
