refactor(lifecycle): add object lock boundary (#4084)

This commit is contained in:
Zhengchao An
2026-06-30 04:11:03 +08:00
committed by GitHub
parent 3d868cb6e8
commit 47b12972ad
6 changed files with 73 additions and 7 deletions
@@ -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.
@@ -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!(
@@ -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.
@@ -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;
@@ -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<String, String>, 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<ObjectLockBlockReason> {
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));
}
}
@@ -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)' \