mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-03 11:57:43 +00:00
15f4e75870
* fix(cache): enforce projected entry capacity Refs: rustfs/backlog#1335 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): fence identity budget eviction by generation Refs rustfs/backlog#1334. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): fence clear against concurrent fills Refs rustfs/backlog#1333 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): linearize memory reservation claims Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): retain allocation memory claims Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): publish memory snapshots by epoch Co-Authored-By: heihutu <heihutu@gmail.com> * fix(cache): coordinate cold object fills Co-Authored-By: heihutu <heihutu@gmail.com> * fix(ecstore): fence metadata cache transition races Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
142 lines
5.5 KiB
Rust
142 lines
5.5 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// 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.
|
|
|
|
//! Write-side counterpart to [`super::body_cache_hook`].
|
|
//!
|
|
//! ecstore terminates object bodies from paths that never pass through the
|
|
//! app-layer usecases: lifecycle/scanner expiry, non-current version cleanup,
|
|
//! and restored-copy expiry all delete objects directly on the store. The GET
|
|
//! body cache is keyed on `(bucket, object, versionId, etag, size, variant)`
|
|
//! and every lookup follows a fresh metadata quorum, so a stale entry can never
|
|
//! be *served* after its object is gone (the GET fails at metadata resolution
|
|
//! first). What lingers is dead body bytes resident until TTL, which evict live
|
|
//! hot entries — a hygiene and capacity problem, not a correctness one.
|
|
//!
|
|
//! This hook lets the app-layer cache adapter drop those bodies from its index
|
|
//! the moment ecstore removes the object (ODC-26, backlog#1131).
|
|
|
|
use crate::object_api::hook_slot::HookSlot;
|
|
use std::sync::Arc;
|
|
|
|
/// Invalidates cached object bodies after an ecstore-internal mutation removed
|
|
/// them. Keyed on `(bucket, object)`; the implementation invalidates every
|
|
/// cached version/etag under that identity.
|
|
#[async_trait::async_trait]
|
|
pub trait ObjectMutationHook: Send + Sync + 'static {
|
|
async fn after_object_mutation(&self, bucket: &str, object: &str);
|
|
}
|
|
|
|
// A `HookSlot`, for the same reason as the GET hook slot: arc-swap cannot hold
|
|
// an unsized `Arc<dyn ObjectMutationHook>`. Registration is a startup event and
|
|
// each delete-path invocation only clones an `Arc` under the read guard,
|
|
// negligible next to the delete it accompanies.
|
|
static OBJECT_MUTATION_HOOK: HookSlot<dyn ObjectMutationHook> = HookSlot::new();
|
|
|
|
/// Register (or re-register) the process-wide object mutation hook.
|
|
///
|
|
/// Re-registration atomically swaps to `hook`, mirroring the GET body hook so a
|
|
/// rebuilt `AppContext` leaves ecstore's delete paths pointed at the newest
|
|
/// adapter.
|
|
pub fn register_object_mutation_hook(hook: Arc<dyn ObjectMutationHook>) {
|
|
OBJECT_MUTATION_HOOK.register(
|
|
hook,
|
|
"object mutation cache hook re-registered with a different instance; \
|
|
the previous adapter's cache is now unreachable by ecstore's delete paths",
|
|
);
|
|
}
|
|
|
|
/// Unregister the process-wide object mutation hook.
|
|
///
|
|
/// Config reloads use this when body caching becomes disabled so the previous
|
|
/// adapter and its cached plaintext bodies are not retained until their TTL.
|
|
pub fn unregister_object_mutation_hook() {
|
|
OBJECT_MUTATION_HOOK.clear();
|
|
}
|
|
|
|
/// The registered hook, if any.
|
|
fn object_mutation_hook() -> Option<Arc<dyn ObjectMutationHook>> {
|
|
OBJECT_MUTATION_HOOK.get()
|
|
}
|
|
|
|
/// Invoke the registered hook for `(bucket, object)`, if one is installed.
|
|
///
|
|
/// A single `None` branch when the cache feature is off, so the ecstore delete
|
|
/// paths pay nothing beyond one relaxed lock read when unconfigured.
|
|
pub(crate) async fn notify_object_mutation(bucket: &str, object: &str) {
|
|
if let Some(hook) = object_mutation_hook() {
|
|
hook.after_object_mutation(bucket, object).await;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Mutex;
|
|
|
|
struct RecordingHook {
|
|
calls: Arc<Mutex<Vec<(String, String)>>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ObjectMutationHook for RecordingHook {
|
|
async fn after_object_mutation(&self, bucket: &str, object: &str) {
|
|
self.calls.lock().unwrap().push((bucket.to_string(), object.to_string()));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial(object_mutation_hook)]
|
|
async fn notify_invokes_registered_hook_with_identity() {
|
|
unregister_object_mutation_hook();
|
|
let calls = Arc::new(Mutex::new(Vec::new()));
|
|
register_object_mutation_hook(Arc::new(RecordingHook {
|
|
calls: Arc::clone(&calls),
|
|
}));
|
|
|
|
notify_object_mutation("bucket", "photos/a.jpg").await;
|
|
|
|
assert_eq!(&*calls.lock().unwrap(), &[("bucket".to_string(), "photos/a.jpg".to_string())]);
|
|
unregister_object_mutation_hook();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial_test::serial(object_mutation_hook)]
|
|
async fn unregister_prevents_later_notifications() {
|
|
unregister_object_mutation_hook();
|
|
let calls = Arc::new(Mutex::new(Vec::new()));
|
|
register_object_mutation_hook(Arc::new(RecordingHook {
|
|
calls: Arc::clone(&calls),
|
|
}));
|
|
unregister_object_mutation_hook();
|
|
|
|
notify_object_mutation("bucket", "object").await;
|
|
|
|
assert!(calls.lock().unwrap().is_empty(), "an unregistered hook must receive no mutation callback");
|
|
}
|
|
|
|
#[test]
|
|
#[serial_test::serial(object_mutation_hook)]
|
|
fn unregister_releases_the_previous_hook() {
|
|
unregister_object_mutation_hook();
|
|
let calls = Arc::new(Mutex::new(Vec::new()));
|
|
let hook = Arc::new(RecordingHook { calls });
|
|
let weak = Arc::downgrade(&hook);
|
|
register_object_mutation_hook(hook);
|
|
|
|
assert!(weak.upgrade().is_some());
|
|
unregister_object_mutation_hook();
|
|
assert!(weak.upgrade().is_none());
|
|
}
|
|
}
|