Skip to content

Commit c9473f7

Browse files
mononoke: make change_target_config return result immediately if it was computed
Summary: # Goal of the stack There goal of this stack is to make megarepo api safer to use. In particular, we want to achieve: 1) If the same request is executed a few times, then it won't cause corrupt repo in any way (i.e. it won't create commits that client didn't intend to create and it won't move bookmarks to unpredictable places) 2) If request finished successfully, but we failed to send the success to the client, then repeating the same request would finish successfully. Achieving #1 is necessary because async_requests_worker might execute a few requests at the same time (though this should be rare). Achieveing #2 is necessary because if we fail to send successful response to the client (e.g. because of network issues), we want client to retry and get this successful response back, so that client can continue with their next request. In order to achieve #1 we make all bookmark move conditional i.e. we move a bookmark only if current location of the bookmark is at the place where client expects it. This should help achieve goal #1, because even if we have two requests executing at the same time, only one of them will successfully move a bookmark. However once we achieve #1 we have a problem with #2 - if a request was successful, but we failed to send a successful reply back to the client then client will retry the request, and it will fail, because a bookmark is already at the new location (because previous request was successful), but client expects it to be at the old location (because client doesn't know that the request was succesful). To fix this issue before executing the request we check if this request was already successful, and we do it heuristically by checking request parameters and verifying the commit remapping state. This doesn't protect against malicious clients, but it should protect from issue #2 described above. So the whole stack of diffs is the following: 1) take a method from megarepo api 2) implement a diff that makes bookmark moves conditional 3) Fix the problem #2 by checking if a previous request was successful or not # This diff Same as with D29848377 - if result was already computed and client retries the same request, then return it. Differential Revision: D29874802 fbshipit-source-id: ebc2f709bc8280305473d6333d0725530c131872
1 parent 47e9220 commit c9473f7

File tree

2 files changed

+214
-11
lines changed

2 files changed

+214
-11
lines changed

eden/mononoke/megarepo_api/src/change_target_config.rs

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,27 +176,34 @@ impl<'a> ChangeTargetConfig<'a> {
176176
// Find the target config version and remapping state that was used to
177177
// create the latest target commit. This config version will be used to
178178
// as a base for comparing with new config.
179-
let (target_bookmark, old_target_cs_id) =
179+
let (target_bookmark, actual_target_location) =
180180
find_target_bookmark_and_value(&ctx, &target_repo, &target).await?;
181-
if old_target_cs_id != target_location {
182-
return Err(MegarepoError::request(anyhow!(
183-
"Can't change target config because \
184-
target_location is set to {} which is different \
185-
from actual target location {}.",
186-
target_location,
187-
old_target_cs_id,
188-
)));
181+
182+
// target doesn't point to the commit we expect - check
183+
// if this method has already succeded and just immediately return the
184+
// result if so.
185+
if actual_target_location != target_location {
186+
return self
187+
.check_if_this_method_has_already_succeeded(
188+
ctx,
189+
&new_version,
190+
(target_location, actual_target_location),
191+
&changesets_to_merge,
192+
&target_repo,
193+
)
194+
.await;
189195
}
196+
190197
let old_target_cs = &target_repo
191-
.changeset(old_target_cs_id)
198+
.changeset(target_location)
192199
.await?
193200
.ok_or_else(|| {
194201
MegarepoError::internal(anyhow!("programming error - target changeset not found!"))
195202
})?;
196203
let (old_remapping_state, old_config) = find_target_sync_config(
197204
&ctx,
198205
target_repo.blob_repo(),
199-
old_target_cs_id,
206+
target_location,
200207
&target,
201208
&self.megarepo_configs,
202209
)
@@ -658,6 +665,47 @@ impl<'a> ChangeTargetConfig<'a> {
658665

659666
Ok(merge.get_changeset_id())
660667
}
668+
669+
// If that change_target_config() call was successful, but failed to send
670+
// successful result to the client (e.g. network issues) then
671+
// client will retry a request. We need to detect this situation and
672+
// send a successful response to the client.
673+
async fn check_if_this_method_has_already_succeeded(
674+
&self,
675+
ctx: &CoreContext,
676+
new_version: &SyncConfigVersion,
677+
(expected_target_location, actual_target_location): (ChangesetId, ChangesetId),
678+
changesets_to_merge: &BTreeMap<SourceName, ChangesetId>,
679+
repo: &RepoContext,
680+
) -> Result<ChangesetId, MegarepoError> {
681+
// Bookmark points a non-expected commit - let's see if changeset it points to was created
682+
// by a previous change_target_config call
683+
684+
// Check that first parent is a target location
685+
let parents = repo
686+
.blob_repo()
687+
.get_changeset_parents_by_bonsai(ctx.clone(), actual_target_location)
688+
.await?;
689+
if parents.get(0) != Some(&expected_target_location) {
690+
return Err(MegarepoError::request(anyhow!(
691+
"Neither {} nor its first parent {:?} point to a target location {}",
692+
actual_target_location,
693+
parents.get(0),
694+
expected_target_location,
695+
)));
696+
}
697+
698+
self.check_if_commit_has_expected_remapping_state(
699+
ctx,
700+
actual_target_location,
701+
new_version,
702+
changesets_to_merge,
703+
repo,
704+
)
705+
.await?;
706+
707+
Ok(actual_target_location)
708+
}
661709
}
662710

663711
#[cfg(test)]

eden/mononoke/megarepo_api/src/change_target_config_test.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,158 @@ async fn test_change_target_config_no_file_dir_conflict_2(fb: FacebookInit) -> R
468468

469469
Ok(())
470470
}
471+
472+
#[fbinit::test]
473+
async fn test_change_target_config_repeat_same_request(fb: FacebookInit) -> Result<(), Error> {
474+
let ctx = CoreContext::test_mock(fb);
475+
let mut test = MegarepoTest::new(&ctx).await?;
476+
let first_source_name = SourceName::new("source_1");
477+
let target: Target = test.target("target".to_string());
478+
479+
init_megarepo(&ctx, &mut test).await?;
480+
481+
let first_source_cs_id =
482+
resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
483+
let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
484+
485+
let version_2 = "version_2".to_string();
486+
let third_source_name = SourceName::new("source_3");
487+
let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
488+
.add_file("third", "third")
489+
.commit()
490+
.await?;
491+
492+
bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
493+
.set_to(third_source_cs_id)
494+
.await?;
495+
SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
496+
.source_builder(first_source_name.clone())
497+
.set_prefix_bookmark_to_source_name()
498+
.linkfile("first", "linkfiles/first_in_other_location")
499+
.build_source()?
500+
.source_builder(third_source_name.clone())
501+
.set_prefix_bookmark_to_source_name()
502+
.build_source()?
503+
.build(&mut test.configs_storage);
504+
505+
let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
506+
let change_target_config = ChangeTargetConfig::new(&configs_storage, &test.mononoke);
507+
change_target_config
508+
.run(
509+
&ctx,
510+
&target,
511+
version_2.clone(),
512+
target_cs_id,
513+
btreemap! {
514+
first_source_name.clone() => first_source_cs_id,
515+
third_source_name.clone() => third_source_cs_id,
516+
},
517+
None,
518+
)
519+
.await?;
520+
521+
// Now repeat the same request - it should succeed
522+
let change_target_config = ChangeTargetConfig::new(&configs_storage, &test.mononoke);
523+
change_target_config
524+
.run(
525+
&ctx,
526+
&target,
527+
version_2.clone(),
528+
target_cs_id,
529+
btreemap! {
530+
first_source_name.clone() => first_source_cs_id,
531+
third_source_name.clone() => third_source_cs_id,
532+
},
533+
None,
534+
)
535+
.await?;
536+
537+
// Now send slightly different request - it should fail
538+
let change_target_config = ChangeTargetConfig::new(&configs_storage, &test.mononoke);
539+
assert!(
540+
change_target_config
541+
.run(
542+
&ctx,
543+
&target,
544+
version_2,
545+
target_cs_id,
546+
btreemap! {
547+
first_source_name.clone() => first_source_cs_id,
548+
},
549+
None,
550+
)
551+
.await
552+
.is_err()
553+
);
554+
Ok(())
555+
}
556+
557+
#[fbinit::test]
558+
async fn test_change_target_config_noop_change(fb: FacebookInit) -> Result<(), Error> {
559+
let ctx = CoreContext::test_mock(fb);
560+
let mut test = MegarepoTest::new(&ctx).await?;
561+
let first_source_name = SourceName::new("source_1");
562+
let target: Target = test.target("target".to_string());
563+
564+
init_megarepo(&ctx, &mut test).await?;
565+
566+
let first_source_cs_id =
567+
resolve_cs_id(&ctx, &test.blobrepo, first_source_name.0.clone()).await?;
568+
let target_cs_id = resolve_cs_id(&ctx, &test.blobrepo, "target").await?;
569+
570+
let version_2 = "version_2".to_string();
571+
let third_source_name = SourceName::new("source_3");
572+
let third_source_cs_id = CreateCommitContext::new_root(&ctx, &test.blobrepo)
573+
.add_file("third", "third")
574+
.commit()
575+
.await?;
576+
577+
bookmark(&ctx, &test.blobrepo, third_source_name.to_string())
578+
.set_to(third_source_cs_id)
579+
.await?;
580+
SyncTargetConfigBuilder::new(test.repo_id(), target.clone(), version_2.clone())
581+
.source_builder(first_source_name.clone())
582+
.set_prefix_bookmark_to_source_name()
583+
.linkfile("first", "linkfiles/first_in_other_location")
584+
.build_source()?
585+
.source_builder(third_source_name.clone())
586+
.set_prefix_bookmark_to_source_name()
587+
.build_source()?
588+
.build(&mut test.configs_storage);
589+
590+
let configs_storage: Arc<dyn MononokeMegarepoConfigs> = Arc::new(test.configs_storage.clone());
591+
let change_target_config = ChangeTargetConfig::new(&configs_storage, &test.mononoke);
592+
let new_target_cs_id = change_target_config
593+
.run(
594+
&ctx,
595+
&target,
596+
version_2.clone(),
597+
target_cs_id,
598+
btreemap! {
599+
first_source_name.clone() => first_source_cs_id,
600+
third_source_name.clone() => third_source_cs_id,
601+
},
602+
None,
603+
)
604+
.await?;
605+
606+
// Now do a noop change on existing commit
607+
let change_target_config = ChangeTargetConfig::new(&configs_storage, &test.mononoke);
608+
let noop_change = change_target_config
609+
.run(
610+
&ctx,
611+
&target,
612+
version_2.clone(),
613+
new_target_cs_id,
614+
btreemap! {
615+
first_source_name.clone() => first_source_cs_id,
616+
third_source_name.clone() => third_source_cs_id,
617+
},
618+
None,
619+
)
620+
.await?;
621+
622+
assert_ne!(noop_change, new_target_cs_id);
623+
624+
Ok(())
625+
}

0 commit comments

Comments
 (0)