fix(lifecycle): correct delete replication fanout (#2609)

Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
cxymds
2026-04-20 18:02:59 +08:00
committed by GitHub
parent 12f355a3bc
commit 1511f9eacb
5 changed files with 693 additions and 50 deletions
@@ -16,12 +16,26 @@ use std::sync::Arc;
use tracing::warn;
use crate::bucket::lifecycle::lifecycle;
use crate::bucket::replication::{DeletedObjectReplicationInfo, check_replicate_delete, schedule_replication_delete};
use crate::bucket::versioning::VersioningApi;
use crate::bucket::versioning_sys::BucketVersioningSys;
use crate::store::ECStore;
use crate::store_api::{ObjectOperations, ObjectOptions, ObjectToDelete};
use rustfs_filemeta::{REPLICATE_INCOMING_DELETE, ReplicationState, version_purge_statuses_map};
use rustfs_lock::MAX_DELETE_LIST;
fn lifecycle_version_delete_replication_state(
replicate_decision_str: String,
pending_status: Option<String>,
) -> ReplicationState {
ReplicationState {
replicate_decision_str,
version_purge_status_internal: pending_status.clone(),
purge_targets: version_purge_statuses_map(pending_status.as_deref().unwrap_or_default()),
..Default::default()
}
}
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(),
@@ -39,7 +53,37 @@ pub async fn delete_object_versions(api: &Arc<ECStore>, bucket: &str, to_del: &[
} else {
remaining = &[];
}
let (_deleted_objs, errors) = api
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 = check_replicate_delete(bucket, object, &info, &opts, None).await;
dsc.replicate_any()
.then(|| lifecycle_version_delete_replication_state(dsc.to_string(), dsc.pending_status()))
}
Err(err) => {
warn!(
bucket,
object = %object.object_name,
version_id = ?version_id,
error = ?err,
"failed to get object info during lifecycle noncurrent version cleanup; skipping delete replication scheduling"
);
None
}
};
replication_candidates.push(candidate);
}
let (mut deleted_objs, errors) = api
.delete_objects(
bucket,
to_del.to_vec(),
@@ -49,6 +93,24 @@ pub async fn delete_object_versions(api: &Arc<ECStore>, bucket: &str, to_del: &[
},
)
.await;
for (i, deleted_obj) in deleted_objs.iter_mut().enumerate() {
if errors.get(i).and_then(|err| err.as_ref()).is_some() {
continue;
}
let Some(replication_state) = replication_candidates.get(i).and_then(|c| c.clone()) else {
continue;
};
deleted_obj.replication_state = Some(replication_state);
schedule_replication_delete(DeletedObjectReplicationInfo {
delete_object: deleted_obj.clone(),
bucket: bucket.to_string(),
event_type: REPLICATE_INCOMING_DELETE.to_string(),
..Default::default()
})
.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>");
@@ -65,3 +127,20 @@ pub async fn delete_object_versions(api: &Arc<ECStore>, bucket: &str, to_del: &[
}
}
}
#[cfg(test)]
mod tests {
use super::lifecycle_version_delete_replication_state;
#[test]
fn lifecycle_version_delete_replication_state_tracks_pending_purge_targets() {
let state = lifecycle_version_delete_replication_state(
"arn:aws:s3:::target=true;false;arn:aws:s3:::target;".to_string(),
Some("arn:aws:s3:::target=PENDING;".to_string()),
);
assert_eq!(state.version_purge_status_internal.as_deref(), Some("arn:aws:s3:::target=PENDING;"));
assert!(state.purge_targets.contains_key("arn:aws:s3:::target"));
assert_eq!(state.replicate_decision_str, "arn:aws:s3:::target=true;false;arn:aws:s3:::target;");
}
}