|
|
… |
268 unchanged lines |
| 269 |
269 |
|
while !pending.is_empty() { |
| 270 |
270 |
|
let mut deferred = Vec::new(); |
| 271 |
271 |
|
let mut last_error = None; |
|
272 |
+ |
let before = pending.len(); |
| 272 |
273 |
|
for key in pending.drain(..) { |
| 273 |
|
- |
let bytes = self.store.get(&key)?; |
|
274 |
+ |
let bytes = match self.store.get(&key) { |
|
275 |
+ |
Ok(b) => b, |
|
276 |
+ |
// Listed, then gone: a gc merged it into a newer |
|
277 |
+ |
// bundle, which this listing holds too. |
|
278 |
+ |
Err(StoreError::NotFound(_)) => continue, |
|
279 |
+ |
Err(e) => return Err(e.into()), |
|
280 |
+ |
}; |
| 274 |
281 |
|
let bundle = wire::decode_bundle(&bytes) |
| 275 |
282 |
|
.map_err(|e| RepoError::Msg(format!("{}: {key}: {e}", self.what())))?; |
| 276 |
283 |
|
match mirror.import_bundle(&bundle, strict) { |
|
|
… |
5 unchanged lines |
| 282 |
289 |
|
Err(e) => return Err(RepoError::Msg(format!("{}: {key}: {e}", self.what()))), |
| 283 |
290 |
|
} |
| 284 |
291 |
|
} |
| 285 |
|
- |
if deferred.len() == pending.len() + deferred.len() && !deferred.is_empty() { |
|
292 |
+ |
if deferred.len() == before { |
| 286 |
293 |
|
// No progress: something those bundles need was never |
| 287 |
294 |
|
// pushed, or a push is still in progress. |
| 288 |
295 |
|
return fail(format!( |
|
|
… |
167 unchanged lines |
| 456 |
463 |
|
)); |
| 457 |
464 |
|
} |
| 458 |
465 |
|
} |
| 459 |
|
- |
let (before_ref, before) = self.with_mirror(|m| m.head_op())?; |
| 460 |
|
- |
let results = |
| 461 |
|
- |
self.with_mirror(|m| m.receive_push(bundle.clone(), updates, policy, tags))?; |
| 462 |
|
- |
if !results.iter().any(|r| r.ok) { |
| 463 |
|
- |
return Ok(results); |
|
466 |
+ |
// The bundle is checked against the mirror and uploaded once; the |
|
467 |
+ |
// record is then moved from the version read. If it moved |
|
468 |
+ |
// meanwhile (another push landed), the mirror is put back, brought |
|
469 |
+ |
// level again, and the updates applied over what landed: two |
|
470 |
+ |
// pushes to different bookmarks both get through, and one to the |
|
471 |
+ |
// same bookmark is told it is stale, as a hub would say. |
|
472 |
+ |
let mut uploaded = false; |
|
473 |
+ |
for attempt in 0..PUSH_ATTEMPTS { |
|
474 |
+ |
let (before_ref, before) = self.with_mirror(|m| m.head_op())?; |
|
475 |
+ |
let results = self.with_mirror(|m| { |
|
476 |
+ |
m.receive_push( |
|
477 |
+ |
bundle.clone(), |
|
478 |
+ |
updates.clone(), |
|
479 |
+ |
policy.clone(), |
|
480 |
+ |
tags.clone(), |
|
481 |
+ |
) |
|
482 |
+ |
})?; |
|
483 |
+ |
if !results.iter().any(|r| r.ok) { |
|
484 |
+ |
return Ok(results); |
|
485 |
+ |
} |
|
486 |
+ |
if !bundle.is_empty() && !uploaded { |
|
487 |
+ |
self.upload(&bundle)?; |
|
488 |
+ |
uploaded = true; |
|
489 |
+ |
} |
|
490 |
+ |
let record = self.record(id)?; |
|
491 |
+ |
match self |
|
492 |
+ |
.store |
|
493 |
+ |
.write_ref(REFS_KEY, &record, synced.version.as_ref()) |
|
494 |
+ |
{ |
|
495 |
+ |
Ok(_) => return Ok(results), |
|
496 |
+ |
Err(StoreError::Conflict(_)) => { |
|
497 |
+ |
self.with_mirror(|m| { |
|
498 |
+ |
let (now, _) = m.head_op()?; |
|
499 |
+ |
if now != before_ref { |
|
500 |
+ |
m.record_op( |
|
501 |
+ |
&[now], |
|
502 |
+ |
"strata push undone: the remote moved", |
|
503 |
+ |
before.view.clone(), |
|
504 |
+ |
)?; |
|
505 |
+ |
} |
|
506 |
+ |
Ok(()) |
|
507 |
+ |
})?; |
|
508 |
+ |
synced = self.sync()?; |
|
509 |
+ |
if attempt + 1 == PUSH_ATTEMPTS { |
|
510 |
+ |
return Ok(results |
|
511 |
+ |
.into_iter() |
|
512 |
+ |
.map(|r| RefResult { |
|
513 |
+ |
name: r.name, |
|
514 |
+ |
ok: false, |
|
515 |
+ |
reason: "the remote kept moving while pushing (other pushes landed first); pull, then push again".into(), |
|
516 |
+ |
}) |
|
517 |
+ |
.collect()); |
|
518 |
+ |
} |
|
519 |
+ |
} |
|
520 |
+ |
Err(e) => return Err(e.into()), |
|
521 |
+ |
} |
| 464 |
522 |
|
} |
| 465 |
|
- |
if !bundle.is_empty() { |
| 466 |
|
- |
self.upload(&bundle)?; |
|
523 |
+ |
unreachable!("the loop returns") |
|
524 |
+ |
} |
|
525 |
+ |
} |
|
526 |
+ |
|
|
527 |
+ |
/// How often a push is applied over what landed meanwhile before giving up. |
|
528 |
+ |
const PUSH_ATTEMPTS: usize = 4; |
|
529 |
+ |
|
|
530 |
+ |
/// The grace period before `gc` removes a bundle it merged: a reader that |
|
531 |
+ |
/// listed it before the merge can still fetch it. |
|
532 |
+ |
pub const REMOTE_GC_GRACE: std::time::Duration = std::time::Duration::from_secs(7 * 86_400); |
|
533 |
+ |
|
|
534 |
+ |
/// Key of the record naming the bundles a gc merged, with when. |
|
535 |
+ |
const TRASH_KEY: &str = "gc"; |
|
536 |
+ |
|
|
537 |
+ |
/// The superseded bundles and when (milliseconds) each was superseded. |
|
538 |
+ |
type Trash = Vec<(String, u64)>; |
|
539 |
+ |
|
|
540 |
+ |
/// What a gc against a store did. |
|
541 |
+ |
#[derive(Clone, Debug, Default, PartialEq, Eq)] |
|
542 |
+ |
pub struct RemoteGc { |
|
543 |
+ |
/// Bundles folded into one new bundle. |
|
544 |
+ |
pub merged: usize, |
|
545 |
+ |
/// The merged bundle's size, bytes. |
|
546 |
+ |
pub bundle_bytes: usize, |
|
547 |
+ |
/// Changes in the merged bundle. |
|
548 |
+ |
pub changes: usize, |
|
549 |
+ |
/// Superseded bundles removed, past the grace period. |
|
550 |
+ |
pub deleted: usize, |
|
551 |
+ |
/// Superseded bundles kept for now by the grace period. |
|
552 |
+ |
pub deferred: usize, |
|
553 |
+ |
} |
|
554 |
+ |
|
|
555 |
+ |
fn now_ms() -> u64 { |
|
556 |
+ |
std::time::SystemTime::now() |
|
557 |
+ |
.duration_since(std::time::UNIX_EPOCH) |
|
558 |
+ |
.map(|d| d.as_millis() as u64) |
|
559 |
+ |
.unwrap_or(0) |
|
560 |
+ |
} |
|
561 |
+ |
|
|
562 |
+ |
impl BucketRemote { |
|
563 |
+ |
/// The bundles a gc superseded, with when: one `KEY\tMILLIS` per line. |
|
564 |
+ |
fn trash(&self) -> Result<(Trash, Option<Version>)> { |
|
565 |
+ |
match self.store.read_ref(TRASH_KEY)? { |
|
566 |
+ |
Some((bytes, v)) => { |
|
567 |
+ |
let text = String::from_utf8_lossy(&bytes); |
|
568 |
+ |
let list = text |
|
569 |
+ |
.lines() |
|
570 |
+ |
.filter_map(|l| { |
|
571 |
+ |
let (k, t) = l.split_once('\t')?; |
|
572 |
+ |
Some((k.to_string(), t.parse().ok()?)) |
|
573 |
+ |
}) |
|
574 |
+ |
.collect(); |
|
575 |
+ |
Ok((list, Some(v))) |
|
576 |
+ |
} |
|
577 |
+ |
None => Ok((Vec::new(), None)), |
| 467 |
578 |
|
} |
| 468 |
|
- |
let record = self.record(id)?; |
| 469 |
|
- |
match self |
|
579 |
+ |
} |
|
580 |
+ |
|
|
581 |
+ |
fn write_trash(&self, list: &[(String, u64)], expected: Option<&Version>) -> Result<Version> { |
|
582 |
+ |
let text: String = list.iter().map(|(k, t)| format!("{k}\t{t}\n")).collect(); |
|
583 |
+ |
Ok(self.store.write_ref(TRASH_KEY, text.as_bytes(), expected)?) |
|
584 |
+ |
} |
|
585 |
+ |
|
|
586 |
+ |
/// Fold every bundle into one holding what the record reaches, and |
|
587 |
+ |
/// remove the bundles that made a gc redundant once `grace` has |
|
588 |
+ |
/// passed since they were superseded (stores.md §6). |
|
589 |
+ |
pub fn gc(&self, grace: std::time::Duration) -> Result<RemoteGc> { |
|
590 |
+ |
let synced = self.sync()?; |
|
591 |
+ |
let Some(id) = synced.id else { |
|
592 |
+ |
return fail(format!("{} holds no repository yet", self.what())); |
|
593 |
+ |
}; |
|
594 |
+ |
let mut report = RemoteGc::default(); |
|
595 |
+ |
let existing: Vec<String> = self |
| 470 |
596 |
|
.store |
| 471 |
|
- |
.write_ref(REFS_KEY, &record, synced.version.as_ref()) |
| 472 |
|
- |
{ |
| 473 |
|
- |
Ok(_) => Ok(results), |
| 474 |
|
- |
Err(StoreError::Conflict(_)) => { |
| 475 |
|
- |
// Someone else moved the record meanwhile. The bundle is in |
| 476 |
|
- |
// the store and harmless; the mirror goes back to what the |
| 477 |
|
- |
// store says, and the push is reported as a hub would. |
| 478 |
|
- |
self.with_mirror(|m| { |
| 479 |
|
- |
let (now, _) = m.head_op()?; |
| 480 |
|
- |
if now != before_ref { |
| 481 |
|
- |
m.record_op( |
| 482 |
|
- |
&[now], |
| 483 |
|
- |
"strata push undone: the remote moved", |
| 484 |
|
- |
before.view.clone(), |
| 485 |
|
- |
)?; |
| 486 |
|
- |
} |
| 487 |
|
- |
Ok(()) |
| 488 |
|
- |
})?; |
| 489 |
|
- |
let _ = self.sync(); |
| 490 |
|
- |
Ok(results |
| 491 |
|
- |
.into_iter() |
| 492 |
|
- |
.map(|r| RefResult { |
| 493 |
|
- |
name: r.name, |
| 494 |
|
- |
ok: false, |
| 495 |
|
- |
reason: "the remote moved while pushing (another push landed first); pull, then push again".into(), |
| 496 |
|
- |
}) |
| 497 |
|
- |
.collect()) |
|
597 |
+ |
.list(BUNDLES_PREFIX)? |
|
598 |
+ |
.into_iter() |
|
599 |
+ |
.map(|e| e.key) |
|
600 |
+ |
.collect(); |
|
601 |
+ |
let (mut trash, mut trash_version) = self.trash()?; |
|
602 |
+ |
let live: Vec<&String> = existing |
|
603 |
+ |
.iter() |
|
604 |
+ |
.filter(|k| !trash.iter().any(|(t, _)| t == *k)) |
|
605 |
+ |
.collect(); |
|
606 |
+ |
if live.len() > 1 { |
|
607 |
+ |
// Everything the record reaches, in dependency order, with |
|
608 |
+ |
// the state objects the bookmarks and tags name. |
|
609 |
+ |
let (bundle, changes) = self.with_mirror(|m| { |
|
610 |
+ |
let resp = m.remote_refs()?; |
|
611 |
+ |
let mut heads: Vec<ChangeHash> = Vec::new(); |
|
612 |
+ |
for r in &resp.refs { |
|
613 |
+ |
heads.extend(r.heads.iter().copied()); |
|
614 |
+ |
} |
|
615 |
+ |
for t in &resp.tags { |
|
616 |
+ |
heads.extend(t.heads.iter().copied()); |
|
617 |
+ |
} |
|
618 |
+ |
heads.sort(); |
|
619 |
+ |
heads.dedup(); |
|
620 |
+ |
let changes = m.negotiate(&[], &heads)?; |
|
621 |
+ |
let mut bundle = m.bundle_for(&changes)?; |
|
622 |
+ |
for r in &resp.refs { |
|
623 |
+ |
bundle.objects.push(( |
|
624 |
+ |
ObjectType::State, |
|
625 |
+ |
StateObject::new(r.heads.clone()).encode(), |
|
626 |
+ |
)); |
|
627 |
+ |
} |
|
628 |
+ |
for t in &resp.tags { |
|
629 |
+ |
bundle.objects.push(( |
|
630 |
+ |
ObjectType::State, |
|
631 |
+ |
StateObject::new(t.heads.clone()).encode(), |
|
632 |
+ |
)); |
|
633 |
+ |
} |
|
634 |
+ |
Ok((bundle, changes.len())) |
|
635 |
+ |
})?; |
|
636 |
+ |
report.changes = changes; |
|
637 |
+ |
report.bundle_bytes = wire::encode_bundle(&bundle).len(); |
|
638 |
+ |
let merged_key = self.upload(&bundle)?; |
|
639 |
+ |
let now = now_ms(); |
|
640 |
+ |
for k in live { |
|
641 |
+ |
if *k != merged_key { |
|
642 |
+ |
trash.push((k.clone(), now)); |
|
643 |
+ |
report.merged += 1; |
|
644 |
+ |
} |
|
645 |
+ |
} |
|
646 |
+ |
trash_version = Some(self.write_trash(&trash, trash_version.as_ref())?); |
|
647 |
+ |
} |
|
648 |
+ |
// What the grace period has released. |
|
649 |
+ |
let now = now_ms(); |
|
650 |
+ |
let mut kept = Vec::new(); |
|
651 |
+ |
for (k, when) in trash { |
|
652 |
+ |
if now.saturating_sub(when) >= grace.as_millis() as u64 { |
|
653 |
+ |
self.store.delete(&k)?; |
|
654 |
+ |
report.deleted += 1; |
|
655 |
+ |
} else { |
|
656 |
+ |
kept.push((k, when)); |
|
657 |
+ |
report.deferred += 1; |
| 498 |
658 |
|
} |
| 499 |
|
- |
Err(e) => Err(e.into()), |
|
659 |
+ |
} |
|
660 |
+ |
if report.deleted > 0 { |
|
661 |
+ |
self.write_trash(&kept, trash_version.as_ref())?; |
| 500 |
662 |
|
} |
|
663 |
+ |
let _ = id; |
|
664 |
+ |
Ok(report) |
| 501 |
665 |
|
} |
| 502 |
666 |
|
} |
| 503 |
667 |
|
|
|
668 |
+ |
impl Repo { |
|
669 |
+ |
/// `oxy gc --remote NAME`: a gc against a remote kept on a store. |
|
670 |
+ |
pub fn gc_remote(&self, name: &str, now: bool) -> Result<RemoteGc> { |
|
671 |
+ |
let Some(url) = self.config.remotes.get(name).cloned() else { |
|
672 |
+ |
return fail(format!("no remote {name:?}")); |
|
673 |
+ |
}; |
|
674 |
+ |
let options = self.remote_options(name); |
|
675 |
+ |
let Some(store) = crate::protocol::open_store(&url, &options) else { |
|
676 |
+ |
return fail(format!( |
|
677 |
+ |
"gc --remote works on a remote kept on a store (s3://, gs://, az://, dir://); {name} is {url}" |
|
678 |
+ |
)); |
|
679 |
+ |
}; |
|
680 |
+ |
let remote = BucketRemote::open(store?, &options)?; |
|
681 |
+ |
let grace = if now { |
|
682 |
+ |
std::time::Duration::ZERO |
|
683 |
+ |
} else { |
|
684 |
+ |
REMOTE_GC_GRACE |
|
685 |
+ |
}; |
|
686 |
+ |
remote.gc(grace) |
|
687 |
+ |
} |
|
688 |
+ |
} |
|
689 |
+ |
|
| 504 |
690 |
|
/// Open a remote on `store` for the local repository described by `options`. |
| 505 |
691 |
|
pub fn open_store_remote( |
| 506 |
692 |
|
store: Box<dyn Store>, |
|
|
… |
228 unchanged lines |
| 735 |
921 |
|
} |
| 736 |
922 |
|
|
| 737 |
923 |
|
#[test] |
| 738 |
|
- |
fn a_record_moved_during_a_push_fails_the_push_and_keeps_the_mirror_honest() { |
|
924 |
+ |
fn a_record_moved_during_a_push_is_applied_over_what_landed() { |
| 739 |
925 |
|
hermetic(); |
| 740 |
926 |
|
let dir = tempfile::tempdir().unwrap(); |
| 741 |
927 |
|
let root = dir.path(); |
|
|
… |
39 unchanged lines |
| 781 |
967 |
|
let r = remote |
| 782 |
968 |
|
.push(bundle, vec![update(state, state2)], None, vec![]) |
| 783 |
969 |
|
.unwrap(); |
| 784 |
|
- |
assert!( |
| 785 |
|
- |
!r[0].ok && r[0].reason.contains("moved while pushing"), |
| 786 |
|
- |
"{r:?}" |
| 787 |
|
- |
); |
|
970 |
+ |
assert!(r[0].ok, "applied over what landed meanwhile: {r:?}"); |
| 788 |
971 |
|
let refs = remote.refs().unwrap(); |
| 789 |
972 |
|
assert_eq!( |
| 790 |
973 |
|
refs.refs[0].heads, |
| 791 |
|
- |
vec![h1], |
| 792 |
|
- |
"the mirror says what the store says" |
|
974 |
+ |
vec![h2], |
|
975 |
+ |
"the store and the mirror both moved" |
| 793 |
976 |
|
); |
| 794 |
977 |
|
} |
| 795 |
978 |
|
|
|
|
… |
55 unchanged lines |
| 851 |
1034 |
|
"{e}" |
| 852 |
1035 |
|
); |
| 853 |
1036 |
|
assert!(!root.join("b/f.txt").exists()); |
|
1037 |
+ |
} |
|
1038 |
+ |
|
|
1039 |
+ |
/// A store shared by two remotes in one test, with a hook run once at |
|
1040 |
+ |
/// the first conditional write: what another push landing between a |
|
1041 |
+ |
/// read of the record and its compare-and-swap looks like. |
|
1042 |
+ |
struct Shared { |
|
1043 |
+ |
inner: std::sync::Arc<MemoryStore>, |
|
1044 |
+ |
fail_uploads: std::sync::atomic::AtomicBool, |
|
1045 |
+ |
vanish: Mutex<Option<String>>, |
|
1046 |
+ |
} |
|
1047 |
+ |
|
|
1048 |
+ |
thread_local! { |
|
1049 |
+ |
static HOOK: std::cell::RefCell<Option<Box<dyn FnOnce()>>> = const { std::cell::RefCell::new(None) }; |
|
1050 |
+ |
} |
|
1051 |
+ |
|
|
1052 |
+ |
impl Shared { |
|
1053 |
+ |
fn new(inner: std::sync::Arc<MemoryStore>) -> Box<Shared> { |
|
1054 |
+ |
Box::new(Shared { |
|
1055 |
+ |
inner, |
|
1056 |
+ |
fail_uploads: Default::default(), |
|
1057 |
+ |
vanish: Mutex::new(None), |
|
1058 |
+ |
}) |
|
1059 |
+ |
} |
|
1060 |
+ |
} |
|
1061 |
+ |
|
|
1062 |
+ |
impl Store for Shared { |
|
1063 |
+ |
fn describe(&self) -> String { |
|
1064 |
+ |
"shared".into() |
|
1065 |
+ |
} |
|
1066 |
+ |
fn has(&self, key: &str) -> crate::strata::Result<bool> { |
|
1067 |
+ |
self.inner.has(key) |
|
1068 |
+ |
} |
|
1069 |
+ |
fn get(&self, key: &str) -> crate::strata::Result<Vec<u8>> { |
|
1070 |
+ |
let mut vanish = self.vanish.lock().unwrap(); |
|
1071 |
+ |
if vanish.as_deref() == Some(key) { |
|
1072 |
+ |
*vanish = None; |
|
1073 |
+ |
return Err(StoreError::NotFound(key.to_string())); |
|
1074 |
+ |
} |
|
1075 |
+ |
self.inner.get(key) |
|
1076 |
+ |
} |
|
1077 |
+ |
fn get_range(&self, key: &str, range: Range<u64>) -> crate::strata::Result<Vec<u8>> { |
|
1078 |
+ |
self.inner.get_range(key, range) |
|
1079 |
+ |
} |
|
1080 |
+ |
fn put_new(&self, key: &str, bytes: &[u8]) -> crate::strata::Result<()> { |
|
1081 |
+ |
if key.starts_with("bundles/") |
|
1082 |
+ |
&& self.fail_uploads.load(std::sync::atomic::Ordering::SeqCst) |
|
1083 |
+ |
{ |
|
1084 |
+ |
return Err(StoreError::Backend("shared: the disk is full".into())); |
|
1085 |
+ |
} |
|
1086 |
+ |
self.inner.put_new(key, bytes) |
|
1087 |
+ |
} |
|
1088 |
+ |
fn list(&self, prefix: &str) -> crate::strata::Result<Vec<Entry>> { |
|
1089 |
+ |
self.inner.list(prefix) |
|
1090 |
+ |
} |
|
1091 |
+ |
fn delete(&self, key: &str) -> crate::strata::Result<()> { |
|
1092 |
+ |
self.inner.delete(key) |
|
1093 |
+ |
} |
|
1094 |
+ |
fn read_ref(&self, key: &str) -> crate::strata::Result<Option<(Vec<u8>, Version)>> { |
|
1095 |
+ |
self.inner.read_ref(key) |
|
1096 |
+ |
} |
|
1097 |
+ |
fn write_ref( |
|
1098 |
+ |
&self, |
|
1099 |
+ |
key: &str, |
|
1100 |
+ |
bytes: &[u8], |
|
1101 |
+ |
expected: Option<&Version>, |
|
1102 |
+ |
) -> crate::strata::Result<Version> { |
|
1103 |
+ |
if key == "refs" && expected.is_some() { |
|
1104 |
+ |
if let Some(hook) = HOOK.with(|h| h.borrow_mut().take()) { |
|
1105 |
+ |
hook(); |
|
1106 |
+ |
} |
|
1107 |
+ |
} |
|
1108 |
+ |
self.inner.write_ref(key, bytes, expected) |
|
1109 |
+ |
} |
|
1110 |
+ |
} |
|
1111 |
+ |
|
|
1112 |
+ |
fn repo_with_commit(root: &Path, name: &str, file: &str, text: &[u8], msg: &str) -> Repo { |
|
1113 |
+ |
let repo = Repo::init(&root.join(name)).unwrap(); |
|
1114 |
+ |
write(&root.join(name), file, text); |
|
1115 |
+ |
repo.commit(msg, &[]).unwrap(); |
|
1116 |
+ |
repo |
|
1117 |
+ |
} |
|
1118 |
+ |
|
|
1119 |
+ |
fn remote_for(repo: &Repo, store: Box<dyn Store>, root: &Path, name: &str) -> BucketRemote { |
|
1120 |
+ |
let options = RemoteOptions { |
|
1121 |
+ |
repo_id: Some(repo.id().unwrap()), |
|
1122 |
+ |
cache_dir: Some(root.join(format!("cache-{name}"))), |
|
1123 |
+ |
..Default::default() |
|
1124 |
+ |
}; |
|
1125 |
+ |
BucketRemote::open(store, &options).unwrap() |
|
1126 |
+ |
} |
|
1127 |
+ |
|
|
1128 |
+ |
fn push_bookmark( |
|
1129 |
+ |
repo: &Repo, |
|
1130 |
+ |
remote: &BucketRemote, |
|
1131 |
+ |
bookmark: &str, |
|
1132 |
+ |
old: Option<ObjectRef>, |
|
1133 |
+ |
) -> Vec<RefResult> { |
|
1134 |
+ |
let heads = repo.heads_of(&repo.view().unwrap(), bookmark).unwrap(); |
|
1135 |
+ |
let state = repo |
|
1136 |
+ |
.store |
|
1137 |
+ |
.put_state(&StateObject::new(heads.clone())) |
|
1138 |
+ |
.unwrap(); |
|
1139 |
+ |
let empty = repo.store.put_state(&StateObject::new(vec![])).unwrap(); |
|
1140 |
+ |
// Only what the remote lacks, as push_bookmark sends. |
|
1141 |
+ |
let have: Vec<ChangeHash> = remote |
|
1142 |
+ |
.refs() |
|
1143 |
+ |
.unwrap() |
|
1144 |
+ |
.refs |
|
1145 |
+ |
.iter() |
|
1146 |
+ |
.flat_map(|r| r.heads.clone()) |
|
1147 |
+ |
.collect(); |
|
1148 |
+ |
let changes = repo.negotiate(&have, &heads).unwrap(); |
|
1149 |
+ |
let mut bundle = repo.bundle_for(&changes).unwrap(); |
|
1150 |
+ |
bundle |
|
1151 |
+ |
.objects |
|
1152 |
+ |
.push((ObjectType::State, StateObject::new(heads).encode())); |
|
1153 |
+ |
let update = RefUpdate { |
|
1154 |
+ |
name: bookmark.into(), |
|
1155 |
+ |
old_state: old.unwrap_or(empty), |
|
1156 |
+ |
new_state: state, |
|
1157 |
+ |
force: false, |
|
1158 |
+ |
attestation: None, |
|
1159 |
+ |
}; |
|
1160 |
+ |
remote.push(bundle, vec![update], None, vec![]).unwrap() |
|
1161 |
+ |
} |
|
1162 |
+ |
|
|
1163 |
+ |
#[test] |
|
1164 |
+ |
fn two_pushes_to_different_bookmarks_both_land() { |
|
1165 |
+ |
hermetic(); |
|
1166 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1167 |
+ |
let root = dir.path(); |
|
1168 |
+ |
let inner = std::sync::Arc::new(MemoryStore::new()); |
|
1169 |
+ |
// A pushes main; B (a clone of the same repository) pushes a |
|
1170 |
+ |
// second bookmark between A's read and A's compare-and-swap. |
|
1171 |
+ |
let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first"); |
|
1172 |
+ |
let ra = remote_for(&a, Shared::new(inner.clone()), root, "a"); |
|
1173 |
+ |
let r = push_bookmark(&a, &ra, "main", None); |
|
1174 |
+ |
assert!(r[0].ok, "{r:?}"); |
|
1175 |
+ |
let b = Repo::init_with_id(&root.join("b"), a.id().unwrap()).unwrap(); |
|
1176 |
+ |
let rb = remote_for(&b, Shared::new(inner.clone()), root, "b"); |
|
1177 |
+ |
rb.refs().unwrap(); |
|
1178 |
+ |
write(&root.join("b"), "g.txt", b"g\n"); |
|
1179 |
+ |
b.commit("on b", &[]).unwrap(); |
|
1180 |
+ |
let (_, op) = b.head_op().unwrap(); |
|
1181 |
+ |
let mut view = op.view.clone(); |
|
1182 |
+ |
let feature_heads = b.heads_of(&view, "main").unwrap(); |
|
1183 |
+ |
view.bookmarks.insert( |
|
1184 |
+ |
"feature".into(), |
|
1185 |
+ |
b.store.put_state(&StateObject::new(feature_heads)).unwrap(), |
|
1186 |
+ |
); |
|
1187 |
+ |
b.record_op(&[b.head_op().unwrap().0], "bookmark create feature", view) |
|
1188 |
+ |
.unwrap(); |
|
1189 |
+ |
// A's second push to main; B's push of feature lands in between. |
|
1190 |
+ |
write(&root.join("a"), "f.txt", b"aa\n"); |
|
1191 |
+ |
a.commit("second", &[]).unwrap(); |
|
1192 |
+ |
let a_old = ra |
|
1193 |
+ |
.refs() |
|
1194 |
+ |
.unwrap() |
|
1195 |
+ |
.refs |
|
1196 |
+ |
.iter() |
|
1197 |
+ |
.find(|r| r.name == "main") |
|
1198 |
+ |
.unwrap() |
|
1199 |
+ |
.state; |
|
1200 |
+ |
let b_remote = std::sync::Arc::new(std::sync::Mutex::new(Some(rb))); |
|
1201 |
+ |
let b_handle = b_remote.clone(); |
|
1202 |
+ |
HOOK.with(|h| { |
|
1203 |
+ |
*h.borrow_mut() = Some(Box::new(move || { |
|
1204 |
+ |
let rb = b_handle.lock().unwrap().take().unwrap(); |
|
1205 |
+ |
let r = push_bookmark(&b, &rb, "feature", None); |
|
1206 |
+ |
assert!(r[0].ok, "{r:?}"); |
|
1207 |
+ |
})) |
|
1208 |
+ |
}); |
|
1209 |
+ |
let r = push_bookmark(&a, &ra, "main", Some(a_old)); |
|
1210 |
+ |
assert!(r[0].ok, "a's push is applied over b's: {r:?}"); |
|
1211 |
+ |
let refs = ra.refs().unwrap(); |
|
1212 |
+ |
let names: Vec<&str> = refs.refs.iter().map(|r| r.name.as_str()).collect(); |
|
1213 |
+ |
assert_eq!(names, ["feature", "main"], "both landed"); |
|
1214 |
+ |
assert_eq!(inner.list("bundles/").unwrap().len(), 3); |
|
1215 |
+ |
let _ = b_remote; |
|
1216 |
+ |
} |
|
1217 |
+ |
|
|
1218 |
+ |
#[test] |
|
1219 |
+ |
fn an_interrupted_push_leaves_the_store_usable() { |
|
1220 |
+ |
hermetic(); |
|
1221 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1222 |
+ |
let root = dir.path(); |
|
1223 |
+ |
let inner = std::sync::Arc::new(MemoryStore::new()); |
|
1224 |
+ |
let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first"); |
|
1225 |
+ |
let shared = Shared::new(inner.clone()); |
|
1226 |
+ |
let fail = &shared.fail_uploads as *const std::sync::atomic::AtomicBool; |
|
1227 |
+ |
let ra = remote_for(&a, shared, root, "a"); |
|
1228 |
+ |
assert!(push_bookmark(&a, &ra, "main", None)[0].ok); |
|
1229 |
+ |
let old = ra.refs().unwrap().refs[0].state; |
|
1230 |
+ |
// The upload fails: nothing in the store changes, the mirror is |
|
1231 |
+ |
// honest, and the next push gets through. |
|
1232 |
+ |
write(&root.join("a"), "f.txt", b"aa\n"); |
|
1233 |
+ |
let h2 = a.commit("second", &[]).unwrap(); |
|
1234 |
+ |
unsafe { (*fail).store(true, std::sync::atomic::Ordering::SeqCst) }; |
|
1235 |
+ |
let e = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
|
1236 |
+ |
push_bookmark(&a, &ra, "main", Some(old)) |
|
1237 |
+ |
})); |
|
1238 |
+ |
assert!(e.is_err(), "the push reports the failed upload"); |
|
1239 |
+ |
assert_eq!(inner.list("bundles/").unwrap().len(), 1); |
|
1240 |
+ |
let refs = ra.refs().unwrap(); |
|
1241 |
+ |
assert_eq!(refs.refs[0].state, old, "the record did not move"); |
|
1242 |
+ |
unsafe { (*fail).store(false, std::sync::atomic::Ordering::SeqCst) }; |
|
1243 |
+ |
let r = push_bookmark(&a, &ra, "main", Some(old)); |
|
1244 |
+ |
assert!(r[0].ok, "{r:?}"); |
|
1245 |
+ |
assert_eq!(ra.refs().unwrap().refs[0].heads, vec![h2]); |
|
1246 |
+ |
} |
|
1247 |
+ |
|
|
1248 |
+ |
#[test] |
|
1249 |
+ |
fn bundles_out_of_order_or_gone_are_handled() { |
|
1250 |
+ |
hermetic(); |
|
1251 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1252 |
+ |
let root = dir.path(); |
|
1253 |
+ |
let inner = std::sync::Arc::new(MemoryStore::new()); |
|
1254 |
+ |
let a = repo_with_commit(root, "a", "f.txt", b"a\n", "first"); |
|
1255 |
+ |
let ra = remote_for(&a, Shared::new(inner.clone()), root, "a"); |
|
1256 |
+ |
assert!(push_bookmark(&a, &ra, "main", None)[0].ok); |
|
1257 |
+ |
let old = ra.refs().unwrap().refs[0].state; |
|
1258 |
+ |
write(&root.join("a"), "f.txt", b"aa\n"); |
|
1259 |
+ |
let h2 = a.commit("second", &[]).unwrap(); |
|
1260 |
+ |
assert!(push_bookmark(&a, &ra, "main", Some(old))[0].ok); |
|
1261 |
+ |
// The second bundle (which needs the first) renamed to sort first. |
|
1262 |
+ |
let keys: Vec<String> = inner |
|
1263 |
+ |
.list("bundles/") |
|
1264 |
+ |
.unwrap() |
|
1265 |
+ |
.into_iter() |
|
1266 |
+ |
.map(|e| e.key) |
|
1267 |
+ |
.collect(); |
|
1268 |
+ |
assert_eq!(keys.len(), 2); |
|
1269 |
+ |
let bytes = inner.get(&keys[1]).unwrap(); |
|
1270 |
+ |
inner.delete(&keys[1]).unwrap(); |
|
1271 |
+ |
inner |
|
1272 |
+ |
.put_new("bundles/00000000-first.bundle", &bytes) |
|
1273 |
+ |
.unwrap(); |
|
1274 |
+ |
let shared = Shared::new(inner.clone()); |
|
1275 |
+ |
let c = Repo::init_with_id(&root.join("c"), a.id().unwrap()).unwrap(); |
|
1276 |
+ |
let rc = remote_for(&c, shared, root, "c"); |
|
1277 |
+ |
assert_eq!( |
|
1278 |
+ |
rc.refs().unwrap().refs[0].heads, |
|
1279 |
+ |
vec![h2], |
|
1280 |
+ |
"the dependent bundle waited for its dependency" |
|
1281 |
+ |
); |
|
1282 |
+ |
// A bundle gone between the listing and the read is skipped. |
|
1283 |
+ |
let shared = Shared::new(inner.clone()); |
|
1284 |
+ |
*shared.vanish.lock().unwrap() = Some(keys[0].clone()); |
|
1285 |
+ |
let d = Repo::init_with_id(&root.join("d"), a.id().unwrap()).unwrap(); |
|
1286 |
+ |
let rd = remote_for(&d, shared, root, "d"); |
|
1287 |
+ |
// Without the first bundle the second cannot be imported: the sync |
|
1288 |
+ |
// says so rather than failing silently. |
|
1289 |
+ |
let e = rd.refs().expect_err("missing changes").to_string(); |
|
1290 |
+ |
assert!(e.contains("cannot be imported"), "{e}"); |
|
1291 |
+ |
} |
|
1292 |
+ |
|
|
1293 |
+ |
#[test] |
|
1294 |
+ |
fn gc_merges_bundles_and_prunes_after_the_grace_period() { |
|
1295 |
+ |
hermetic(); |
|
1296 |
+ |
let dir = tempfile::tempdir().unwrap(); |
|
1297 |
+ |
let root = dir.path(); |
|
1298 |
+ |
let url = format!("dir://{}", root.join("bucket").display()); |
|
1299 |
+ |
let store = LocalStore::open(root.join("bucket")).unwrap(); |
|
1300 |
+ |
let mut a = Repo::init(&root.join("a")).unwrap(); |
|
1301 |
+ |
add_remote(&mut a, "bucket", &url); |
|
1302 |
+ |
for i in 1..=3 { |
|
1303 |
+ |
write(&root.join("a"), "f.txt", format!("{i}\n").as_bytes()); |
|
1304 |
+ |
a.commit(&format!("change {i}"), &[]).unwrap(); |
|
1305 |
+ |
assert!(a.push("bucket", false).unwrap()[0].ok); |
|
1306 |
+ |
} |
|
1307 |
+ |
a.tag_create("v3", &a.bookmark_heads().unwrap(), "three") |
|
1308 |
+ |
.unwrap(); |
|
1309 |
+ |
assert!(a.push("bucket", false).unwrap().iter().all(|r| r.ok)); |
|
1310 |
+ |
assert_eq!( |
|
1311 |
+ |
keys(&store, "bundles/").len(), |
|
1312 |
+ |
4, |
|
1313 |
+ |
"three pushes and the tag's" |
|
1314 |
+ |
); |
|
1315 |
+ |
// A gc folds the four into one; the old ones wait out the grace. |
|
1316 |
+ |
let r = a.gc_remote("bucket", false).unwrap(); |
|
1317 |
+ |
assert_eq!( |
|
1318 |
+ |
(r.merged, r.changes, r.deleted, r.deferred), |
|
1319 |
+ |
(4, 3, 0, 4), |
|
1320 |
+ |
"{r:?}" |
|
1321 |
+ |
); |
|
1322 |
+ |
assert!(r.bundle_bytes > 0); |
|
1323 |
+ |
assert_eq!(keys(&store, "bundles/").len(), 5); |
|
1324 |
+ |
let b = Repo::clone(&url, &root.join("b")).unwrap(); |
|
1325 |
+ |
assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"3\n"); |
|
1326 |
+ |
assert_eq!(b.tags().unwrap().len(), 1); |
|
1327 |
+ |
// Past the grace period they go, and a clone needs only the one. |
|
1328 |
+ |
let r = a.gc_remote("bucket", true).unwrap(); |
|
1329 |
+ |
assert_eq!((r.merged, r.deleted, r.deferred), (0, 4, 0), "{r:?}"); |
|
1330 |
+ |
assert_eq!(keys(&store, "bundles/").len(), 1); |
|
1331 |
+ |
let c = Repo::clone(&url, &root.join("c")).unwrap(); |
|
1332 |
+ |
assert_eq!(c.bookmark_heads().unwrap(), a.bookmark_heads().unwrap()); |
|
1333 |
+ |
// The earlier clone, which saw the old bundles, carries on. |
|
1334 |
+ |
write(&root.join("b"), "f.txt", b"4\n"); |
|
1335 |
+ |
b.commit("four", &[]).unwrap(); |
|
1336 |
+ |
assert!(b.push("origin", false).unwrap()[0].ok); |
|
1337 |
+ |
assert!(a.pull("bucket").unwrap().changed); |
|
1338 |
+ |
assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"4\n"); |
|
1339 |
+ |
// Nothing more to merge: a second gc is a no-op. |
|
1340 |
+ |
let r = a.gc_remote("bucket", true).unwrap(); |
|
1341 |
+ |
assert_eq!((r.merged, r.deleted), (2, 2)); |
|
1342 |
+ |
assert!(a.gc_remote("nope", true).is_err()); |
| 854 |
1343 |
|
} |
| 855 |
1344 |
|
|
| 856 |
1345 |
|
#[test] |
|
|
… |
13 unchanged lines |