diff options
| author | tv <tv@krebsco.de> | 2026-10-25 12:55:27 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-25 12:55:27 +0100 |
| commit | d6620dc266b593e9ed269bcb7e65d3d8c9710d27 (patch) | |
| tree | 15d86e3fa2e0ef3b2cea6907c7b778a8ee6ed2b4 /src/swarm | |
| parent | 9845a07960fa083db0b0c7e2312f98706e9684cb (diff) | |
add possibility to start with a document
The --recover-from option allows a node start with a given document.
Diffstat (limited to 'src/swarm')
| -rw-r--r-- | src/swarm/bootstrap.rs | 65 |
1 files changed, 61 insertions, 4 deletions
diff --git a/src/swarm/bootstrap.rs b/src/swarm/bootstrap.rs index 84ff156..125321b 100644 --- a/src/swarm/bootstrap.rs +++ b/src/swarm/bootstrap.rs @@ -7,18 +7,25 @@ //! //! The genesis node records the swarm ID in the document (`_continuum/swarm`), so //! snapshots carry it forward and every backup names its swarm. +//! +//! A node recovering from a backup (`--recover-from`) starts on the backup's document, +//! swarm and epoch instead, and is ready at once, like a genesis node with content. use std::sync::atomic::Ordering; use std::time::Duration; -use anyhow::{Context, Result}; +use std::path::Path; + +use anyhow::{Context, Result, bail}; +use automerge::{ActorId, AutoCommit}; use serde_json::Value as JsonValue; use tokio::time::Instant; use tracing::{debug, info, warn}; use uuid::Uuid; +use crate::document::replica::Epoch; use crate::document::store; -use crate::remake::snapshot::{META_ROOT, SWARM}; +use crate::remake::snapshot::{self, META_ROOT, SWARM}; use crate::state::AppState; use crate::swarm::peer::{remember_peer, send_hello_once}; @@ -29,8 +36,31 @@ const BOOTSTRAP_REPORT: Duration = Duration::from_secs(300); const RESOLVE_RETRY: Duration = Duration::from_secs(5); const RESOLVE_RETRY_MAX: Duration = Duration::from_secs(300); -pub(crate) async fn bootstrap(state: &AppState, seeds: &[String], genesis: bool) -> Result<()> { - if genesis { +/// How this node starts. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Start { + /// Join a swarm through seeds (or remembered members). + Join, + /// Start a swarm, empty (`--genesis`). + Genesis, + /// Start a swarm from a backup already loaded as the document (`--recover-from`). + Recovered, +} + +pub(crate) async fn bootstrap(state: &AppState, seeds: &[String], start: Start) -> Result<()> { + if start == Start::Recovered { + let Some(swarm_id) = state.swarm_id().await else { + bail!("recovering without a swarm ID"); + }; + info!(swarm_id = %swarm_id, epoch = %state.local_epoch(), "Recovered the swarm from a backup"); + 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 + // rejoins them if they moved on to a newer epoch. + resolve_seeds(state, &mut seeds.to_vec()).await; + return Ok(()); + } + if start == Start::Genesis { // A remembered swarm starts again under its ID: nodes of it still running then // merge with this one instead of ending up in another swarm. let swarm_id = match state.swarm_id().await { @@ -102,6 +132,33 @@ fn swarm_id_path() -> Vec<String> { vec![META_ROOT.to_owned(), SWARM.to_owned()] } +/// The swarm ID the document names, if any. +fn read_swarm_id(doc: &AutoCommit) -> Option<String> { + match store::read_path(doc, &swarm_id_path()) { + Ok(Some(JsonValue::String(id))) if !id.is_empty() => Some(id), + _ => None, + } +} + +/// A backup to recover from (`--recover-from`, as `GET /admin/document` saves it): the +/// document under a fresh actor, the epoch it is on and the swarm it names. +pub(crate) fn load_backup(path: &Path) -> Result<(AutoCommit, Epoch, String)> { + let bytes = + std::fs::read(path).with_context(|| format!("Reading the backup {}", 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 { + bail!( + "The backup {} names no swarm (no _continuum/swarm); it predates swarm IDs in documents", + path.display() + ); + }; + let epoch = snapshot::epoch_of(&mut doc) + .map_err(|reason| anyhow::anyhow!("The backup {}: {reason}", path.display()))?; + Ok((doc, epoch, swarm_id)) +} + /// Resolves the seeds in `unresolved`, says HELLO to each that resolves, and keeps the /// rest for later. async fn resolve_seeds(state: &AppState, unresolved: &mut Vec<String>) { |
