fix(lifecycle): harden S3 lifecycle rule handling (#4115)

* fix(lifecycle): honor noncurrent version retention

* fix(lifecycle): validate transition rules

* fix(lifecycle): support immediate multipart abort

* fix(lifecycle): harden select restore metadata

* fix(lifecycle): expose stale multipart cleanup to app

* fix(lifecycle): reduce expiry helper arguments
This commit is contained in:
cxymds
2026-06-30 19:06:34 +08:00
committed by GitHub
parent 7484a61fa3
commit f9340da74a
5 changed files with 683 additions and 100 deletions
+44 -2
View File
@@ -23,8 +23,8 @@ use super::storage_api::bucket_usecase::bucket::{
ObjectLockConfigExt as _, VersioningConfigExt as _,
bucket_target_sys::BucketTargetSys,
lifecycle::bucket_lifecycle_ops::{
enqueue_expiry_for_existing_objects, enqueue_transition_for_existing_objects, validate_lifecycle_config,
validate_transition_tier,
enqueue_expiry_for_existing_objects, enqueue_transition_for_existing_objects, run_stale_multipart_upload_cleanup_once,
validate_lifecycle_config, validate_transition_tier,
},
metadata::{
BUCKET_CORS_CONFIG, BUCKET_LIFECYCLE_CONFIG, BUCKET_NOTIFICATION_CONFIG, BUCKET_POLICY_CONFIG,
@@ -756,6 +756,13 @@ fn lifecycle_has_expiry_rules(config: &BucketLifecycleConfiguration) -> bool {
})
}
fn lifecycle_has_abort_multipart_rules(config: &BucketLifecycleConfiguration) -> bool {
config.rules.iter().any(|rule| {
rule.status == ExpirationStatus::from_static(ExpirationStatus::ENABLED)
&& rule.abort_incomplete_multipart_upload.is_some()
})
}
#[derive(Clone, Default)]
pub struct DefaultBucketUsecase {
context: Option<Arc<AppContext>>,
@@ -1721,6 +1728,17 @@ impl DefaultBucketUsecase {
});
}
if lifecycle_has_abort_multipart_rules(&input_cfg)
&& let Some(store) = self.object_store()
{
let bucket_name = bucket.clone();
let request_context = req.extensions.get::<request_context::RequestContext>().cloned();
spawn_background_with_context(request_context, async move {
let deleted = run_stale_multipart_upload_cleanup_once(store).await;
debug!(bucket = %bucket_name, deleted, "completed lifecycle abort multipart cleanup trigger");
});
}
rustfs_scanner::record_dirty_usage_bucket(&bucket);
Ok(S3Response::new(PutBucketLifecycleConfigurationOutput::default()))
}
@@ -2789,6 +2807,30 @@ mod tests {
assert!(lifecycle_has_transition_rules(&config));
}
#[test]
fn lifecycle_has_abort_multipart_rules_accepts_enabled_abort_rule() {
let config = BucketLifecycleConfiguration {
expiry_updated_at: None,
rules: vec![LifecycleRule {
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
expiration: None,
abort_incomplete_multipart_upload: Some(s3s::dto::AbortIncompleteMultipartUpload {
days_after_initiation: Some(0),
}),
del_marker_expiration: None,
filter: None,
id: Some("enabled-abort".to_string()),
noncurrent_version_expiration: None,
noncurrent_version_transitions: None,
prefix: None,
transitions: None,
}],
};
assert!(lifecycle_has_abort_multipart_rules(&config));
assert!(!lifecycle_has_expiry_rules(&config));
}
#[tokio::test]
async fn execute_list_buckets_returns_internal_error_when_store_uninitialized() {
let input = ListBucketsInput::builder().build().unwrap();
+7
View File
@@ -336,6 +336,13 @@ pub(crate) mod bucket {
.await
}
pub(crate) async fn run_stale_multipart_upload_cleanup_once(api: Arc<crate::storage::storage_api::ECStore>) -> usize {
crate::storage::storage_api::ecstore_bucket::lifecycle::bucket_lifecycle_ops::run_stale_multipart_upload_cleanup_once(
api,
)
.await
}
pub(crate) async fn enqueue_transition_immediate(
oi: &crate::storage::storage_api::StorageObjectInfo,
src: LcEventSrc,