summaryrefslogtreecommitdiffstats
path: root/src/swarm/bootstrap.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/swarm/bootstrap.rs')
-rw-r--r--src/swarm/bootstrap.rs167
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.