1use crate::tombstone::tombstone_winner;
15use crate::{
16 BlobStore, Body, CoreError, Identity, KeyPrefix, MergeOutcome, PutResult, Record, RecordKey,
17 Result, Revision, Store, merge,
18};
19use std::collections::BTreeSet;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct SyncConflict {
26 pub key: RecordKey,
27 pub a: Box<Record>,
28 pub b: Box<Record>,
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Eq)]
33#[must_use = "a SyncReport may contain unresolved conflicts that must be handled"]
34pub struct SyncReport {
35 pub copied_to_a: Vec<RecordKey>,
37 pub copied_to_b: Vec<RecordKey>,
39 pub fast_forwarded_to_a: Vec<RecordKey>,
42 pub fast_forwarded_to_b: Vec<RecordKey>,
45 pub merged: Vec<RecordKey>,
49 pub conflicts: Vec<SyncConflict>,
51 pub unconverged: Vec<RecordKey>,
57}
58
59impl SyncReport {
60 #[must_use]
65 pub fn converged(&self) -> bool {
66 self.unconverged.is_empty()
67 }
68}
69
70const MAX_SYNC_PASSES: usize = 16;
77
78pub async fn sync(a: &dyn Store, b: &dyn Store) -> Result<SyncReport> {
92 sync_with_ancestry(a, b, None).await
93}
94
95pub async fn sync_with_ancestry(
102 a: &dyn Store,
103 b: &dyn Store,
104 ancestry: Option<&dyn BlobStore>,
105) -> Result<SyncReport> {
106 let mut report = SyncReport::default();
107 for _ in 0..MAX_SYNC_PASSES {
108 let (pass, raced) = sync_pass(a, b, ancestry).await?;
109 report = pass;
110 if raced.is_empty() {
111 break; }
113 report.unconverged = raced;
116 }
117 Ok(report)
118}
119
120async fn ancestry_base(ancestry: Option<&dyn BlobStore>, rec_a: &Record, rec_b: &Record) -> Body {
124 if let Some(anc) = ancestry
125 && let (Some(pa), Some(pb)) = (&rec_a.parent, &rec_b.parent)
126 && pa == pb
127 && let Ok(Some(bytes)) = anc.get_blob(&pa.hash).await
128 {
129 return Body::Inline(bytes);
130 }
131 Body::Inline(Vec::new())
132}
133
134#[derive(Debug, PartialEq, Eq)]
136enum Relation {
137 InSync,
139 AAhead,
141 BAhead,
143 Diverged,
146}
147
148fn relate(a: &Record, b: &Record) -> Relation {
149 if a.revision == b.revision {
150 Relation::InSync
151 } else if a.ancestors.contains(&b.revision) {
152 Relation::AAhead
153 } else if b.ancestors.contains(&a.revision) {
154 Relation::BAhead
155 } else {
156 Relation::Diverged
157 }
158}
159
160fn lossless_cap(a: &Record, b: &Record) -> usize {
165 a.ancestors.len() + b.ancestors.len() + 2
166}
167
168async fn sync_pass(
174 a: &dyn Store,
175 b: &dyn Store,
176 ancestry: Option<&dyn BlobStore>,
177) -> Result<(SyncReport, Vec<RecordKey>)> {
178 let mut report = SyncReport::default();
179 let mut raced: Vec<RecordKey> = Vec::new();
180
181 let mut keys: BTreeSet<RecordKey> = BTreeSet::new();
184 keys.extend(a.list_raw(&KeyPrefix::default()).await?);
185 keys.extend(b.list_raw(&KeyPrefix::default()).await?);
186
187 for key in keys {
188 let ra = a.get_raw(&key).await?;
189 let rb = b.get_raw(&key).await?;
190 match (ra, rb) {
191 (Some(rec), None) => {
192 if copy(b, &rec).await? {
193 report.copied_to_b.push(key);
194 } else {
195 raced.push(key);
196 }
197 }
198 (None, Some(rec)) => {
199 if copy(a, &rec).await? {
200 report.copied_to_a.push(key);
201 } else {
202 raced.push(key);
203 }
204 }
205 (Some(rec_a), Some(rec_b)) => match relate(&rec_a, &rec_b) {
206 Relation::InSync => {}
207 Relation::AAhead => {
208 if overwrite(b, &rec_a, &rec_b.revision).await? {
209 report.fast_forwarded_to_b.push(key);
210 } else {
211 raced.push(key);
212 }
213 }
214 Relation::BAhead => {
215 if overwrite(a, &rec_b, &rec_a.revision).await? {
216 report.fast_forwarded_to_a.push(key);
217 } else {
218 raced.push(key);
219 }
220 }
221 Relation::Diverged => match (rec_a.is_tombstone(), rec_b.is_tombstone()) {
222 (true, true) => {
223 let winner = tombstone_winner(&rec_a, &rec_b, lossless_cap(&rec_a, &rec_b));
224 let la = overwrite(a, &winner, &rec_a.revision).await?;
228 let lb = overwrite(b, &winner, &rec_b.revision).await?;
229 if la && lb {
230 report.merged.push(key);
231 } else {
232 raced.push(key);
233 }
234 }
235 (true, false) | (false, true) => {
236 report.conflicts.push(SyncConflict {
239 key,
240 a: Box::new(rec_a),
241 b: Box::new(rec_b),
242 });
243 }
244 (false, false) => {
245 let base = ancestry_base(ancestry, &rec_a, &rec_b).await;
246 match merge(rec_a.kind.merge_class(), &base, &rec_a.body, &rec_b.body) {
247 MergeOutcome::Merged(body) => {
248 let merged = build_merged(&key, &rec_a, &rec_b, body);
249 let la = overwrite(a, &merged, &rec_a.revision).await?;
250 let lb = overwrite(b, &merged, &rec_b.revision).await?;
251 if la && lb {
252 report.merged.push(key);
253 } else {
254 raced.push(key);
258 }
259 }
260 MergeOutcome::NeedsResolution => {
261 report.conflicts.push(SyncConflict {
262 key,
263 a: Box::new(rec_a),
264 b: Box::new(rec_b),
265 });
266 }
267 }
268 }
269 },
270 },
271 (None, None) => {}
272 }
273 }
274 Ok((report, raced))
275}
276
277async fn copy(dst: &dyn Store, rec: &Record) -> Result<bool> {
283 Ok(matches!(
284 dst.put_raw(rec.clone(), None).await?,
285 PutResult::Committed(_)
286 ))
287}
288
289async fn overwrite(dst: &dyn Store, rec: &Record, expected: &Revision) -> Result<bool> {
294 match dst.put_raw(rec.clone(), Some(expected.clone())).await {
295 Ok(PutResult::Committed(_)) => Ok(true),
296 Ok(PutResult::Conflict(_)) => Ok(false),
297 Err(CoreError::NotFound(_)) => Ok(false),
298 Err(e) => Err(e),
299 }
300}
301
302fn build_merged(key: &RecordKey, a: &Record, b: &Record, body: Body) -> Record {
305 debug_assert_eq!(&a.key, key);
306 debug_assert_eq!(&b.key, key);
307 crate::tombstone::reconciled_record(
308 a,
309 b,
310 body,
311 Identity::new("gonzalo-sync"),
312 "sync",
313 lossless_cap(a, b),
314 )
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::memstore::MemStore;
321 use crate::{CoreError, DeleteResult, Meta, PutResult, RecordKind, store::Conflict};
322 use async_trait::async_trait;
323 use std::collections::{BTreeMap, HashSet};
324 use std::sync::Mutex;
325
326 #[allow(clippy::large_enum_variant)]
331 enum Race {
332 FlakyOnce(Mutex<HashSet<RecordKey>>),
334 Always,
337 TombstoneArrives(Mutex<Option<Record>>),
341 }
342
343 struct RacyStore {
346 inner: MemStore,
347 race: Race,
348 }
349
350 impl RacyStore {
351 fn flaky_once() -> Self {
358 Self {
359 inner: MemStore::new(),
360 race: Race::FlakyOnce(Mutex::new(HashSet::new())),
361 }
362 }
363
364 fn always() -> Self {
369 Self {
370 inner: MemStore::new(),
371 race: Race::Always,
372 }
373 }
374
375 fn tombstone_arrives(arriving: Record) -> Self {
381 Self {
382 inner: MemStore::new(),
383 race: Race::TombstoneArrives(Mutex::new(Some(arriving))),
384 }
385 }
386
387 async fn interfere(
392 &self,
393 record: &Record,
394 expected: &Option<Revision>,
395 raw: bool,
396 ) -> Result<Option<PutResult>> {
397 match &self.race {
398 Race::FlakyOnce(tripped) => {
399 let trip = expected.is_some() && {
400 let mut g = tripped.lock().unwrap();
401 g.insert(record.key.clone())
402 };
403 if trip && let Some(current) = self.inner.get_raw(&record.key).await? {
404 return Ok(Some(PutResult::Conflict(Box::new(Conflict {
405 key: record.key.clone(),
406 expected: expected.clone(),
407 current,
408 }))));
409 }
410 Ok(None)
411 }
412 Race::Always => match self.inner.get_raw(&record.key).await? {
413 None if expected.is_none() => Ok(None),
414 None => Err(CoreError::NotFound(record.key.clone())),
415 Some(current) => Ok(Some(PutResult::Conflict(Box::new(Conflict {
416 key: record.key.clone(),
417 expected: expected.clone(),
418 current,
419 })))),
420 },
421 Race::TombstoneArrives(slot) => {
422 if raw {
423 let arriving = if expected.is_none() {
424 let mut guard = slot.lock().unwrap();
425 match guard.as_ref() {
426 Some(t) if t.key == record.key => guard.take(),
427 _ => None,
428 }
429 } else {
430 None
431 };
432 if let Some(t) = arriving {
433 let _ = self.inner.put_raw(t, None).await?;
434 }
435 }
436 Ok(None)
437 }
438 }
439 }
440 }
441
442 #[async_trait]
443 impl Store for RacyStore {
444 async fn get(&self, key: &RecordKey) -> Result<Option<Record>> {
445 self.inner.get(key).await
446 }
447 async fn put(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
448 if let Some(raced) = self.interfere(&record, &expected, false).await? {
449 return Ok(raced);
450 }
451 self.inner.put(record, expected).await
452 }
453 async fn put_raw(&self, record: Record, expected: Option<Revision>) -> Result<PutResult> {
454 if let Some(raced) = self.interfere(&record, &expected, true).await? {
455 return Ok(raced);
456 }
457 self.inner.put_raw(record, expected).await
458 }
459 async fn list(&self, prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
460 self.inner.list(prefix).await
461 }
462 async fn delete_as(
463 &self,
464 key: &RecordKey,
465 expected: Option<Revision>,
466 author: Option<Identity>,
467 ) -> Result<DeleteResult> {
468 self.inner.delete_as(key, expected, author).await
469 }
470 async fn get_raw(&self, key: &RecordKey) -> Result<Option<Record>> {
471 self.inner.get_raw(key).await
472 }
473 async fn list_raw(&self, prefix: &KeyPrefix) -> Result<Vec<RecordKey>> {
474 self.inner.list_raw(prefix).await
475 }
476 async fn purge(&self, key: &RecordKey, expected: Revision) -> Result<DeleteResult> {
477 self.inner.purge(key, expected).await
478 }
479 }
480
481 fn rec(id: &str, kind: RecordKind, payload: &str) -> Record {
482 let body = Body::Inline(payload.as_bytes().to_vec());
483 Record {
484 revision: Revision::initial(body.bytes()),
485 parent: None,
486 body,
487 kind,
488 key: RecordKey::new("ns", "col", id),
489 meta: Meta {
490 author: Identity::new("t"),
491 origin_system: "test".into(),
492 created: 0,
493 updated: 0,
494 labels: BTreeMap::new(),
495 },
496 links: Vec::new(),
497 ancestors: Vec::new(),
498 deleted_at: None,
499 deleted_blob: None,
500 deleted_kind: None,
501 }
502 }
503
504 #[tokio::test]
505 async fn copies_one_sided_records_both_directions() {
506 let a = MemStore::new();
507 let b = MemStore::new();
508 let _ = a
509 .put(rec("only_a", RecordKind::Topic, "x"), None)
510 .await
511 .unwrap();
512 let _ = b
513 .put(rec("only_b", RecordKind::Topic, "y"), None)
514 .await
515 .unwrap();
516
517 let report = sync(&a, &b).await.unwrap();
518 assert_eq!(
519 report.copied_to_b,
520 vec![RecordKey::new("ns", "col", "only_a")]
521 );
522 assert_eq!(
523 report.copied_to_a,
524 vec![RecordKey::new("ns", "col", "only_b")]
525 );
526 assert!(
527 a.get(&RecordKey::new("ns", "col", "only_b"))
528 .await
529 .unwrap()
530 .is_some()
531 );
532 assert!(
533 b.get(&RecordKey::new("ns", "col", "only_a"))
534 .await
535 .unwrap()
536 .is_some()
537 );
538 }
539
540 #[tokio::test]
541 async fn append_only_divergence_auto_merges() {
542 let a = MemStore::new();
543 let b = MemStore::new();
544 let _ = a
545 .put(rec("t", RecordKind::Topic, "base\nfrom_a\n"), None)
546 .await
547 .unwrap();
548 let _ = b
549 .put(rec("t", RecordKind::Topic, "base\nfrom_b\n"), None)
550 .await
551 .unwrap();
552
553 let report = sync(&a, &b).await.unwrap();
554 assert_eq!(report.merged, vec![RecordKey::new("ns", "col", "t")]);
555 assert!(report.conflicts.is_empty());
556 let merged = a
557 .get(&RecordKey::new("ns", "col", "t"))
558 .await
559 .unwrap()
560 .unwrap();
561 let text = String::from_utf8(merged.body.bytes().to_vec()).unwrap();
562 assert!(text.contains("from_a") && text.contains("from_b") && text.contains("base"));
563 let mb = b
565 .get(&RecordKey::new("ns", "col", "t"))
566 .await
567 .unwrap()
568 .unwrap();
569 assert_eq!(merged.revision, mb.revision);
570 }
571
572 #[tokio::test]
573 async fn checkpoint_divergence_surfaces_conflict() {
574 let a = MemStore::new();
575 let b = MemStore::new();
576 let _ = a
577 .put(rec("c", RecordKind::Checkpoint, "a"), None)
578 .await
579 .unwrap();
580 let _ = b
581 .put(rec("c", RecordKind::Checkpoint, "b"), None)
582 .await
583 .unwrap();
584
585 let report = sync(&a, &b).await.unwrap();
586 assert_eq!(report.conflicts.len(), 1);
587 assert_eq!(report.conflicts[0].key, RecordKey::new("ns", "col", "c"));
588 assert!(report.merged.is_empty());
589 }
590
591 #[tokio::test]
592 async fn memory_tier_divergence_surfaces_conflict() {
593 let a = MemStore::new();
594 let b = MemStore::new();
595 let _ = a
596 .put(rec("m", RecordKind::MemoryTier, "a"), None)
597 .await
598 .unwrap();
599 let _ = b
600 .put(rec("m", RecordKind::MemoryTier, "b"), None)
601 .await
602 .unwrap();
603
604 let report = sync(&a, &b).await.unwrap();
605 assert_eq!(report.conflicts.len(), 1);
606 assert!(report.merged.is_empty());
607 }
608
609 #[tokio::test]
610 async fn session_divergence_auto_merges() {
611 let a = MemStore::new();
612 let b = MemStore::new();
613 let _ = a
614 .put(rec("s", RecordKind::Session, "base\nfrom_a\n"), None)
615 .await
616 .unwrap();
617 let _ = b
618 .put(rec("s", RecordKind::Session, "base\nfrom_b\n"), None)
619 .await
620 .unwrap();
621
622 let report = sync(&a, &b).await.unwrap();
623 assert_eq!(report.merged, vec![RecordKey::new("ns", "col", "s")]);
624 assert!(report.conflicts.is_empty());
625 }
626
627 #[tokio::test]
628 async fn session_with_blank_and_duplicate_lines_survives_divergent_sync() {
629 let a = MemStore::new();
636 let b = MemStore::new();
637 let _ = a
638 .put(
639 rec("s", RecordKind::Session, "a\n\nyes\nyes\nfrom_a\n"),
640 None,
641 )
642 .await
643 .unwrap();
644 let _ = b
645 .put(
646 rec("s", RecordKind::Session, "a\n\nyes\nyes\nfrom_b\n"),
647 None,
648 )
649 .await
650 .unwrap();
651
652 let report = sync(&a, &b).await.unwrap();
653 let key = RecordKey::new("ns", "col", "s");
654 assert_eq!(report.merged, vec![key.clone()]);
655 assert!(report.conflicts.is_empty());
656
657 let ma = a.get(&key).await.unwrap().unwrap();
658 let text = String::from_utf8(ma.body.bytes().to_vec()).unwrap();
659 assert_eq!(text, "a\n\nyes\nyes\nfrom_a\nfrom_b\n");
661 let mb = b.get(&key).await.unwrap().unwrap();
663 assert_eq!(ma.revision, mb.revision);
664 assert_eq!(ma.body, mb.body);
665 }
666
667 #[tokio::test]
668 async fn re_loops_until_a_racing_store_converges() {
669 let a = MemStore::new();
673 let b = RacyStore::flaky_once();
674 let _ = a
675 .put(rec("t", RecordKind::Topic, "base\nfrom_a\n"), None)
676 .await
677 .unwrap();
678 let _ = b
679 .put(rec("t", RecordKind::Topic, "base\nfrom_b\n"), None)
680 .await
681 .unwrap();
682
683 let report = sync(&a, &b).await.unwrap();
684
685 assert!(report.conflicts.is_empty());
686 let key = RecordKey::new("ns", "col", "t");
687 let ra = a.get(&key).await.unwrap().unwrap();
688 let rb = b.get(&key).await.unwrap().unwrap();
689 assert_eq!(ra.revision, rb.revision);
691 let text = String::from_utf8(rb.body.bytes().to_vec()).unwrap();
692 assert!(text.contains("from_a") && text.contains("from_b"));
693 assert_eq!(report.fast_forwarded_to_b, vec![key]);
696 assert!(report.merged.is_empty());
697 }
698
699 fn structured_divergence() -> (Record, Record, &'static str) {
702 let base = rec("m", RecordKind::MemoryTier, r#"{"name":"a","content":"x"}"#);
703 let base_rev = base.revision.clone();
704 let mut ours = rec("m", RecordKind::MemoryTier, r#"{"name":"b","content":"x"}"#);
705 ours.parent = Some(base_rev.clone());
706 let mut theirs = rec("m", RecordKind::MemoryTier, r#"{"name":"a","content":"y"}"#);
707 theirs.parent = Some(base_rev);
708 (ours, theirs, r#"{"name":"a","content":"x"}"#)
709 }
710
711 #[tokio::test]
712 async fn structured_divergence_merges_with_ancestry() {
713 use crate::ancestry::tests::Mem;
714 let a = Mem::default();
715 let b = Mem::default();
716 let ancestry = Mem::default();
717 let (ours, theirs, base_body) = structured_divergence();
718 ancestry.put_blob(base_body.as_bytes()).await.unwrap();
720 let _ = a.put(ours, None).await.unwrap();
721 let _ = b.put(theirs, None).await.unwrap();
722
723 let report = sync_with_ancestry(&a, &b, Some(&ancestry)).await.unwrap();
724
725 let key = RecordKey::new("ns", "col", "m");
726 assert_eq!(report.merged, vec![key.clone()], "3-way merged");
727 assert!(report.conflicts.is_empty());
728 let merged = a.get(&key).await.unwrap().unwrap();
730 let v: serde_json::Value = serde_json::from_slice(merged.body.bytes()).unwrap();
731 assert_eq!(v, serde_json::json!({"name": "b", "content": "y"}));
732 }
733
734 #[tokio::test]
735 async fn structured_divergence_conflicts_without_ancestry() {
736 use crate::ancestry::tests::Mem;
737 let a = Mem::default();
738 let b = Mem::default();
739 let (ours, theirs, _) = structured_divergence();
740 let _ = a.put(ours, None).await.unwrap();
741 let _ = b.put(theirs, None).await.unwrap();
742
743 let report = sync(&a, &b).await.unwrap();
746 assert_eq!(report.conflicts.len(), 1);
747 assert!(report.merged.is_empty());
748 }
749
750 #[tokio::test]
751 async fn bounded_retry_terminates_under_continuous_writes() {
752 let a = RacyStore::always();
756 let b = RacyStore::always();
757 let _ = a
758 .put(rec("t", RecordKind::Topic, "base\nfrom_a\n"), None)
759 .await
760 .unwrap();
761 let _ = b
762 .put(rec("t", RecordKind::Topic, "base\nfrom_b\n"), None)
763 .await
764 .unwrap();
765
766 let report = sync(&a, &b).await.unwrap();
767
768 let key = RecordKey::new("ns", "col", "t");
770 let ra = a.get(&key).await.unwrap().unwrap();
771 let rb = b.get(&key).await.unwrap().unwrap();
772 assert_ne!(ra.revision, rb.revision);
773 assert!(report.merged.is_empty());
774 }
775
776 fn k(id: &str) -> RecordKey {
777 RecordKey::new("ns", "col", id)
778 }
779
780 async fn commit(store: &dyn Store, r: Record, expected: Option<Revision>) -> Revision {
781 let PutResult::Committed(rev) = store.put(r, expected).await.unwrap() else {
782 panic!("put did not commit");
783 };
784 rev
785 }
786
787 async fn edit(store: &dyn Store, id: &str, kind: RecordKind, payload: &str) -> Revision {
791 let cur = store
792 .get(&k(id))
793 .await
794 .unwrap()
795 .expect("edit needs a live record");
796 let mut r = rec(id, kind, payload);
797 r.revision = cur.revision.next(payload.as_bytes());
798 r.parent = Some(cur.revision.clone());
799 commit(store, r, Some(cur.revision)).await
800 }
801
802 async fn delete(store: &dyn Store, id: &str, expected: Revision) -> Record {
803 assert!(matches!(
804 store.delete(&k(id), Some(expected)).await.unwrap(),
805 DeleteResult::Deleted
806 ));
807 let t = store.get_raw(&k(id)).await.unwrap().unwrap();
808 assert!(t.is_tombstone());
809 t
810 }
811
812 #[tokio::test]
815 async fn stale_peer_takes_the_tombstone() {
816 let a = MemStore::new();
817 let b = MemStore::new();
818 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
819 let _ = sync(&a, &b).await.unwrap();
820 let tomb = delete(&a, "d", r0).await;
821
822 let report = sync(&a, &b).await.unwrap();
823
824 assert_eq!(report.fast_forwarded_to_b, vec![k("d")]);
825 assert!(report.conflicts.is_empty() && report.merged.is_empty());
826 assert!(report.copied_to_a.is_empty() && report.copied_to_b.is_empty());
827 assert!(
828 b.get(&k("d")).await.unwrap().is_none(),
829 "hidden from consumers"
830 );
831 let tb = b.get_raw(&k("d")).await.unwrap().unwrap();
832 assert!(tb.is_tombstone());
833 assert_eq!(tb.revision, tomb.revision);
834 assert_eq!(sync(&a, &b).await.unwrap(), SyncReport::default());
836 }
837
838 #[tokio::test]
839 async fn stale_peer_takes_the_tombstone_in_either_sync_direction() {
840 let a = MemStore::new();
841 let b = MemStore::new();
842 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
843 let _ = sync(&a, &b).await.unwrap();
844 let tomb = delete(&a, "d", r0).await;
845
846 let report = sync(&b, &a).await.unwrap();
848
849 assert_eq!(report.fast_forwarded_to_a, vec![k("d")]);
850 assert!(report.conflicts.is_empty());
851 assert!(b.get(&k("d")).await.unwrap().is_none());
852 assert_eq!(
853 b.get_raw(&k("d")).await.unwrap().unwrap().revision,
854 tomb.revision
855 );
856 assert!(a.get(&k("d")).await.unwrap().is_none(), "never copied back");
857 assert_eq!(
858 a.get_raw(&k("d")).await.unwrap().unwrap().revision,
859 tomb.revision
860 );
861 }
862
863 #[tokio::test]
864 async fn recreation_after_delete_propagates_live() {
865 let a = MemStore::new();
866 let b = MemStore::new();
867 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
868 let _ = sync(&a, &b).await.unwrap();
869 let _ = delete(&a, "d", r0).await;
870 let _ = sync(&a, &b).await.unwrap();
871
872 let r2 = commit(&a, rec("d", RecordKind::Topic, "again\n"), None).await;
874 assert_eq!(r2.counter, 2);
875
876 let report = sync(&a, &b).await.unwrap();
877
878 assert_eq!(report.fast_forwarded_to_b, vec![k("d")]);
879 assert!(report.conflicts.is_empty());
880 let live = b
881 .get(&k("d"))
882 .await
883 .unwrap()
884 .expect("recreated record is live on b");
885 assert_eq!(live.revision, r2);
886 assert_eq!(live.body.bytes(), b"again\n");
887 }
888
889 #[tokio::test]
890 async fn delete_vs_concurrent_edit_is_a_conflict_and_writes_nothing() {
891 let a = MemStore::new();
893 let b = MemStore::new();
894 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
895 let _ = sync(&a, &b).await.unwrap();
896 let tomb = delete(&a, "d", r0).await;
897 let edited = edit(&b, "d", RecordKind::Topic, "v0\nedit\n").await;
898
899 let report = sync(&a, &b).await.unwrap();
900
901 assert_eq!(report.conflicts.len(), 1);
902 assert_eq!(report.conflicts[0].key, k("d"));
903 assert!(report.conflicts[0].a.is_tombstone());
904 assert!(!report.conflicts[0].b.is_tombstone());
905 assert!(report.merged.is_empty());
906 assert!(report.fast_forwarded_to_a.is_empty() && report.fast_forwarded_to_b.is_empty());
907 assert_eq!(
908 a.get_raw(&k("d")).await.unwrap().unwrap().revision,
909 tomb.revision
910 );
911 assert_eq!(b.get(&k("d")).await.unwrap().unwrap().revision, edited);
912 }
913
914 #[tokio::test]
915 async fn edit_vs_concurrent_delete_on_b_is_a_conflict_and_writes_nothing() {
916 let a = MemStore::new();
920 let b = MemStore::new();
921 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
922 let _ = sync(&a, &b).await.unwrap();
923 let edited = edit(&a, "d", RecordKind::Topic, "v0\nedit\n").await;
924 let tomb = delete(&b, "d", r0).await;
925
926 let report = sync(&a, &b).await.unwrap();
927
928 assert_eq!(report.conflicts.len(), 1);
929 assert_eq!(report.conflicts[0].key, k("d"));
930 assert!(!report.conflicts[0].a.is_tombstone());
931 assert!(report.conflicts[0].b.is_tombstone());
932 assert!(report.merged.is_empty());
933 assert!(report.fast_forwarded_to_a.is_empty() && report.fast_forwarded_to_b.is_empty());
934 assert_eq!(a.get(&k("d")).await.unwrap().unwrap().revision, edited);
936 assert_eq!(
937 b.get_raw(&k("d")).await.unwrap().unwrap().revision,
938 tomb.revision
939 );
940 }
941
942 #[tokio::test]
943 async fn concurrent_tombstones_converge_on_the_higher_one() {
944 let a = MemStore::new();
945 let b = MemStore::new();
946 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
947 let _ = sync(&a, &b).await.unwrap();
948 let r1 = edit(&a, "d", RecordKind::Topic, "v0\nv1\n").await;
950 let ta = delete(&a, "d", r1.clone()).await;
951 let tb = delete(&b, "d", r0.clone()).await;
952 assert_eq!(ta.revision.counter, 2);
953 assert_eq!(tb.revision.counter, 1);
954
955 let report = sync(&a, &b).await.unwrap();
956
957 assert_eq!(report.merged, vec![k("d")]);
958 assert!(report.conflicts.is_empty());
959 for store in [&a, &b] {
960 let got = store.get_raw(&k("d")).await.unwrap().unwrap();
961 assert!(got.is_tombstone());
962 assert_eq!(got.revision, ta.revision);
963 assert!(got.ancestors.contains(&tb.revision));
964 assert!(got.ancestors.contains(&r1));
965 assert!(got.ancestors.contains(&r0));
966 assert!(!got.ancestors.contains(&ta.revision));
967 }
968 assert_eq!(sync(&a, &b).await.unwrap(), SyncReport::default());
969 }
970
971 #[tokio::test]
972 async fn one_sided_tombstone_is_copied() {
973 let a = MemStore::new();
974 let b = MemStore::new();
975 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
976 let tomb = delete(&a, "d", r0).await;
977
978 let report = sync(&a, &b).await.unwrap();
979
980 assert_eq!(report.copied_to_b, vec![k("d")]);
981 assert!(b.get(&k("d")).await.unwrap().is_none());
982 assert_eq!(
983 b.get_raw(&k("d")).await.unwrap().unwrap().revision,
984 tomb.revision
985 );
986 assert_eq!(sync(&a, &b).await.unwrap(), SyncReport::default());
987 }
988
989 #[tokio::test]
992 async fn opaque_fast_forward_no_longer_conflicts() {
993 let a = MemStore::new();
994 let b = MemStore::new();
995 let _ = commit(&a, rec("c", RecordKind::Checkpoint, "c0"), None).await;
996 let _ = sync(&a, &b).await.unwrap();
997
998 let c1 = edit(&a, "c", RecordKind::Checkpoint, "c1").await;
1000 let report = sync(&a, &b).await.unwrap();
1001 assert!(report.conflicts.is_empty(), "behind is not diverged");
1002 assert_eq!(report.fast_forwarded_to_b, vec![k("c")]);
1003 assert_eq!(b.get(&k("c")).await.unwrap().unwrap().revision, c1);
1004
1005 let c2 = edit(&b, "c", RecordKind::Checkpoint, "c2").await;
1007 let report = sync(&a, &b).await.unwrap();
1008 assert!(report.conflicts.is_empty());
1009 assert_eq!(report.fast_forwarded_to_a, vec![k("c")]);
1010 assert_eq!(a.get(&k("c")).await.unwrap().unwrap().revision, c2);
1011 }
1012
1013 #[tokio::test]
1014 async fn checkpoint_true_divergence_from_a_shared_parent_still_conflicts() {
1015 let a = MemStore::new();
1016 let b = MemStore::new();
1017 let _ = commit(&a, rec("c", RecordKind::Checkpoint, "c0"), None).await;
1018 let _ = sync(&a, &b).await.unwrap();
1019 let ca = edit(&a, "c", RecordKind::Checkpoint, "from_a").await;
1020 let cb = edit(&b, "c", RecordKind::Checkpoint, "from_b").await;
1021
1022 let report = sync(&a, &b).await.unwrap();
1023
1024 assert_eq!(report.conflicts.len(), 1);
1025 assert!(report.fast_forwarded_to_a.is_empty() && report.fast_forwarded_to_b.is_empty());
1026 assert_eq!(a.get(&k("c")).await.unwrap().unwrap().revision, ca);
1027 assert_eq!(b.get(&k("c")).await.unwrap().unwrap().revision, cb);
1028 }
1029
1030 #[tokio::test]
1031 async fn legacy_record_without_ancestors_takes_the_merge_path() {
1032 let a = MemStore::new();
1033 let b = MemStore::new();
1034 let r0 = commit(&a, rec("l", RecordKind::Topic, "base\n"), None).await;
1035 let mut legacy = rec("l", RecordKind::Topic, "base\nmore\n");
1038 legacy.revision = r0.next(b"base\nmore\n");
1039 legacy.parent = Some(r0);
1040 let _ = commit(&b, legacy, None).await;
1041 assert!(
1042 b.get_raw(&k("l"))
1043 .await
1044 .unwrap()
1045 .unwrap()
1046 .ancestors
1047 .is_empty()
1048 );
1049
1050 let report = sync(&a, &b).await.unwrap();
1051
1052 assert_eq!(
1053 report.merged,
1054 vec![k("l")],
1055 "no chain: merge, not fast-forward"
1056 );
1057 assert!(report.fast_forwarded_to_a.is_empty() && report.fast_forwarded_to_b.is_empty());
1058 assert_eq!(
1059 a.get(&k("l")).await.unwrap().unwrap().body.bytes(),
1060 b"base\nmore\n"
1061 );
1062 }
1063
1064 #[tokio::test]
1065 async fn merge_folds_both_sides_ancestors() {
1066 let a = MemStore::new();
1067 let b = MemStore::new();
1068 let r0 = commit(&a, rec("t", RecordKind::Topic, "base\n"), None).await;
1069 let _ = sync(&a, &b).await.unwrap();
1070 let ra = edit(&a, "t", RecordKind::Topic, "base\nfrom_a\n").await;
1071 let rb = edit(&b, "t", RecordKind::Topic, "base\nfrom_b\n").await;
1072
1073 let report = sync(&a, &b).await.unwrap();
1074
1075 assert_eq!(report.merged, vec![k("t")]);
1076 let ma = a.get_raw(&k("t")).await.unwrap().unwrap();
1077 let mb = b.get_raw(&k("t")).await.unwrap().unwrap();
1078 assert_eq!(ma.revision, mb.revision);
1079 assert_eq!(ma.ancestors, mb.ancestors, "byte-identical on both sides");
1080 assert!(ma.ancestors.contains(&ra));
1081 assert!(ma.ancestors.contains(&rb));
1082 assert!(ma.ancestors.contains(&r0));
1083 assert!(!ma.ancestors.contains(&ma.revision));
1084 }
1085
1086 #[tokio::test]
1087 async fn truncated_chain_never_overwrites() {
1088 let a = MemStore::new().with_ancestor_cap(2);
1089 let b = MemStore::new();
1090 let c0 = commit(&a, rec("c", RecordKind::Checkpoint, "c0"), None).await;
1091 let _ = sync(&a, &b).await.unwrap();
1092 let _ = edit(&a, "c", RecordKind::Checkpoint, "c1").await;
1094 let _ = edit(&a, "c", RecordKind::Checkpoint, "c2").await;
1095 let _ = edit(&a, "c", RecordKind::Checkpoint, "c3").await;
1096 let ra = a.get_raw(&k("c")).await.unwrap().unwrap();
1097 assert_eq!(ra.ancestors.len(), 2);
1098 assert!(!ra.ancestors.contains(&c0));
1099
1100 let report = sync(&a, &b).await.unwrap();
1101
1102 assert_eq!(report.conflicts.len(), 1);
1104 assert!(report.fast_forwarded_to_b.is_empty());
1105 assert_eq!(b.get(&k("c")).await.unwrap().unwrap().revision, c0);
1106 }
1107
1108 #[tokio::test]
1109 async fn mixed_ancestor_caps_converge() {
1110 let a = MemStore::new().with_ancestor_cap(4);
1111 let b = MemStore::new().with_ancestor_cap(32);
1112 let _ = commit(&a, rec("t", RecordKind::Topic, "0\n"), None).await;
1113 let _ = sync(&a, &b).await.unwrap();
1114
1115 for i in 1..=10 {
1117 let _ = edit(&b, "t", RecordKind::Topic, &format!("{i}\n")).await;
1118 }
1119 let report = sync(&a, &b).await.unwrap();
1120 assert!(report.conflicts.is_empty());
1121 assert_eq!(report.fast_forwarded_to_a, vec![k("t")]);
1122 assert_eq!(
1123 a.get_raw(&k("t")).await.unwrap().unwrap().ancestors.len(),
1124 4
1125 );
1126
1127 let _ = edit(&a, "t", RecordKind::Topic, "11\n").await;
1129 let r12 = edit(&a, "t", RecordKind::Topic, "12\n").await;
1130 let report = sync(&a, &b).await.unwrap();
1131 assert!(report.conflicts.is_empty());
1132 assert_eq!(report.fast_forwarded_to_b, vec![k("t")]);
1133
1134 let ra = a.get_raw(&k("t")).await.unwrap().unwrap();
1135 let rb = b.get_raw(&k("t")).await.unwrap().unwrap();
1136 assert_eq!(ra.revision, r12);
1137 assert_eq!(rb.revision, r12);
1138 assert_eq!(ra.ancestors.len(), 4, "A truncates to its own cap");
1139 assert_eq!(
1140 rb.ancestors.len(),
1141 12,
1142 "B keeps r0..r11 under its larger cap"
1143 );
1144 assert_eq!(sync(&a, &b).await.unwrap(), SyncReport::default());
1145 }
1146
1147 #[tokio::test]
1150 async fn tombstone_reaches_a_peer_that_races_the_first_overwrite() {
1151 let a = MemStore::new();
1152 let b = RacyStore::flaky_once();
1153 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
1154 let _ = sync(&a, &b).await.unwrap(); let tomb = delete(&a, "d", r0).await;
1156
1157 let report = sync(&a, &b).await.unwrap();
1158
1159 assert!(report.conflicts.is_empty());
1160 assert_eq!(report.fast_forwarded_to_b, vec![k("d")]);
1161 let tb = b.get_raw(&k("d")).await.unwrap().unwrap();
1162 assert!(tb.is_tombstone());
1163 assert_eq!(tb.revision, tomb.revision);
1164 }
1165
1166 #[tokio::test]
1167 async fn tombstone_sync_against_an_always_racing_peer_terminates() {
1168 let a = MemStore::new();
1169 let b = RacyStore::always();
1170 let r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
1171 let _ = commit(&b, rec("d", RecordKind::Topic, "v0\n"), None).await;
1172 let tomb = delete(&a, "d", r0.clone()).await;
1173
1174 let report = sync(&a, &b).await.unwrap();
1176
1177 assert!(report.fast_forwarded_to_b.is_empty());
1178 assert!(report.conflicts.is_empty());
1179 assert_eq!(b.get(&k("d")).await.unwrap().unwrap().revision, r0);
1180 assert_eq!(
1181 a.get_raw(&k("d")).await.unwrap().unwrap().revision,
1182 tomb.revision
1183 );
1184
1185 assert!(
1188 !report.converged(),
1189 "an exhausted run must not look converged"
1190 );
1191 assert_eq!(report.unconverged, vec![k("d")]);
1192 }
1193
1194 #[tokio::test]
1195 async fn a_settled_pair_reports_convergence() {
1196 let a = MemStore::new();
1198 let b = MemStore::new();
1199 commit(&a, rec("x", RecordKind::Topic, "v0\n"), None).await;
1200
1201 let report = sync(&a, &b).await.unwrap();
1202
1203 assert_eq!(report.copied_to_b, vec![k("x")]);
1204 assert!(report.converged());
1205 assert!(report.unconverged.is_empty());
1206 }
1207
1208 #[tokio::test]
1209 async fn a_pass_that_settles_after_a_race_reports_convergence() {
1210 let a = MemStore::new();
1214 let b = RacyStore::flaky_once();
1215 let _ = a
1216 .put(rec("t", RecordKind::Topic, "base\nfrom_a\n"), None)
1217 .await
1218 .unwrap();
1219 let _ = b
1220 .put(rec("t", RecordKind::Topic, "base\nfrom_b\n"), None)
1221 .await
1222 .unwrap();
1223
1224 let report = sync(&a, &b).await.unwrap();
1225
1226 assert!(report.conflicts.is_empty());
1227 assert!(report.converged(), "the retry pass landed cleanly");
1228 assert!(report.unconverged.is_empty());
1229 }
1230
1231 #[tokio::test]
1234 async fn overwrite_treats_not_found_and_conflict_as_a_race() {
1235 let b = MemStore::new();
1236 let stray = rec("x", RecordKind::Topic, "x\n");
1238 assert!(
1239 !overwrite(&b, &stray, &Revision::initial(b"gone"))
1240 .await
1241 .unwrap()
1242 );
1243 assert!(b.get_raw(&k("x")).await.unwrap().is_none());
1244
1245 let r0 = commit(&b, rec("d", RecordKind::Topic, "v0\n"), None).await;
1247 let tomb = delete(&b, "d", r0).await;
1248 let incoming = rec("d", RecordKind::Topic, "v1\n");
1249 assert!(
1250 !overwrite(&b, &incoming, &Revision::initial(b"stale"))
1251 .await
1252 .unwrap()
1253 );
1254 assert_eq!(
1255 b.get_raw(&k("d")).await.unwrap().unwrap().revision,
1256 tomb.revision
1257 );
1258 }
1259
1260 #[tokio::test]
1261 async fn copy_over_a_tombstone_conflicts_and_writes_nothing() {
1262 let b = MemStore::new();
1263 let r0 = commit(&b, rec("d", RecordKind::Topic, "v0\n"), None).await;
1264 let tomb = delete(&b, "d", r0).await;
1265 assert!(
1267 !copy(&b, &rec("d", RecordKind::Topic, "v0\n"))
1268 .await
1269 .unwrap()
1270 );
1271 let still = b.get_raw(&k("d")).await.unwrap().unwrap();
1272 assert!(still.is_tombstone(), "put_raw never recreates");
1273 assert_eq!(still.revision, tomb.revision);
1274 }
1275
1276 #[tokio::test]
1277 async fn tombstone_arriving_mid_copy_is_not_resurrected() {
1278 let c = MemStore::new();
1282 let r0 = commit(&c, rec("d", RecordKind::Topic, "v0\n"), None).await;
1283 let tomb = delete(&c, "d", r0.clone()).await;
1284
1285 let a = MemStore::new();
1286 let a_r0 = commit(&a, rec("d", RecordKind::Topic, "v0\n"), None).await;
1287 assert_eq!(a_r0, r0);
1288 let b = RacyStore::tombstone_arrives(tomb.clone());
1289
1290 let report = sync(&a, &b).await.unwrap();
1291
1292 assert!(report.conflicts.is_empty());
1295 assert!(report.copied_to_b.is_empty());
1296 assert_eq!(report.fast_forwarded_to_a, vec![k("d")]);
1297 for store in [&a as &dyn Store, &b as &dyn Store] {
1298 assert!(
1299 store.get(&k("d")).await.unwrap().is_none(),
1300 "not resurrected"
1301 );
1302 let got = store.get_raw(&k("d")).await.unwrap().unwrap();
1303 assert!(got.is_tombstone());
1304 assert_eq!(got.revision, tomb.revision);
1305 }
1306 assert_eq!(sync(&a, &b).await.unwrap(), SyncReport::default());
1307 }
1308}