mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 18:08:21 +00:00
85fd824581
* feat(object-data-cache): close write/delete-side invalidation gaps The object data cache exposed only a single per-(bucket,object) invalidation primitive and no write-side ecstore hook, so several delete paths left dead bodies resident until TTL (hygiene/capacity, not stale-serving: lookups follow a fresh metadata quorum and cannot serve a gone object). This adds the missing primitives and wires them in. ODC-26 (backlog#1131): add an `ObjectMutationHook` trait beside the GET body hook, registered next to it at startup, and call it from the ecstore-internal delete paths (`apply_expiry_on_non_transitioned_objects`, `expire_transitioned_object` including the restored-copy branch, and `delete_object_versions`). The app impl is one `invalidate_object` call under a new `AfterLifecycleExpiry` reason. ODC-27 (backlog#1132): force prefix delete now invalidates the whole prefix, not just the prefix string. `store.delete_object(delete_prefix)` returns no deleted-name list, so this uses a new prefix primitive rather than the batch path. ODC-28 (backlog#1133): DeleteBucket now flushes the bucket via a new bucket-scope primitive (covers force and non-force, which share the delete_bucket call). ODC-C2 (backlog#1143): add `ObjectDataCache::clear()` and two admin handlers (GET stats, POST flush) routed through admin runtime_sources. The starshard identity index gains a single `remove_matching` full-scan API backing prefix/bucket/clear; it is documented as admin/delete-path only and never runs on the GET or fill hot path. New invalidation reasons and metric labels added; outcome (removed/noop) labelling kept correct for every new primitive. Also fixes a pre-existing broken intra-doc link in memory.rs. Co-Authored-By: heihutu <heihutu@gmail.com> * refactor(ecstore): extract the shared HookSlot behind both cache hooks This PR introduced object_mutation_hook.rs by mirroring body_cache_hook.rs, which left two process-global registration slots whose register/get/clear bodies were line-for-line identical except the trait type and the WARN string: a RwLock<Option<Arc<dyn _>>>, an Arc::ptr_eq "different instance" warning, the poison-recovery closure, and the same read-lock-and-clone read. Two copies of the same swap-vs-warn logic can drift apart under maintenance. Hoist it into a generic HookSlot<T: ?Sized> that owns the logic once. Each hook module keeps its `static HOOK: HookSlot<dyn XxxHook>` and its thin, unchanged public wrappers (register_/get_/clear_), so the crate's public surface and every call site are untouched — this is an internal consolidation, not a contract change. The load-bearing #1126 guarantee (newest registration wins, so a rebuilt AppContext is never stranded on a first-wins slot) previously had no direct test — the hook tests only covered register-then-notify. HookSlot now has its own unit tests including re_registration_swaps_to_the_latest_instance; mutation-testing confirms a first-wins regression fails exactly that test. No behavior change: the two hooks' existing tests, the P0 body_cache_hook_e2e regressions, and the app-layer mutation-hook tests all pass unchanged. Refs: backlog#1126, backlog#1131 Co-Authored-By: heihutu <heihutu@gmail.com> * fix(admin): register the object-data-cache routes in the policy inventory This PR added GET /object-data-cache/stats and POST /object-data-cache/flush but did not list them in the two registries that must account for every admin route: the route-policy inventory (route_policy.rs) and the route matrix (route_registration_test.rs). Their coverage tests — route_policy_inventory_covers_registered_routes and test_admin_route_matrix_matches_registered_routes — failed on CI because a registered route had no policy/matrix entry. These two tests are not part of `make pre-commit` (which runs fmt + arch + quick-check, not the full suite), so the gap passed local pre-commit and only surfaced in the CI Test-and-Lint lane. stats is a read (ServerInfoAdminAction, Sensitive); flush mutates (ConfigUpdateAdminAction, High) — matching the actions the handlers already enforce. The MinIO-alias matrix test is unaffected: these are native rustfs endpoints with no MinIO equivalent. Refs: backlog#1143 Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
143 lines
5.7 KiB
Rust
143 lines
5.7 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.
|
|
|
|
use std::sync::Arc;
|
|
use tracing::debug;
|
|
|
|
const LOG_COMPONENT_ECSTORE: &str = "ecstore";
|
|
const LOG_SUBSYSTEM_LIFECYCLE: &str = "lifecycle";
|
|
const EVENT_LIFECYCLE_CLEANUP_SKIPPED: &str = "lifecycle_cleanup_skipped";
|
|
const EVENT_LIFECYCLE_CLEANUP_FAILED: &str = "lifecycle_cleanup_failed";
|
|
|
|
use crate::bucket::lifecycle::lifecycle;
|
|
use crate::bucket::replication::{ReplicationLifecycleBridge, ReplicationState, replication_state_to_filemeta};
|
|
use crate::bucket::versioning::VersioningApi;
|
|
use crate::bucket::versioning_sys::BucketVersioningSys;
|
|
use crate::object_api::ObjectOptions;
|
|
use crate::storage_api_contracts::object::{ObjectOperations as _, ObjectToDelete};
|
|
use crate::store::ECStore;
|
|
use rustfs_lock::MAX_DELETE_LIST;
|
|
|
|
pub async fn delete_object_versions(api: &Arc<ECStore>, bucket: &str, to_del: &[ObjectToDelete], _lc_event: lifecycle::Event) {
|
|
let version_suspended = match BucketVersioningSys::get(bucket).await {
|
|
Ok(vc) => vc.suspended(),
|
|
Err(err) => {
|
|
debug!(
|
|
event = EVENT_LIFECYCLE_CLEANUP_SKIPPED,
|
|
component = LOG_COMPONENT_ECSTORE,
|
|
subsystem = LOG_SUBSYSTEM_LIFECYCLE,
|
|
bucket,
|
|
error = ?err,
|
|
reason = "versioning_config_unavailable",
|
|
"Skipped lifecycle noncurrent version cleanup"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
let mut remaining = to_del;
|
|
loop {
|
|
let mut to_del = remaining;
|
|
if to_del.len() > MAX_DELETE_LIST {
|
|
remaining = &to_del[MAX_DELETE_LIST..];
|
|
to_del = &to_del[..MAX_DELETE_LIST];
|
|
} else {
|
|
remaining = &[];
|
|
}
|
|
|
|
let mut replication_candidates: Vec<Option<ReplicationState>> = Vec::with_capacity(to_del.len());
|
|
for object in to_del.iter() {
|
|
let version_id = object.version_id.map(|vid| vid.to_string());
|
|
let opts = ObjectOptions {
|
|
version_id: version_id.clone(),
|
|
versioned: true,
|
|
version_suspended,
|
|
..Default::default()
|
|
};
|
|
let candidate = match api.get_object_info(bucket, &object.object_name, &opts).await {
|
|
Ok(info) => {
|
|
let dsc = ReplicationLifecycleBridge::check_delete_replication(bucket, object, &info, &opts).await;
|
|
dsc.replicate_any()
|
|
.then(|| ReplicationLifecycleBridge::version_delete_replication_state(&dsc))
|
|
}
|
|
Err(err) => {
|
|
debug!(
|
|
event = EVENT_LIFECYCLE_CLEANUP_SKIPPED,
|
|
component = LOG_COMPONENT_ECSTORE,
|
|
subsystem = LOG_SUBSYSTEM_LIFECYCLE,
|
|
bucket,
|
|
object = %object.object_name,
|
|
version_id = ?version_id,
|
|
error = ?err,
|
|
reason = "object_info_unavailable",
|
|
"Skipped lifecycle delete replication scheduling"
|
|
);
|
|
None
|
|
}
|
|
};
|
|
replication_candidates.push(candidate);
|
|
}
|
|
|
|
let (mut deleted_objs, errors) = api
|
|
.delete_objects(
|
|
bucket,
|
|
to_del.to_vec(),
|
|
ObjectOptions {
|
|
version_suspended,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
for (i, deleted_obj) in deleted_objs.iter_mut().enumerate() {
|
|
if errors.get(i).and_then(|err| err.as_ref()).is_some() {
|
|
continue;
|
|
}
|
|
// Evict any cached body for the successfully deleted noncurrent
|
|
// version so it does not sit resident until TTL (ODC-26).
|
|
if let Some(target) = to_del.get(i) {
|
|
crate::object_api::notify_object_mutation(bucket, &target.object_name).await;
|
|
}
|
|
let Some(replication_state) = replication_candidates.get(i).and_then(|c| c.clone()) else {
|
|
continue;
|
|
};
|
|
deleted_obj.replication_state = Some(replication_state_to_filemeta(&replication_state));
|
|
ReplicationLifecycleBridge::schedule_delete(bucket.to_string(), deleted_obj.clone()).await;
|
|
}
|
|
|
|
for (i, err) in errors.iter().enumerate() {
|
|
if let Some(e) = err {
|
|
let obj_name = to_del.get(i).map(|o| o.object_name.as_str()).unwrap_or("<unknown>");
|
|
let vid = to_del
|
|
.get(i)
|
|
.and_then(|o| o.version_id)
|
|
.map(|v| v.to_string())
|
|
.unwrap_or_default();
|
|
debug!(
|
|
event = EVENT_LIFECYCLE_CLEANUP_FAILED,
|
|
component = LOG_COMPONENT_ECSTORE,
|
|
subsystem = LOG_SUBSYSTEM_LIFECYCLE,
|
|
bucket,
|
|
object = obj_name,
|
|
version_id = %vid,
|
|
error = ?e,
|
|
"Failed lifecycle noncurrent version cleanup"
|
|
);
|
|
}
|
|
}
|
|
if remaining.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
}
|