Repository navigation
Existence cache remove callbacks #1947
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
Changes from all commits
99477fc
da947b1
9288648
1394805
62afbb3
3f80417
4493a22
c088db5
c2046d6
8145773
ffe5cf2
ca23f8e
e0e1dcb
5e2d752
76d5db6
5217897
93d4bc7
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 |
|---|---|---|
|
|
@@ -104,12 +104,14 @@ build --@rules_rust//:clippy_flag=-Dclippy::alloc_instead_of_core | |
| build --@rules_rust//:clippy_flag=-Dclippy::as_underscore | ||
| build --@rules_rust//:clippy_flag=-Wclippy::dbg_macro | ||
| build --@rules_rust//:clippy_flag=-Wclippy::decimal_literal_representation | ||
| build --@rules_rust//:clippy_flag=-Dclippy::elidable_lifetime_names | ||
| build --@rules_rust//:clippy_flag=-Aclippy::get_unwrap | ||
| build --@rules_rust//:clippy_flag=-Dclippy::missing_const_for_fn | ||
| build --@rules_rust//:clippy_flag=-Aclippy::missing_docs_in_private_items | ||
| build --@rules_rust//:clippy_flag=-Wclippy::print_stdout | ||
| build --@rules_rust//:clippy_flag=-Dclippy::semicolon_if_nothing_returned | ||
| build --@rules_rust//:clippy_flag=-Dclippy::std_instead_of_core | ||
| build --@rules_rust//:clippy_flag=-Wclippy::todo | ||
| build --@rules_rust//:clippy_flag=-Dclippy::todo | ||
| build --@rules_rust//:clippy_flag=-Aclippy::too_long_first_doc_paragraph | ||
| build --@rules_rust//:clippy_flag=-Wclippy::unimplemented | ||
| build --@rules_rust//:clippy_flag=-Aclippy::unwrap_in_result | ||
|
|
@@ -154,10 +156,13 @@ build:nightly --@rules_rust//rust/toolchain/channel=nightly | |
| # AddressSanitizer to detect memory leaks. | ||
| build:asan --config=nightly | ||
| build:asan --@rules_rust//:extra_rustc_flags=-Zsanitizer=address | ||
| # asan explicitly needs to _not_ use the Nix toolchains and so this | ||
| # is needed to load the rust toolchains in Bazel | ||
| build:asan --extra_toolchains=@rust_toolchains//:all | ||
|
|
||
| # ThreadSanitizer to detect data races. Tests under tsan shouldn't be cached | ||
| # since they tend to produce irreproducible false negatives. | ||
| build:tsan --config=nightly -c dbg | ||
| build:tsan --config=nightly --config=debug | ||
|
Member
Author
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. Same effect, but clearer |
||
| build:tsan --@rules_rust//:extra_rustc_flags=-Zsanitizer=thread | ||
| build:tsan --cache_test_results=no | ||
| build:tsan --run_under=//tools:tsan.sh | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright 2025 The NativeLink Authors. All rights reserved. | ||
| // | ||
| // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // See LICENSE file for details | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| use std::sync::Arc; | ||
|
|
||
| use nativelink_util::evicting_map::RemoveStateCallback; | ||
| use nativelink_util::store_trait::{RemoveItemCallback, StoreKey}; | ||
| use tonic::async_trait; | ||
|
|
||
| // Generic struct to hold a RemoveItemCallback ref for the purposes | ||
| // of a RemoveStateCallback call | ||
| #[derive(Debug)] | ||
| pub struct RemoveItemCallbackHolder { | ||
| callback_fn: Arc<Box<dyn RemoveItemCallback>>, | ||
| } | ||
|
|
||
| impl RemoveItemCallbackHolder { | ||
| pub fn new(callback: &Arc<Box<dyn RemoveItemCallback>>) -> Self { | ||
| Self { | ||
| callback_fn: callback.clone(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl RemoveStateCallback<StoreKey<'static>> for RemoveItemCallbackHolder { | ||
| async fn callback(&self, key: &StoreKey<'static>) { | ||
| self.callback_fn.callback(key).await; | ||
|
Comment on lines
+37
to
+39
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. The It's a pretty big piece of functionality.
Member
Author
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. Yup, I'll add a few doc comments to the various new traits tomorrow. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |
|
|
||
| use core::pin::Pin; | ||
| use std::borrow::Cow; | ||
| use std::sync::Arc; | ||
| use std::sync::{Arc, Weak}; | ||
| use std::time::SystemTime; | ||
|
|
||
| use async_trait::async_trait; | ||
|
|
@@ -26,7 +26,11 @@ use nativelink_util::common::DigestInfo; | |
| use nativelink_util::evicting_map::{EvictingMap, LenEntry}; | ||
| use nativelink_util::health_utils::{HealthStatus, HealthStatusIndicator}; | ||
| use nativelink_util::instant_wrapper::InstantWrapper; | ||
| use nativelink_util::store_trait::{Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo}; | ||
| use nativelink_util::store_trait::{ | ||
| RemoveItemCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo, | ||
| }; | ||
| use parking_lot::Mutex; | ||
| use tracing::{debug, info, trace}; | ||
|
|
||
| #[derive(Clone, Debug)] | ||
| struct ExistenceItem(u64); | ||
|
|
@@ -47,7 +51,13 @@ impl LenEntry for ExistenceItem { | |
| pub struct ExistenceCacheStore<I: InstantWrapper> { | ||
| #[metric(group = "inner_store")] | ||
| inner_store: Store, | ||
| existence_cache: EvictingMap<DigestInfo, ExistenceItem, I>, | ||
| existence_cache: EvictingMap<DigestInfo, DigestInfo, ExistenceItem, I>, | ||
|
|
||
| // We need to pause them temporarily when inserting into the inner store | ||
| // as if it immediately expires them, we should only apply the remove callbacks | ||
| // afterwards. If this is None, we're not pausing; if it's Some it's the location to | ||
| // store them in temporarily | ||
| pause_remove_callbacks: Arc<Mutex<Option<Vec<StoreKey<'static>>>>>, | ||
|
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. Are paused callbacks still flushed if an update fails mid way?
Member
Author
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. Yes. The update is in two phases - inner store and then the existence cache - and both don't do any returns of errors until after the |
||
| } | ||
|
|
||
| impl ExistenceCacheStore<SystemTime> { | ||
|
|
@@ -56,6 +66,39 @@ impl ExistenceCacheStore<SystemTime> { | |
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl<I: InstantWrapper> RemoveItemCallback for ExistenceCacheStore<I> { | ||
| async fn callback(&self, store_key: &StoreKey<'_>) { | ||
| debug!(?store_key, "Removing item from cache due to callback"); | ||
| let new_key = store_key.borrow(); | ||
| let deleted_key = self.existence_cache.remove(&new_key.into_digest()).await; | ||
| if !deleted_key { | ||
| info!(?store_key, "Failed to delete key from cache on callback"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| struct ExistenceCacheCallback<I: InstantWrapper> { | ||
| cache: Weak<ExistenceCacheStore<I>>, | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl<I: InstantWrapper> RemoveItemCallback for ExistenceCacheCallback<I> { | ||
| async fn callback(&self, store_key: &StoreKey<'_>) { | ||
| let cache = self.cache.upgrade(); | ||
| if let Some(local_cache) = cache { | ||
| if let Some(callbacks) = &mut *local_cache.pause_remove_callbacks.lock_arc() { | ||
| callbacks.push(store_key.borrow().into_owned()); | ||
| } else { | ||
| local_cache.callback(store_key).await; | ||
| } | ||
| } else { | ||
| debug!("Cache dropped, so not doing callback"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<I: InstantWrapper> ExistenceCacheStore<I> { | ||
| pub fn new_with_time( | ||
| spec: &ExistenceCacheSpec, | ||
|
|
@@ -64,10 +107,19 @@ impl<I: InstantWrapper> ExistenceCacheStore<I> { | |
| ) -> Arc<Self> { | ||
| let empty_policy = EvictionPolicy::default(); | ||
| let eviction_policy = spec.eviction_policy.as_ref().unwrap_or(&empty_policy); | ||
| Arc::new(Self { | ||
| let existence_cache_store = Arc::new(Self { | ||
| inner_store, | ||
| existence_cache: EvictingMap::new(eviction_policy, anchor_time), | ||
| }) | ||
| pause_remove_callbacks: Arc::new(Mutex::new(None)), | ||
| }); | ||
| let other_ref = Arc::downgrade(&existence_cache_store); | ||
| existence_cache_store | ||
| .inner_store | ||
| .register_remove_callback(&Arc::new(Box::new(ExistenceCacheCallback { | ||
| cache: other_ref, | ||
| }))) | ||
| .expect("Register remove callback should work"); | ||
| existence_cache_store | ||
| } | ||
|
|
||
| pub async fn exists_in_cache(&self, digest: &DigestInfo) -> bool { | ||
|
|
@@ -184,15 +236,31 @@ impl<I: InstantWrapper> StoreDriver for ExistenceCacheStore<I> { | |
| .err_tip(|| "In ExistenceCacheStore::update")?; | ||
| return Ok(()); | ||
| } | ||
| { | ||
| let mut locked_callbacks = self.pause_remove_callbacks.lock_arc(); | ||
| if locked_callbacks.is_none() { | ||
| locked_callbacks.replace(vec![]); | ||
| } | ||
| } | ||
| trace!(?digest, "Inserting into inner cache"); | ||
| let result = self.inner_store.update(digest, reader, size_info).await; | ||
| if result.is_ok() { | ||
| trace!(?digest, "Inserting into existence cache"); | ||
| if let UploadSizeInfo::ExactSize(size) = size_info { | ||
| let _ = self | ||
| .existence_cache | ||
| .insert(digest, ExistenceItem(size)) | ||
| .await; | ||
| } | ||
| } | ||
| { | ||
| let mut locked_callbacks = self.pause_remove_callbacks.lock_arc(); | ||
| if let Some(callbacks) = locked_callbacks.take() { | ||
| for store_key in callbacks { | ||
| self.callback(&store_key).await; | ||
| } | ||
| } | ||
| } | ||
| result | ||
| } | ||
|
|
||
|
|
@@ -228,6 +296,13 @@ impl<I: InstantWrapper> StoreDriver for ExistenceCacheStore<I> { | |
| fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { | ||
| self | ||
| } | ||
|
|
||
| fn register_remove_callback( | ||
| self: Arc<Self>, | ||
| callback: &Arc<Box<dyn RemoveItemCallback>>, | ||
| ) -> Result<(), Error> { | ||
| self.inner_store.register_remove_callback(callback) | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
|
|
||
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.
Needed this to make it run locally