From 47b12972ad59a52c2a24eb6d844e8e9d1a54210b Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Tue, 30 Jun 2026 04:11:03 +0800 Subject: [PATCH] refactor(lifecycle): add object lock boundary (#4084) --- crates/ecstore/src/bucket/lifecycle/README.md | 9 +++- .../bucket/lifecycle/bucket_lifecycle_ops.rs | 9 ++-- .../ecstore/src/bucket/lifecycle/evaluator.rs | 4 +- crates/ecstore/src/bucket/lifecycle/mod.rs | 1 + .../bucket/lifecycle/object_lock_boundary.rs | 44 +++++++++++++++++++ scripts/check_architecture_migration_rules.sh | 13 ++++++ 6 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 crates/ecstore/src/bucket/lifecycle/object_lock_boundary.rs diff --git a/crates/ecstore/src/bucket/lifecycle/README.md b/crates/ecstore/src/bucket/lifecycle/README.md index 70468bf4d..a21e7aa8b 100644 --- a/crates/ecstore/src/bucket/lifecycle/README.md +++ b/crates/ecstore/src/bucket/lifecycle/README.md @@ -10,8 +10,8 @@ and tier services. | Module | Current role | Split blocker | |---|---|---| | `core.rs` | Lifecycle rule model, action evaluation, object options, and transition/expiry decisions. | Uses ECStore object metadata types and compatibility DTO re-exports. | -| `bucket_lifecycle_ops.rs` | Worker orchestration, expiry, transition, stale multipart cleanup, audit, replication delete scheduling, and queue state. | Depends on `ECStore`, `SetDisks`, runtime globals, bucket metadata/versioning/replication, disk internals, event notification, and tier services. | -| `evaluator.rs` | Bucket lifecycle evaluation wrapper. | Reads object-lock and replication config from ECStore bucket modules. | +| `bucket_lifecycle_ops.rs` | Worker orchestration, expiry, transition, stale multipart cleanup, audit, replication delete scheduling, and queue state. | Depends on `ECStore`, `SetDisks`, runtime globals, bucket metadata/versioning/replication, object-lock checks, disk internals, event notification, and tier services. | +| `evaluator.rs` | Bucket lifecycle evaluation wrapper. | Uses lifecycle-local object-lock boundary and still reads replication config from ECStore bucket modules. | | `rule.rs` | Lifecycle rule filter helpers. | Uses lifecycle-local tagging boundary. | | `tier_delete_journal.rs` | Remote tier delete journal persistence and recovery. | Uses lifecycle-local config persistence boundary, object IO contracts, metadata bucket paths, and `ECStore`. | | `tier_free_version_recovery.rs` | Free-version recovery queue and object restoration path. | Depends on `ECStore`, object metadata, storage-api contracts, and lifecycle queue callbacks. | @@ -28,6 +28,7 @@ and tier services. | `LifecycleRuntime` | Expiry state, transition state, tier config, deployment ID, local node name, queue metrics, cancellation, and worker sizing. | Direct runtime source/global access and process environment reads inside worker code. | | `LifecycleConfigStore` | Persist, read, and remove lifecycle-owned journal/config objects. | Direct ECStore config persistence helper imports from worker paths. | | `LifecycleTagFilter` | Decode object tag strings for lifecycle rule matching. | Direct bucket tagging helper imports from lifecycle rule paths. | +| `LifecycleObjectLockStore` | Object-lock retention and deletion checks used by lifecycle evaluation and worker deletion paths. | Direct object-lock module imports from lifecycle code. | | `LifecycleReplicationSink` | Lifecycle-originated delete and version-purge replication scheduling. | Direct imports from bucket replication modules. | | `LifecycleAuditSink` | Lifecycle audit and notification event emission. | Direct event notification service calls and audit-side effects from worker code. | @@ -56,3 +57,7 @@ Current first boundary: `runtime_boundary.rs` centralizes lifecycle access to runtime state while preserving the existing ECStore-backed implementations. `config_boundary.rs` centralizes lifecycle-owned config object persistence for tier delete journal recovery while preserving the existing ECStore config store. +`tagging_boundary.rs` centralizes lifecycle tag decoding while preserving the +existing ECStore bucket tagging implementation. +`object_lock_boundary.rs` centralizes lifecycle object-lock checks while +preserving the existing ECStore object-lock implementation. diff --git a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs index b60f7a617..0ab7d529c 100644 --- a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs +++ b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::runtime_boundary as runtime_sources; +use super::{object_lock_boundary, runtime_boundary as runtime_sources}; use crate::bucket::lifecycle::bucket_lifecycle_audit::{ LcAuditEvent, LcEventSrc, emit_non_transitioned_expiration_event, emit_transition_complete_event, emit_transition_failed_event, emit_transitioned_expiration_event, @@ -25,7 +25,6 @@ use crate::bucket::lifecycle::tier_delete_journal::{process_tier_delete_journal_ use crate::bucket::lifecycle::tier_free_version_recovery::{DEFAULT_FREE_VERSION_RECOVERY_LIMIT, recover_tier_free_versions}; use crate::bucket::lifecycle::tier_last_day_stats::{DailyAllTierStats, LastDayTierStats}; use crate::bucket::lifecycle::tier_sweeper::{Jentry, delete_object_from_remote_tier_idempotent}; -use crate::bucket::object_lock::objectlock_sys::check_object_lock_for_deletion; use crate::bucket::replication::{ DeletedObjectReplicationInfo, ReplicationConfig, check_replicate_delete, schedule_replication_delete, }; @@ -2522,7 +2521,11 @@ pub async fn eval_action_from_lifecycle( return lifecycle::Event::default(); } // Lifecycle operations should never bypass governance retention - if lock_enabled && check_object_lock_for_deletion(&oi.bucket, oi, false).await.is_some() { + if lock_enabled + && object_lock_boundary::check_object_lock_for_deletion(&oi.bucket, oi, false) + .await + .is_some() + { //if serverDebugLog { if oi.version_id.is_some() { debug!( diff --git a/crates/ecstore/src/bucket/lifecycle/evaluator.rs b/crates/ecstore/src/bucket/lifecycle/evaluator.rs index 9b759ac94..cf0767e39 100644 --- a/crates/ecstore/src/bucket/lifecycle/evaluator.rs +++ b/crates/ecstore/src/bucket/lifecycle/evaluator.rs @@ -18,8 +18,8 @@ use s3s::dto::{BucketLifecycleConfiguration, ObjectLockConfiguration, ObjectLock use time::OffsetDateTime; use tracing::info; +use super::object_lock_boundary; use crate::bucket::lifecycle::lifecycle::{Event, Lifecycle, ObjectOpts}; -use crate::bucket::object_lock::objectlock_sys::is_object_locked_by_metadata; use crate::bucket::replication::ReplicationConfig; use rustfs_common::metrics::IlmAction; @@ -89,7 +89,7 @@ impl Evaluator { } // Use the common function to check if the object is locked - is_object_locked_by_metadata(&obj.user_defined, obj.delete_marker) + object_lock_boundary::is_object_locked_by_metadata(&obj.user_defined, obj.delete_marker) } /// eval will return a lifecycle event for each object in objs for a given time. diff --git a/crates/ecstore/src/bucket/lifecycle/mod.rs b/crates/ecstore/src/bucket/lifecycle/mod.rs index bbe9ef8d1..55b65a6b0 100644 --- a/crates/ecstore/src/bucket/lifecycle/mod.rs +++ b/crates/ecstore/src/bucket/lifecycle/mod.rs @@ -17,6 +17,7 @@ pub mod bucket_lifecycle_ops; mod config_boundary; pub mod core; pub mod evaluator; +mod object_lock_boundary; pub use self::core as lifecycle; pub mod rule; mod runtime_boundary; diff --git a/crates/ecstore/src/bucket/lifecycle/object_lock_boundary.rs b/crates/ecstore/src/bucket/lifecycle/object_lock_boundary.rs new file mode 100644 index 000000000..4aa598228 --- /dev/null +++ b/crates/ecstore/src/bucket/lifecycle/object_lock_boundary.rs @@ -0,0 +1,44 @@ +// 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::collections::HashMap; + +use crate::bucket::object_lock::objectlock_sys::{self, ObjectLockBlockReason}; +use crate::object_api::ObjectInfo; + +pub(crate) fn is_object_locked_by_metadata(user_defined: &HashMap, is_delete_marker: bool) -> bool { + objectlock_sys::is_object_locked_by_metadata(user_defined, is_delete_marker) +} + +pub(crate) async fn check_object_lock_for_deletion( + bucket: &str, + obj_info: &ObjectInfo, + bypass_governance: bool, +) -> Option { + objectlock_sys::check_object_lock_for_deletion(bucket, obj_info, bypass_governance).await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_object_locked_by_metadata_preserves_object_lock_parser_behavior() { + let mut user_defined = HashMap::new(); + user_defined.insert("x-amz-object-lock-legal-hold".to_string(), "ON".to_string()); + + assert!(is_object_locked_by_metadata(&user_defined, false)); + assert!(!is_object_locked_by_metadata(&user_defined, true)); + } +} diff --git a/scripts/check_architecture_migration_rules.sh b/scripts/check_architecture_migration_rules.sh index 5516a4213..16c024260 100755 --- a/scripts/check_architecture_migration_rules.sh +++ b/scripts/check_architecture_migration_rules.sh @@ -125,6 +125,7 @@ STORE_API_LIFECYCLE_HELPER_OLD_CONSUMER_HITS_FILE="${TMP_DIR}/store_api_lifecycl LIFECYCLE_AUDIT_SINK_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_audit_sink_bypass_hits.txt" LIFECYCLE_CONFIG_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_config_boundary_bypass_hits.txt" LIFECYCLE_TAGGING_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_tagging_boundary_bypass_hits.txt" +LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/lifecycle_object_lock_boundary_bypass_hits.txt" STORE_API_EXTERNAL_LIST_CONSUMER_HITS_FILE="${TMP_DIR}/store_api_external_list_consumer_hits.txt" STORE_API_EXTERNAL_OPERATION_CONSUMER_HITS_FILE="${TMP_DIR}/store_api_external_operation_consumer_hits.txt" STORE_API_OBJECT_OPERATION_LOCAL_METHOD_HITS_FILE="${TMP_DIR}/store_api_object_operation_local_method_hits.txt" @@ -828,6 +829,18 @@ if [[ -s "$LIFECYCLE_TAGGING_BOUNDARY_BYPASS_HITS_FILE" ]]; then report_failure "lifecycle tag decoding must stay behind lifecycle tagging_boundary: $(paste -sd '; ' "$LIFECYCLE_TAGGING_BOUNDARY_BYPASS_HITS_FILE")" fi +( + cd "$ROOT_DIR" + rg -n --with-filename 'crate::bucket::object_lock::objectlock_sys::(check_object_lock_for_deletion|is_object_locked_by_metadata)|use crate::bucket::object_lock::objectlock_sys' \ + crates/ecstore/src/bucket/lifecycle \ + --glob '*.rs' | + rg -v '^crates/ecstore/src/bucket/lifecycle/object_lock_boundary\.rs:' || true +) >"$LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE" + +if [[ -s "$LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE" ]]; then + report_failure "lifecycle object-lock checks must stay behind lifecycle object_lock_boundary: $(paste -sd '; ' "$LIFECYCLE_OBJECT_LOCK_BOUNDARY_BYPASS_HITS_FILE")" +fi + ( cd "$ROOT_DIR" rg -n --no-heading 'rustfs_ecstore::store_api(?:::\{[^}]*\b(?:ListObjectVersionsInfo|ListObjectsV2Info|ObjectInfoOrErr)\b|::(?:ListObjectVersionsInfo|ListObjectsV2Info|ObjectInfoOrErr)\b)' \