Skip to content

Commit 0abea02

Browse files
Pierre Chevaliermeta-codesync[bot]
authored andcommitted
Implement opportunistic boundary completion
Summary: ## Context Currently, we have a problem with backfills for repos with many commits, and with large working copies near the top of the repo: Our strategy consists of splitting the repo into "slices" and deriving "boundaries" (the parents of the base commits for a set of slices) from a "predecessor" derived data types. Deriving a boundary this way does work that's `O(working copy size)`, but it only needs the predecessor type to exist for the commit, instead of the target type to exist for its parent. This enables us to take advantage of the number of workers we have available as we can parallelize the work, which is necessary for a repo like `configerator` (130M public commits) or `fbsource` (70M public commits). There is one hiccup with this strategy, though: near the top of a repo like fbsource, the working copy is so large that a single "derive boundaries request" can take weeks to compute. By that time, it's quite likely that some previous slices caught up, so we're waiting for boundary derivation for nothing and just wasting time. In practice, it has meant that we have had to fiddle with the backfilling parameters: do a first pass with "slice size" 100k or so, which parallelizes the work optimally, then do a second pass with "slice size" 5M, which allows to avoid having boundary commits too near the top of the repo where computation is unacceptably expensive. ## Solution Here, we introduce the concept of "opportunistic boundary completion": instead of always waiting for the "derive from predecessors" calls to finish, we will move on if the commits we are trying to derive get derived by some other job before we have time to complete them. This should be strictly more performant, and hopefully will remove a lot of the fiddling work in backfilling large repos. The entire feature is gated by a JK, because I only trust the implementation so much, and safe_jk2 `scm/mononoke:derived_data_backfill_opportunistic_boundary_completion`: the jk was created [here](https://www.internalfb.com/intern/justknobs/?name=scm%2Fmononoke#derived_data_backfill_opportunistic_boundary_completion) and set to false for now Reviewed By: lmvasquezg Differential Revision: D109853257 fbshipit-source-id: d9008c5889e4b0958a86aff0b31d2ff857be7202
1 parent 2cb8f7e commit 0abea02

4 files changed

Lines changed: 146 additions & 3 deletions

File tree

eden/mononoke/common/mononoke_macros/test_just_knobs/just_knobs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"scm/mononoke:cross_repo_skip_backsyncing_ordinary_empty_commits": false,
44
"scs/mononoke:defer_to_backsyncer_for_backsync": true,
55
"scm/mononoke:dbcm_read_from_manifest": true,
6+
"scm/mononoke:derived_data_backfill_opportunistic_boundary_completion": true,
67
"scm/mononoke:derived_data_disable_derivation_workers": false,
78
"scm/mononoke:derived_data_disable_remote_derivation": false,
89
"scm/mononoke:derived_data_use_content_manifests": true,

eden/mononoke/features/async_requests/worker_lib/BUCK

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,20 @@ rust_library(
77
name = "worker_lib",
88
srcs = glob(["src/**/*.rs"]),
99
test_deps = [
10+
"//common/rust/shed/facet:facet",
1011
"//common/rust/shed/fbinit:fbinit",
1112
"//common/rust/shed/fbinit:fbinit-tokio",
1213
"//eden/mononoke/common/mononoke_macros:mononoke_macros",
14+
"//eden/mononoke/derived_data:fsnodes",
1315
"//eden/mononoke/features/async_requests:requests_table",
16+
"//eden/mononoke/repo_attributes/bonsai_hg_mapping:bonsai_hg_mapping",
17+
"//eden/mononoke/repo_attributes/bookmarks:bookmarks",
18+
"//eden/mononoke/repo_attributes/filestore:filestore",
19+
"//eden/mononoke/repo_attributes/repo_derivation_queues/if:derivation_queue_thrift-rust",
20+
"//eden/mononoke/repo_attributes/repo_identity:repo_identity",
21+
"//eden/mononoke/repo_factory:test_repo_factory",
1422
"//eden/mononoke/scs/if:source_control-rust",
23+
"//eden/mononoke/tests/utils:tests_utils",
1524
],
1625
deps = [
1726
"fbsource//third-party/rust:anyhow",

eden/mononoke/features/async_requests/worker_lib/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,14 @@ tokio = { version = "1.52.3", features = ["full", "test-util", "tracing"] }
4646
tracing = { version = "0.1.41", features = ["attributes", "valuable"] }
4747

4848
[dev-dependencies]
49+
bonsai_hg_mapping = { version = "0.1.0", path = "../../../repo_attributes/bonsai_hg_mapping" }
50+
bookmarks = { version = "0.1.0", path = "../../../repo_attributes/bookmarks" }
51+
derivation_queue_thrift = { version = "0.1.0", path = "../../../repo_attributes/repo_derivation_queues/if" }
52+
facet = { version = "0.1.0", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main", features = ["impl_never_type"] }
4953
fbinit = { version = "0.2.0", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" }
5054
fbinit-tokio = { version = "0.1.2", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" }
55+
filestore = { version = "0.1.0", path = "../../../repo_attributes/filestore" }
56+
fsnodes = { version = "0.1.0", path = "../../../derived_data/fsnodes" }
57+
repo_identity = { version = "0.1.0", path = "../../../repo_attributes/repo_identity" }
58+
test_repo_factory = { version = "0.1.0", path = "../../../repo_factory/test_repo_factory" }
59+
tests_utils = { version = "0.1.0", path = "../../../tests/utils" }

eden/mononoke/features/async_requests/worker_lib/src/backfill.rs

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ const JK_BACKFILL_CHUNK_RETRY_LIMIT: &str = "scm/mononoke:derived_data_backfill_
6464
const CHUNK_RETRY_BASE_BACKOFF: Duration = Duration::from_secs(1);
6565
const CHUNK_RETRY_MAX_BACKOFF: Duration = Duration::from_secs(30);
6666

67+
/// JustKnob gating opportunistic boundary completion: when enabled, each
68+
/// boundary's predecessor derivation races against the boundary being derived
69+
/// by another task (e.g. a concurrent slice), so a slow predecessor derivation
70+
/// is abandoned as soon as it becomes redundant.
71+
const JK_BACKFILL_OPPORTUNISTIC_BOUNDARIES: &str =
72+
"scm/mononoke:derived_data_backfill_opportunistic_boundary_completion";
73+
74+
/// Poll interval for noticing that a boundary changeset has been derived by
75+
/// another task (e.g. a concurrently-running slice), so we can abandon its
76+
/// now-redundant predecessor derivation.
77+
const BOUNDARY_DERIVED_POLL_INTERVAL: Duration = Duration::from_secs(1);
78+
6779
/// Get a DerivedDataManager with optional read/write throttling applied.
6880
/// If JustKnobs are not set or are zero, returns the manager unchanged.
6981
fn get_throttled_manager(manager: &DerivedDataManager) -> Result<DerivedDataManager> {
@@ -137,6 +149,27 @@ fn requires_serial_slice_processing(derived_data_type: DerivableType) -> bool {
137149
.is_err()
138150
}
139151

152+
/// Resolves once `csid` has `derived_data_type` derived by any means, polling
153+
/// periodically. Used to race against predecessor derivation: if a concurrent
154+
/// slice derives the boundary first, we can abandon the slower
155+
/// `derive_from_predecessor` call. Transient lookup errors are ignored and
156+
/// retried, since predecessor derivation remains the guaranteed backstop.
157+
async fn wait_until_derived(
158+
manager: &DerivedDataManager,
159+
ctx: &CoreContext,
160+
csid: ChangesetId,
161+
derived_data_type: DerivableType,
162+
) {
163+
loop {
164+
tokio::time::sleep(BOUNDARY_DERIVED_POLL_INTERVAL).await;
165+
if let Ok(true) =
166+
BulkDerivation::is_derived(manager, ctx, csid, None, derived_data_type).await
167+
{
168+
return;
169+
}
170+
}
171+
}
172+
140173
/// Compute derive_boundaries request - derives boundary changesets using predecessor derivation
141174
pub(crate) async fn compute_derive_boundaries(
142175
ctx: &CoreContext,
@@ -189,6 +222,7 @@ pub(crate) async fn compute_derive_boundaries(
189222
let manager = with_type_enabled(&manager, derived_data_type);
190223
let concurrency = params.concurrency.max(1) as usize;
191224
let use_predecessor = params.use_predecessor_derivation;
225+
let opportunistic = justknobs::eval(JK_BACKFILL_OPPORTUNISTIC_BOUNDARIES, None, None);
192226

193227
stream::iter(boundary_cs_ids)
194228
.map(Ok::<_, Error>)
@@ -198,14 +232,29 @@ pub(crate) async fn compute_derive_boundaries(
198232
let derived_count = derived_count.clone();
199233
async move {
200234
if use_predecessor {
201-
BulkDerivation::unsafe_derive_untopologically(
235+
let derive = BulkDerivation::unsafe_derive_untopologically(
202236
&manager,
203237
&ctx,
204238
csid,
205239
None, // rederivation
206240
derived_data_type,
207-
)
208-
.await?;
241+
);
242+
if opportunistic {
243+
// Race predecessor derivation against the boundary becoming
244+
// derived by another task (e.g. a concurrently-running slice
245+
// whose head is this boundary). Whichever happens first
246+
// satisfies the boundary; if it's derived elsewhere we drop
247+
// the now-redundant (and possibly slow) predecessor future.
248+
tokio::select! {
249+
biased;
250+
res = derive => {
251+
res?;
252+
}
253+
_ = wait_until_derived(&manager, &ctx, csid, derived_data_type) => {}
254+
}
255+
} else {
256+
derive.await?;
257+
}
209258
} else {
210259
manager
211260
.derive_bulk_locally(
@@ -808,3 +857,78 @@ async fn process_repo_backfill(
808857
)
809858
.await
810859
}
860+
861+
#[cfg(test)]
862+
mod tests {
863+
use bonsai_hg_mapping::BonsaiHgMapping;
864+
use bookmarks::Bookmarks;
865+
use commit_graph::CommitGraph;
866+
use commit_graph::CommitGraphWriter;
867+
use context::CoreContext;
868+
use derivation_queue_thrift::DerivationPriority;
869+
use fbinit::FacebookInit;
870+
use filestore::FilestoreConfig;
871+
use fsnodes::RootFsnodeId;
872+
use mononoke_macros::mononoke;
873+
use repo_blobstore::RepoBlobstore;
874+
use repo_derived_data::RepoDerivedData;
875+
use repo_derived_data::RepoDerivedDataRef;
876+
use repo_identity::RepoIdentity;
877+
use tests_utils::CreateCommitContext;
878+
879+
use super::*;
880+
881+
#[facet::container]
882+
struct TestRepo(
883+
dyn BonsaiHgMapping,
884+
dyn Bookmarks,
885+
CommitGraph,
886+
dyn CommitGraphWriter,
887+
RepoDerivedData,
888+
RepoBlobstore,
889+
FilestoreConfig,
890+
RepoIdentity,
891+
);
892+
893+
/// `wait_until_derived` must observe a changeset transitioning to derived
894+
/// (by any path) and resolve. This is the only non-trivial bit of the
895+
/// opportunistic-boundary race: the `select!` itself is plumbing, but this
896+
/// poll has to actually detect real derivation against a real manager.
897+
#[mononoke::fbinit_test]
898+
async fn wait_until_derived_resolves_after_derivation(fb: FacebookInit) -> Result<()> {
899+
let ctx = CoreContext::test_mock(fb);
900+
let repo: TestRepo = test_repo_factory::build_empty(fb)
901+
.await
902+
.map_err(|e| anyhow::anyhow!("{e}"))?;
903+
let cs_id = CreateCommitContext::new_root(&ctx, &repo)
904+
.add_file("a", "a")
905+
.commit()
906+
.await?;
907+
let manager = repo.repo_derived_data().manager();
908+
909+
// Underived to begin with.
910+
assert!(
911+
!BulkDerivation::is_derived(manager, &ctx, cs_id, None, DerivableType::Fsnodes).await?,
912+
"changeset should start underived",
913+
);
914+
915+
// Derive concurrently with the waiter. The waiter must observe the
916+
// transition and resolve; if it never did, this join would hang and the
917+
// test would time out.
918+
tokio::join!(
919+
async {
920+
repo.repo_derived_data()
921+
.derive::<RootFsnodeId>(&ctx, cs_id, DerivationPriority::LOW)
922+
.await
923+
.expect("derivation should succeed");
924+
},
925+
wait_until_derived(manager, &ctx, cs_id, DerivableType::Fsnodes),
926+
);
927+
928+
assert!(
929+
BulkDerivation::is_derived(manager, &ctx, cs_id, None, DerivableType::Fsnodes).await?,
930+
"changeset should be derived after the waiter resolved",
931+
);
932+
Ok(())
933+
}
934+
}

0 commit comments

Comments
 (0)