diff options
Diffstat (limited to 'src/remake/delta.rs')
| -rw-r--r-- | src/remake/delta.rs | 82 |
1 files changed, 74 insertions, 8 deletions
diff --git a/src/remake/delta.rs b/src/remake/delta.rs index c8f6b7c..59acc8c 100644 --- a/src/remake/delta.rs +++ b/src/remake/delta.rs @@ -159,22 +159,23 @@ pub(crate) struct Missing { /// Splits `doc`'s history against a snapshot `clock`: a change (actor, seq) is in the /// snapshot iff seq <= clock[actor]. /// -/// Only changes this node wrote itself (actor `own`) are worth replaying: other nodes' -/// changes after the base were replayed by those nodes when they switched, and replaying -/// them again would overwrite newer writes in the new epoch with stale ones. +/// Only changes this node wrote itself (any of the actors in `own`: one per run and +/// epoch) are worth replaying when it rejoins: other nodes' changes after the base were +/// replayed by the nodes that switched, and replaying them again would overwrite newer +/// writes in the new epoch with stale ones. pub(crate) fn missing_from_snapshot( doc: &mut AutoCommit, clock: &BTreeMap<String, u64>, - own: &ActorId, + own: &[ActorId], ) -> Missing { - let own_hex = own.to_string(); + let own_hex: HashSet<String> = own.iter().map(ActorId::to_string).collect(); let changes: Vec<(ChangeHash, Vec<ChangeHash>, bool)> = doc .get_changes(&[]) .iter() .map(|change| { let actor = change.actor_id().to_string(); let in_snapshot = clock.get(&actor).is_some_and(|&seq| change.seq() <= seq); - let own_missing = actor == own_hex && !in_snapshot; + let own_missing = own_hex.contains(&actor) && !in_snapshot; (change.hash(), change.deps().to_vec(), own_missing) }) .collect(); @@ -278,6 +279,71 @@ mod tests { store::read_path(doc, &[]).unwrap().unwrap() } + /// A random store write as the API makes it (one change): a scalar or a small map + /// at a path up to three keys deep, or a delete. Writes the store refuses (e.g. into + /// a scalar) are rolled back. + fn random_write(rng: &mut crate::testrand::Rng, doc: &mut AutoCommit) { + const KEYS: [&str; 3] = ["a", "b", "c"]; + let depth = rng.between(1, 3); + let path: Vec<String> = (0..depth).map(|_| rng.pick(&KEYS).to_string()).collect(); + let result = if rng.chance(0.2) { + store::delete_path(doc, &path, false) + } else { + let value = match rng.below(4) { + 0 => json!(rng.below(100)), + 1 => json!(format!("s{}", rng.below(100))), + 2 => json!(rng.chance(0.5)), + _ => { + let mut map = serde_json::Map::new(); + map.insert(rng.pick(&KEYS).to_string(), json!(rng.below(100))); + serde_json::Value::Object(map) + } + }; + store::write_path(doc, &path, &value) + }; + match result { + Ok(()) => { + doc.commit_with(automerge::transaction::CommitOptions::default().with_time(0)); + } + Err(_) => { + doc.rollback(); + } + } + } + + /// Compaction's promise: a snapshot of the base plus everything replayed since gives + /// the old document's content, whatever several nodes wrote concurrently. + #[test] + fn replay_reproduces_the_document() { + crate::testrand::for_seeds(200, |rng| { + let actor = |i: u8| ActorId::from(&[i; 16][..]); + let mut old = AutoCommit::new().with_actor(actor(0)); + for _ in 0..rng.between(0, 8) { + random_write(rng, &mut old); + } + let base = old.get_heads(); + let mut new = AutoCommit::new().with_actor(actor(100)); + store::materialize_into(&old, &mut new, &|_| false).unwrap(); + new.commit(); + + let mut nodes: Vec<AutoCommit> = (1..=rng.between(1, 3)) + .map(|i| old.fork().with_actor(actor(i as u8))) + .collect(); + for _ in 0..rng.between(1, 20) { + let node = rng.below(nodes.len()); + random_write(rng, &mut nodes[node]); + } + for node in &mut nodes { + old.merge(node).unwrap(); + } + let after = old.get_heads(); + + let stats = replay(&mut old, &base, &after, &mut new, &Translator::default()); + assert_eq!(stats.failed, 0); + assert_eq!(view(&new), view(&old)); + }); + } + #[test] fn minimal_paths() { let mut doc = AutoCommit::new(); @@ -370,7 +436,7 @@ mod tests { doc.commit(); let after = doc.get_heads(); - let missing = missing_from_snapshot(&mut doc, &clock, &own); + let missing = missing_from_snapshot(&mut doc, &clock, std::slice::from_ref(&own)); assert_eq!(missing.own, 1); assert_eq!(missing.before, with_foreign); assert_ne!(missing.before, base); @@ -381,7 +447,7 @@ mod tests { // Nothing of ours missing: nothing to replay. let full: BTreeMap<String, u64> = [(own.to_string(), 2)].into_iter().collect(); - let none = missing_from_snapshot(&mut doc, &full, &own); + let none = missing_from_snapshot(&mut doc, &full, std::slice::from_ref(&own)); assert_eq!(none.own, 0); let mut heads = after.clone(); heads.sort(); |
