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