-
Notifications
You must be signed in to change notification settings - Fork 239
Let fast-slow has() trust the fast store when configured #2728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,8 @@ pub struct FastSlowStore { | |
| slow_direction: StoreDirection, | ||
| /// See [`FastSlowSpec::bypass_dedup_threshold_bytes`]. | ||
| bypass_dedup_threshold_bytes: u64, | ||
| /// See [`FastSlowSpec::trust_fast_store_for_has`]. | ||
| trust_fast_store_for_has: bool, | ||
| weak_self: Weak<Self>, | ||
| #[metric] | ||
| metrics: FastSlowStoreMetrics, | ||
|
|
@@ -166,6 +168,7 @@ impl FastSlowStore { | |
| slow_direction: spec.slow_direction, | ||
| // 0 (default) disables the bypass entirely (always dedup). | ||
| bypass_dedup_threshold_bytes: spec.bypass_dedup_threshold_bytes, | ||
| trust_fast_store_for_has: spec.trust_fast_store_for_has, | ||
| weak_self: weak_self.clone(), | ||
| metrics: FastSlowStoreMetrics::default(), | ||
| populating_digests: Mutex::new(HashMap::new()), | ||
|
|
@@ -206,6 +209,34 @@ impl FastSlowStore { | |
| .is_some_and(|size| size >= self.bypass_dedup_threshold_bytes) | ||
| } | ||
|
|
||
| /// Asks the slow store only about the keys still unresolved in | ||
| /// `results` and writes its answers back into the matching slots. Used | ||
| /// by `has_with_results` when a fast-store hit is trusted as existence. | ||
| async fn has_in_slow_store_for_misses( | ||
| &self, | ||
| keys: &[StoreKey<'_>], | ||
| results: &mut [Option<u64>], | ||
| ) -> Result<(), Error> { | ||
| let (miss_indexes, miss_keys): (Vec<usize>, Vec<StoreKey<'_>>) = keys | ||
| .iter() | ||
| .zip(results.iter()) | ||
| .enumerate() | ||
| .filter(|(_, (_, result))| result.is_none()) | ||
| .map(|(i, (key, _))| (i, key.borrow())) | ||
| .unzip(); | ||
| if miss_keys.is_empty() { | ||
| return Ok(()); | ||
| } | ||
| let mut slow_results = vec![None; miss_keys.len()]; | ||
| self.slow_store | ||
| .has_with_results(&miss_keys, &mut slow_results) | ||
| .await?; | ||
| for (i, size) in miss_indexes.into_iter().zip(slow_results) { | ||
| results[i] = size; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub const fn fast_store(&self) -> &Store { | ||
| &self.fast_store | ||
| } | ||
|
|
@@ -479,8 +510,18 @@ impl StoreDriver for FastSlowStore { | |
| return self.fast_store.has_with_results(key, results).await; | ||
| } | ||
|
|
||
| // Check with the slow store first. | ||
| self.slow_store.has_with_results(key, results).await?; | ||
| if self.trust_fast_store_for_has { | ||
| self.fast_store.has_with_results(key, results).await?; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add failure/concurrency tests for the policy introduced here:
The new in-flight-write test uses a Noop fast tier, so it verifies only the fast-miss path. The fast-hit case deliberately skips the in-flight wait because its result slot is already Some; explicitly testing that distinction would prevent the claim that the merge is “unchanged” from being read as a persistence guarantee for all hits. If that early visibility is not intended, the in-flight merge needs to take precedence over trusted fast hits. |
||
| self.has_in_slow_store_for_misses(key, results).await?; | ||
| } else { | ||
| // NOTE: By default we intentionally *NEVER* check the fast store, | ||
| // this is to ensure that we re-upload data to the slow store if | ||
| // it only exists in the fast store. This does not affect workers | ||
| // as they do not check existence through `has` and instead go | ||
| // direct to loading the data which bypasses the check and will | ||
| // load from the fast store if it does not exist in the slow store. | ||
| self.slow_store.has_with_results(key, results).await?; | ||
| } | ||
|
|
||
| // Check for any in-flight requests to the slow store next. | ||
| let mut in_flight_futs = FuturesUnordered::new(); | ||
|
|
@@ -502,13 +543,6 @@ impl StoreDriver for FastSlowStore { | |
| results[i] = size; | ||
| } | ||
|
|
||
| // NOTE: We intentionally *NEVER* check the fast store, this is to | ||
| // ensure that we re-upload data to the slow store if it only exists | ||
| // in the fast store. This does not affect workers as they do not | ||
| // check existence through `has` and instead go direct to loading the | ||
| // data which bypasses the check and will load from the fast store if | ||
| // it does not exist in the slow store. | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“The fast tier does not evict” is not sufficient safety guidance for this flag. A non-evicting memory cache can disappear on restart, and a persistent node-local cache can answer FindMissingBlobs on replica A while the subsequent read goes to replica B, whose fast tier and shared slow tier both lack the blob. No eviction is needed for that failure. “Another path makes the slow tier authoritative” also needs to specify what actually guarantees persistence and visibility; this flag itself schedules no replication.
Suggested replacement:
Please also regenerate the configuration reference using
gen:config-reference, and updateweb/apps/docs/content/docs/how-to/stores/compose-stores.mdxplus the narrativereference/nativelink-config/store-overview.mdx. The latter's existing fast/slow durability warning should distinguish default slow-tier existence checks from this opt-in policy. Add an explicit warning next to an opt-in example, covering shared versus node-local fast tiers and that “no eviction” does not mean persistent storage. Keep exhaustive field details in the generated reference, per AGENTS.md.