fix(replication): persist force-delete handoff state (#5641)

* fix(replication): persist force-delete handoff state

* fix(arch): route force-delete config access through boundary

* style: format force-delete imports

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
cxymds
2026-08-03 09:44:28 +08:00
committed by GitHub
parent 035ce5d784
commit 380ec74ece
19 changed files with 662 additions and 88 deletions
+54 -6
View File
@@ -41,11 +41,11 @@ use super::storage_api::object_usecase::bucket::{
predict_lifecycle_expiration,
quota::{QuotaCheckResult, QuotaError, QuotaOperation},
replication::{
DeleteReplicationConfigSnapshot, REPLICATE_INCOMING_DELETE, ReplicationStatusType, delete_replication_state_from_config,
delete_replication_version_id, deleted_object_has_pending_replication_delete, has_active_delete_rule,
load_delete_config_snapshot, must_replicate_object, schedule_object_replication, schedule_replication_delete,
schedule_replication_deletes, set_deleted_object_replication_state, should_schedule_delete_replication,
should_use_existing_delete_replication_info,
DeleteReplicationConfigSnapshot, REPLICATE_INCOMING_DELETE, ReplicationStatusType, commit_force_delete_intent,
delete_replication_state_from_config, delete_replication_version_id, deleted_object_has_pending_replication_delete,
force_delete_target_set, has_active_delete_rule, load_delete_config_snapshot, must_replicate_object,
persist_force_delete_intent, schedule_object_replication, schedule_replication_delete, schedule_replication_deletes,
set_deleted_object_replication_state, should_schedule_delete_replication, should_use_existing_delete_replication_info,
},
tagging::decode_tags,
validate_restore_request,
@@ -7137,6 +7137,7 @@ impl DefaultObjectUsecase {
opts.expected_current_version_id = expected_current_version_id.clone();
let replicate_force_delete = force_delete && !replica && has_active_delete_rule(&delete_config_snapshot, &key);
let mut force_delete_intent = None;
// Check Object Lock retention before deletion
// TODO: Future optimization (separate PR) - If performance becomes critical under high delete load:
@@ -7176,6 +7177,17 @@ impl DefaultObjectUsecase {
let _ = invalidate_object_data_cache_before_mutation(&cache_adapter, &bucket, &key).await;
}
if replicate_force_delete
&& let Some((target_arns, generation)) = force_delete_target_set(&delete_config_snapshot, &key)
&& !target_arns.is_empty()
{
let operation_id =
persist_force_delete_intent(store.clone(), bucket.clone(), key.clone(), target_arns.clone(), generation)
.await
.map_err(ApiError::from)?;
force_delete_intent = Some((operation_id, target_arns, generation));
}
let obj_info = {
match store
.delete_object_with_tier_delete_journal(&bucket, &key, opts.clone())
@@ -7183,6 +7195,18 @@ impl DefaultObjectUsecase {
{
Ok(obj) => obj,
Err(err) => {
if let Some((operation_id, _, _)) = force_delete_intent.as_ref()
&& let Err(cleanup_error) =
crate::storage::storage_api::complete_force_delete_intent(store.clone(), *operation_id).await
{
warn!(
bucket = %bucket,
object = %key,
operation_id = %operation_id,
error = %cleanup_error,
"failed to remove uncommitted force-delete intent after local delete failure"
);
}
if is_err_bucket_not_found(&err) {
return Err(S3Error::with_message(S3ErrorCode::NoSuchBucket, "Bucket not found".to_string()));
}
@@ -7211,7 +7235,31 @@ impl DefaultObjectUsecase {
}
if obj_info.name.is_empty() {
if replicate_force_delete {
if let Some((operation_id, target_arns, generation)) = force_delete_intent {
if let Err(error) = commit_force_delete_intent(store.clone(), operation_id).await {
warn!(
bucket = %bucket,
object = %key,
operation_id = %operation_id,
error = %error,
"failed to mark force-delete intent committed after local delete"
);
}
let generation = i64::try_from(generation.unix_timestamp_nanos()).unwrap_or(i64::MAX);
schedule_replication_delete(
StorageDeletedObject {
object_name: key.clone(),
force_delete: true,
force_delete_id: Some(operation_id),
force_delete_target_arns: target_arns,
force_delete_generation: Some(generation),
..Default::default()
},
bucket.clone(),
REPLICATE_INCOMING_DELETE.to_string(),
)
.await;
} else if replicate_force_delete {
let mut delete_object = StorageDeletedObject {
object_name: key.clone(),
force_delete: true,