refactor(replication): move app decisions into crate (#4191)

This commit is contained in:
Zhengchao An
2026-07-02 17:14:55 +08:00
committed by GitHub
parent 902cf1d3dc
commit b972e25c51
5 changed files with 351 additions and 96 deletions
+159 -3
View File
@@ -19,10 +19,14 @@ use rustfs_utils::http::{
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_TAGGING, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER,
SUFFIX_REPLICATION_RESET_STATUS, get_header_map,
};
use s3s::dto::ReplicationConfiguration;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::{
ObjectOpts, ReplicationStatusType, ReplicationType, ResyncTargetDecision, VersionPurgeStatusType, target_reset_header,
ObjectOpts, ReplicateDecision, ReplicateTargetDecision, ReplicationConfigurationExt as _, ReplicationState,
ReplicationStatusType, ReplicationType, ResyncTargetDecision, VersionPurgeStatusType, replication_statuses_map,
target_reset_header, version_purge_statuses_map,
};
#[derive(Debug, Clone, Default)]
@@ -97,6 +101,59 @@ pub fn delete_replication_object_opts(dobj: &ObjectToDelete, source: &Replicatio
}
}
#[derive(Debug, Clone)]
pub struct ReplicationDeleteStateSource {
pub name: String,
pub user_tags: String,
pub version_id: Option<Uuid>,
pub delete_marker: bool,
pub replica: bool,
}
pub fn delete_replication_state_from_config(
config: &ReplicationConfiguration,
source: &ReplicationDeleteStateSource,
) -> Option<ReplicationState> {
let opts = ObjectOpts {
name: source.name.clone(),
user_tags: source.user_tags.clone(),
version_id: source.version_id,
delete_marker: source.delete_marker,
op_type: ReplicationType::Delete,
replica: source.replica,
..Default::default()
};
let target_arns = config.filter_target_arns(&opts);
if target_arns.is_empty() {
return None;
}
let mut decision = ReplicateDecision::new();
for target_arn in target_arns {
let mut target_opts = opts.clone();
target_opts.target_arn = target_arn.clone();
let replicate = config.replicate(&target_opts);
decision.set(ReplicateTargetDecision::new(target_arn, replicate, false));
}
if !decision.replicate_any() {
return None;
}
let pending_status = decision.pending_status();
let mut state = ReplicationState {
replicate_decision_str: decision.to_string(),
..Default::default()
};
if source.version_id.is_some() {
state.version_purge_status_internal = pending_status.clone();
state.purge_targets = version_purge_statuses_map(pending_status.as_deref().unwrap_or_default());
} else {
state.replication_status_internal = pending_status.clone();
state.targets = replication_statuses_map(pending_status.as_deref().unwrap_or_default());
}
Some(state)
}
pub fn heal_uses_delete_replication_path(delete_marker: bool, version_purge_status: &VersionPurgeStatusType) -> bool {
delete_marker || !version_purge_status.is_empty()
}
@@ -189,12 +246,18 @@ pub fn resync_target_for_object(
#[cfg(test)]
mod tests {
use super::{
MustReplicateOptions, ReplicationDeleteSource, ReplicationResyncTargetObject, delete_replication_missing_source_decision,
delete_replication_object_opts, heal_uses_delete_replication_path, is_ssec_encrypted, resync_target_for_object,
MustReplicateOptions, ReplicationDeleteSource, ReplicationDeleteStateSource, ReplicationResyncTargetObject,
delete_replication_missing_source_decision, delete_replication_object_opts, delete_replication_state_from_config,
heal_uses_delete_replication_path, is_ssec_encrypted, resync_target_for_object,
};
use crate::{ReplicationStatusType, ReplicationType, VersionPurgeStatusType, target_reset_header};
use rustfs_storage_api::ObjectToDelete;
use rustfs_utils::http::{AMZ_BUCKET_REPLICATION_STATUS, SSEC_ALGORITHM_HEADER};
use s3s::dto::{
DeleteMarkerReplication, DeleteMarkerReplicationStatus, Destination, ExistingObjectReplication,
ExistingObjectReplicationStatus, ReplicaModifications, ReplicaModificationsStatus, ReplicationConfiguration,
ReplicationRule, ReplicationRuleStatus, SourceSelectionCriteria,
};
use std::collections::HashMap;
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
@@ -288,6 +351,99 @@ mod tests {
assert_eq!(opts.op_type, ReplicationType::Delete);
}
fn delete_replication_rule(arn: &str, replica_modifications: bool) -> ReplicationRule {
ReplicationRule {
delete_marker_replication: Some(DeleteMarkerReplication {
status: Some(DeleteMarkerReplicationStatus::from_static(DeleteMarkerReplicationStatus::ENABLED)),
}),
delete_replication: None,
destination: Destination {
bucket: arn.to_string(),
..Default::default()
},
existing_object_replication: Some(ExistingObjectReplication {
status: ExistingObjectReplicationStatus::from_static(ExistingObjectReplicationStatus::ENABLED),
}),
filter: None,
id: Some("rule-1".to_string()),
prefix: Some("test/".to_string()),
priority: Some(1),
source_selection_criteria: replica_modifications.then_some(SourceSelectionCriteria {
replica_modifications: Some(ReplicaModifications {
status: ReplicaModificationsStatus::from_static(ReplicaModificationsStatus::ENABLED),
}),
sse_kms_encrypted_objects: None,
}),
status: ReplicationRuleStatus::from_static(ReplicationRuleStatus::ENABLED),
}
}
#[test]
fn delete_replication_state_tracks_downstream_delete_marker_targets() {
let arn = "arn:aws:s3:::target-bucket";
let config = ReplicationConfiguration {
role: arn.to_string(),
rules: vec![delete_replication_rule(arn, true)],
};
let source = ReplicationDeleteStateSource {
name: "test/object.txt".to_string(),
user_tags: String::new(),
version_id: None,
delete_marker: true,
replica: true,
};
let state = delete_replication_state_from_config(&config, &source)
.expect("replica delete marker should be forwarded to downstream targets");
let pending = format!("{arn}=PENDING;");
assert_eq!(state.replication_status_internal.as_deref(), Some(pending.as_str()));
assert_eq!(state.replicate_decision_str, format!("{arn}=true;false;{arn};"));
assert!(state.targets.contains_key(arn));
}
#[test]
fn delete_replication_state_skips_replica_delete_without_replica_modifications() {
let arn = "arn:aws:s3:::target-bucket";
let config = ReplicationConfiguration {
role: arn.to_string(),
rules: vec![delete_replication_rule(arn, false)],
};
let source = ReplicationDeleteStateSource {
name: "test/object.txt".to_string(),
user_tags: String::new(),
version_id: None,
delete_marker: true,
replica: true,
};
assert!(delete_replication_state_from_config(&config, &source).is_none());
}
#[test]
fn delete_replication_state_tracks_delete_marker_version_purges() {
let arn = "arn:aws:s3:::target-bucket";
let config = ReplicationConfiguration {
role: arn.to_string(),
rules: vec![delete_replication_rule(arn, false)],
};
let source = ReplicationDeleteStateSource {
name: "test/object.txt".to_string(),
user_tags: String::new(),
version_id: Some(Uuid::new_v4()),
delete_marker: true,
replica: false,
};
let state = delete_replication_state_from_config(&config, &source)
.expect("delete-marker version purge should honor delete-marker replication rules");
let pending = format!("{arn}=PENDING;");
assert_eq!(state.version_purge_status_internal.as_deref(), Some(pending.as_str()));
assert_eq!(state.replicate_decision_str, format!("{arn}=true;false;{arn};"));
assert!(state.purge_targets.contains_key(arn));
}
#[test]
fn resync_target_includes_object_at_reset_before_boundary() {
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);