diff options
| author | tv <tv@krebsco.de> | 2026-10-31 19:53:27 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-31 19:53:27 +0100 |
| commit | 9c12bf2e8815dc9b42be62f1e1fd2932dd02938b (patch) | |
| tree | 09cd985fae9142a247a2285c755f9d002a2c7b30 /src/remake/snapshot.rs | |
| parent | f4b0a7c5159d418e22682d2bd7d413fb3587ca7f (diff) | |
use Automerge as Document instead of AutoCommit
This allows a lot of operations to drop mut.
Diffstat (limited to 'src/remake/snapshot.rs')
| -rw-r--r-- | src/remake/snapshot.rs | 94 |
1 files changed, 49 insertions, 45 deletions
diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs index c818af6..157a28d 100644 --- a/src/remake/snapshot.rs +++ b/src/remake/snapshot.rs @@ -20,7 +20,8 @@ use std::collections::BTreeMap; -use automerge::{ActorId, AutoCommit, ChangeHash, transaction::CommitOptions}; +use crate::document::{ActorId, ChangeHash, Document, ReadDoc}; +use automerge::transaction::CommitOptions; use serde_json::{Map as JsonMap, Value as JsonValue, json}; use sha2::{Digest, Sha256}; @@ -55,7 +56,7 @@ pub(crate) struct Snapshot { /// The new epoch: next generation, ID = hash of the snapshot change. pub(crate) epoch: Epoch, /// A document consisting of exactly that one change. - pub(crate) doc: AutoCommit, + pub(crate) doc: Document, /// Highest sequence number per actor (hex) contained in the base. A change /// (actor, seq) is part of the snapshot iff seq <= clock[actor]. Read by tests only: /// nodes read the clock from the epoch metadata (`EpochMetadata::clock`). @@ -116,11 +117,11 @@ impl SnapshotRequest { /// No lock is held while building; the caller only needs a lock for the clone. /// Returns the copy as well, for callers that continue working with it. pub(crate) async fn build_detached( - mut copy: AutoCommit, + copy: Document, request: SnapshotRequest, -) -> Result<(Snapshot, AutoCommit), SnapshotError> { +) -> Result<(Snapshot, Document), SnapshotError> { tokio::task::spawn_blocking(move || { - let snapshot = build(&mut copy, &request.params())?; + let snapshot = build(©, &request.params())?; Ok((snapshot, copy)) }) .await @@ -173,7 +174,7 @@ struct MetadataRecord { /// carry the metadata of all earlier epochs forward (only remake records stay /// behind), so a node many epochs behind can still find the epoch that followed its own. pub(crate) fn read_epoch_metadata( - doc: &AutoCommit, + doc: &Document, generation: u64, ) -> Result<EpochMetadata, String> { let value = store::read_path(doc, &epoch_metadata_path(generation)) @@ -209,7 +210,7 @@ pub(crate) fn read_epoch_metadata( /// /// This does not prove the snapshot's content matches the old epoch; the hash commits /// to it, and the peer is a swarm member trusted as much as for ordinary sync. -pub(crate) fn verify_document(doc: &mut AutoCommit, epoch: Epoch) -> Result<EpochMetadata, String> { +pub(crate) fn verify_document(doc: &Document, epoch: Epoch) -> Result<EpochMetadata, String> { let root = ChangeHash(epoch.id); let Some(change) = doc.get_change_by_hash(&root) else { return Err(format!( @@ -227,7 +228,7 @@ pub(crate) fn verify_document(doc: &mut AutoCommit, epoch: Epoch) -> Result<Epoc /// The epoch `doc` is on: genesis if it holds no epoch metadata; otherwise the newest /// generation it has metadata for, whose snapshot change is the document's only change /// without dependencies (verified). For a document loaded from a backup. -pub(crate) fn epoch_of(doc: &mut AutoCommit) -> Result<Epoch, String> { +pub(crate) fn epoch_of(doc: &Document) -> Result<Epoch, String> { let generation = store::keys_at(doc, &meta::path(&[EPOCHS])) .map_err(|error| error.to_string())? .iter() @@ -263,7 +264,7 @@ pub(crate) fn epoch_metadata_path(generation: u64) -> Vec<String> { /// Builds the snapshot of `source` at `params.base_heads`. pub(crate) fn build( - source: &mut AutoCommit, + source: &Document, params: &SnapshotParams<'_>, ) -> Result<Snapshot, SnapshotError> { // fork_at with unknown heads would quietly build something else. @@ -273,21 +274,24 @@ pub(crate) fn build( } } - let mut base = source.fork_at(params.base_heads)?; - let clock = clock_of(&mut base); + let base = source.fork_at(params.base_heads)?; + let clock = clock_of(&base); let generation = params.previous.generation + 1; let kind = by_name(params.kind).ok_or_else(|| SnapshotError::UnknownKind(params.kind.to_owned()))?; - let mut snapshot = AutoCommit::new().with_actor(snapshot_actor(params.remake_id, generation)); + let mut snapshot = Document::new().with_actor(snapshot_actor(params.remake_id, generation)); + + // One transaction, so the snapshot is a single change. + let mut target = snapshot.transaction(); // The user content is the kind's. - kind.build(&base, params.content, &mut snapshot)?; + kind.build(&base, params.content, &mut target)?; // Protocol data carries forward, whatever the kind: forget records, earlier epochs' // metadata (rejoining nodes need it). Remake records and the votes register describe // the old epoch's protocol and stay behind. - match store::delete_path(&mut snapshot, &meta::path(&[]), false) { + match store::delete_path(&mut target, &meta::path(&[]), false) { Ok(()) | Err(StoreError::NotFound) => {} Err(error) => return Err(error.into()), } @@ -297,7 +301,7 @@ pub(crate) fn build( .into_iter() .filter(|key| key != REMAKES && key != VOTES) { - store::copy_path(&base, &mut snapshot, &meta::path(&[key.as_str()]))?; + store::copy_path(&base, &mut target, &meta::path(&[key.as_str()]))?; } let mut base_heads: Vec<String> = params.base_heads.iter().map(|h| h.to_string()).collect(); @@ -317,9 +321,9 @@ pub(crate) fn build( "kind": kind.name(), "translation": params.translation, })); - store::write_path(&mut snapshot, &epoch_metadata_path(generation), &metadata)?; + store::write_path(&mut target, &epoch_metadata_path(generation), &metadata)?; - let _ = snapshot.commit_with(CommitOptions::default().with_time(0)); + target.commit_with(CommitOptions::default().with_time(0)); let heads = snapshot.get_heads(); let [id] = heads.as_slice() else { @@ -337,7 +341,7 @@ pub(crate) fn build( } /// Highest sequence number per actor in `doc`'s history. -fn clock_of(doc: &mut AutoCommit) -> BTreeMap<String, u64> { +fn clock_of(doc: &Document) -> BTreeMap<String, u64> { let mut clock = BTreeMap::new(); for change in doc.get_changes(&[]) { let seq = clock.entry(change.actor_id().to_string()).or_insert(0); @@ -379,8 +383,8 @@ mod tests { use crate::document::testing::change; use automerge::{ObjType, ROOT, ReadDoc, ScalarValue, Value, transaction::Transactable}; - fn doc(actor: &[u8]) -> AutoCommit { - AutoCommit::new().with_actor(ActorId::from(actor)) + fn doc(actor: &[u8]) -> Document { + Document::new().with_actor(ActorId::from(actor)) } fn params<'a>(heads: &'a [ChangeHash], policy: &'a JsonValue) -> SnapshotParams<'a> { @@ -398,14 +402,14 @@ mod tests { static NULL: JsonValue = JsonValue::Null; - fn snapshot_of(source: &mut AutoCommit) -> Snapshot { + fn snapshot_of(source: &Document) -> Snapshot { let heads = source.get_heads(); let policy = json!({}); build(source, ¶ms(&heads, &policy)).expect("snapshot") } /// The user-visible view, without our own metadata. - fn user_view(doc: &AutoCommit) -> JsonValue { + fn user_view(doc: &Document) -> JsonValue { let mut view = store::read_path(doc, &[]).unwrap().unwrap(); view.as_object_mut().unwrap().remove(meta::ROOT); view @@ -443,8 +447,8 @@ mod tests { assert_eq!(user_view(&a), user_view(&b)); - let snap_a = snapshot_of(&mut a); - let snap_b = snapshot_of(&mut b); + let snap_a = snapshot_of(&a); + let snap_b = snapshot_of(&b); // Different histories mean different clocks, and the clock is part of the // metadata. Equal views alone therefore give equal content but not equal IDs; @@ -468,9 +472,9 @@ mod tests { let mut b = doc(b"actor-b"); b.merge(&mut a.clone()).unwrap(); - let first = snapshot_of(&mut a); - let again = snapshot_of(&mut a); - let elsewhere = snapshot_of(&mut b); + let first = snapshot_of(&a); + let again = snapshot_of(&a); + let elsewhere = snapshot_of(&b); assert_eq!(first.epoch, again.epoch); assert_eq!(first.epoch, elsewhere.epoch); @@ -498,7 +502,7 @@ mod tests { "test setup should produce a conflict" ); - let snap = snapshot_of(&mut a); + let snap = snapshot_of(&a); assert_eq!(user_view(&snap.doc), json!({ "cfg": { "x": 1, "y": 2 } })); let (_, conflicts) = store::read_path_with_conflicts(&snap.doc, &[]) @@ -529,7 +533,7 @@ mod tests { change(&mut a, |a| a.put(ROOT, "late", true).unwrap()); let policy = json!({}); - let snap = build(&mut a, ¶ms(&base, &policy)).unwrap(); + let snap = build(&a, ¶ms(&base, &policy)).unwrap(); let view = user_view(&snap.doc); assert!(view.get("late").is_none()); @@ -548,11 +552,11 @@ mod tests { change(&mut a, |a| { store::write_path(a, &["k".into()], &json!(1)).unwrap() }); - assert_eq!(epoch_of(&mut a), Ok(Epoch::GENESIS)); + assert_eq!(epoch_of(&a), Ok(Epoch::GENESIS)); let heads = a.get_heads(); let policy = json!({}); - let mut snap = build(&mut a, ¶ms(&heads, &policy)).unwrap(); + let snap = build(&a, ¶ms(&heads, &policy)).unwrap(); // Saved and loaded, with a write since, as a backup would be. let mut later = snap .doc @@ -561,8 +565,8 @@ mod tests { change(&mut later, |later| { store::write_path(later, &["k".into()], &json!(2)).unwrap() }); - let mut backup = AutoCommit::load(&later.save()).unwrap(); - assert_eq!(epoch_of(&mut backup), Ok(snap.epoch)); + let backup = Document::load(&later.save()).unwrap(); + assert_eq!(epoch_of(&backup), Ok(snap.epoch)); } #[test] @@ -591,7 +595,7 @@ mod tests { let heads = a.get_heads(); let policy = json!({ "replayWindow": { "epochs": 1 } }); - let snap = build(&mut a, ¶ms(&heads, &policy)).unwrap(); + let snap = build(&a, ¶ms(&heads, &policy)).unwrap(); let meta = store::read_path(&snap.doc, &meta::path(&[])) .unwrap() @@ -624,7 +628,7 @@ mod tests { a.put(ROOT, "neg_zero", -0.0_f64).unwrap(); }); - let snap = snapshot_of(&mut a); + let snap = snapshot_of(&a); let d = &snap.doc; match d.get(ROOT, "c").unwrap() { @@ -665,7 +669,7 @@ mod tests { let mut a = doc(b"actor-a"); change(&mut a, |a| a.put(ROOT, "x", 1_i64).unwrap()); - let mut snap = snapshot_of(&mut a); + let snap = snapshot_of(&a); let heads = snap.doc.get_heads(); assert_eq!(heads.len(), 1); assert_eq!(heads[0].0, snap.epoch.id); @@ -683,15 +687,15 @@ mod tests { let policy = json!({}); assert!(matches!( - build(&mut a, ¶ms(&foreign, &policy)), + build(&a, ¶ms(&foreign, &policy)), Err(SnapshotError::MissingBase(_)) )); } #[test] fn empty_document() { - let mut a = doc(b"actor-a"); - let snap = snapshot_of(&mut a); + let a = doc(b"actor-a"); + let snap = snapshot_of(&a); assert_eq!(snap.epoch.generation, 1); assert!(snap.clock.is_empty()); assert_eq!(user_view(&snap.doc), json!({})); @@ -706,7 +710,7 @@ mod tests { change(&mut late, |late| late.put(ROOT, "shared", "base").unwrap()); let base = late.get_heads(); let policy = json!({}); - let mut snap = build(&mut late, ¶ms(&base, &policy)).unwrap(); + let snap = build(&late, ¶ms(&base, &policy)).unwrap(); // Meanwhile the swarm moved on in the new epoch. let mut swarm = snap @@ -718,8 +722,8 @@ mod tests { change(&mut late, |late| late.put(ROOT, "mine", "late").unwrap()); - let mut fetched = AutoCommit::load(&bytes).unwrap(); - let metadata = verify_document(&mut fetched, snap.epoch).unwrap(); + let fetched = Document::load(&bytes).unwrap(); + let metadata = verify_document(&fetched, snap.epoch).unwrap(); assert_eq!(metadata.previous, Epoch::GENESIS); assert_eq!(metadata.remake_id, "c-1"); assert_eq!( @@ -730,14 +734,14 @@ mod tests { // Documents of other epochs are refused. let mut wrong = snap.epoch; wrong.generation = 2; - assert!(verify_document(&mut fetched, wrong).is_err()); + assert!(verify_document(&fetched, wrong).is_err()); let mut wrong = snap.epoch; wrong.id = [0; 32]; - assert!(verify_document(&mut fetched, wrong).is_err()); + assert!(verify_document(&fetched, wrong).is_err()); let own = late.get_actor().clone(); let missing = - delta::missing_from_snapshot(&mut late, &metadata.clock, std::slice::from_ref(&own)); + delta::missing_from_snapshot(&late, &metadata.clock, std::slice::from_ref(&own)); assert_eq!(missing.own, 1); let heads = late.get_heads(); let mut adopted = fetched.with_actor(ActorId::from(&b"late-node-next"[..])); |
