From 3acbbd6cd6b64c6c39408dd254cc2dbcab1b3e65 Mon Sep 17 00:00:00 2001 From: tv Date: Tue, 20 Oct 2026 01:36:34 +0200 Subject: add epoch catch-up / snapshots --- src/compaction/switch.rs | 72 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 24 deletions(-) (limited to 'src/compaction/switch.rs') diff --git a/src/compaction/switch.rs b/src/compaction/switch.rs index 5eb1962..9f17df2 100644 --- a/src/compaction/switch.rs +++ b/src/compaction/switch.rs @@ -15,20 +15,22 @@ //! The write lock is held only for step 3, which is proportional to the changes made //! during steps 1-2. //! -//! A node whose build doesn't match the committed epoch stays on the old epoch and -//! reports the mismatch; fetching the snapshot from peers comes with step 6. +//! A node whose build doesn't match the committed epoch, or that never saw the commit, +//! fetches the new epoch from a peer instead (`catchup`). use std::time::Duration; -use automerge::{ActorId, AutoCommit}; +use automerge::{ActorId, AutoCommit, ChangeHash}; use tokio::sync::broadcast; use tracing::{error, info, warn}; +use super::catchup; use super::delta::{self, ReplayStats}; use super::local::LocalStatus; use super::records::{self, unix_millis, EpochRecord, Record}; use super::snapshot::{self, COMPACTIONS, META_ROOT}; use super::Host; +use crate::replica::Epoch; use crate::store; use crate::sync::paths_related; @@ -79,13 +81,15 @@ async fn check(host: &H) { .collect(); let (id, record) = match committed.as_slice() { - [] => return, + // Nothing committed here; maybe peers moved on without us. + [] => return catchup::check(host).await, [(id, record)] => (id.clone(), record.clone()), several => { - // The settle protocol prevents this; never guess which one is right. + // The settle protocol prevents this; never guess which one is right. Peers + // that did switch can still be followed. let ids: Vec<&str> = several.iter().map(|(id, _)| id.as_str()).collect(); - error!(compactions = ?ids, "Several compactions committed for this epoch; not switching"); - return; + error!(compactions = ?ids, "Several compactions committed for this epoch; not switching on my own"); + return catchup::check(host).await; } }; @@ -94,7 +98,9 @@ async fn check(host: &H) { .status(&id) .is_some_and(|status| status.error.is_some()) { - return; // already failed; reported in the compaction's status + // Our own switch failed (reported in the compaction's status): fetch the epoch + // from a peer that made it. + return catchup::check(host).await; } let switch_delay = host.dormant_after() / 4; // one keepalive interval @@ -148,7 +154,7 @@ async fn switch(host: &H, id: &str, record: &Record) -> Result Result<(AutoCommit, Vec, ReplayStats), String> { + move || -> Result<(AutoCommit, Vec, ReplayStats), String> { let mut copy = copy; let snapshot = match prebuilt { Some(snapshot) => snapshot, @@ -179,21 +185,7 @@ async fn switch(host: &H, id: &str, record: &Record) -> Result 0 { - info!( - replayed = late.replayed, - failed = late.failed, - "Replayed late changes" - ); - } - Ok::<_, String>(new_doc) - }) - .await?; + let old = swap_in(host, expected, new_doc, copy_heads).await?; // 4. Keep the old document as configured. host.compaction() @@ -208,3 +200,35 @@ async fn switch(host: &H, id: &str, record: &Record) -> Result( + host: &H, + target: Epoch, + new_doc: AutoCommit, + copy_heads: Vec, +) -> Result { + host.switch_epoch(target, move |live| { + // Only one worker switches, but be sure the copy still describes the live document. + if !copy_heads + .iter() + .all(|head| live.get_change_by_hash(head).is_some()) + { + return Err("the document was replaced while preparing the switch".to_owned()); + } + let mut new_doc = new_doc; + let live_heads = live.get_heads(); + let late = delta::replay(live, ©_heads, &live_heads, &mut new_doc); + if late.replayed + late.failed > 0 { + info!( + replayed = late.replayed, + failed = late.failed, + "Replayed late changes" + ); + } + Ok(new_doc) + }) + .await +} -- cgit v1.3.1