mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
refactor(replication): move resync decision contracts (#4173)
* refactor(replication): move resync decision contracts * fix(replication): avoid resync status clones
This commit is contained in:
@@ -16,7 +16,6 @@ pub(crate) use rustfs_replication::{MrfOpKind, MrfReplicateEntry};
|
||||
pub(crate) use rustfs_replication::{
|
||||
REPLICATE_EXISTING, REPLICATE_EXISTING_DELETE, REPLICATE_HEAL, REPLICATE_HEAL_DELETE, REPLICATE_INCOMING_DELETE,
|
||||
ReplicateDecision, ReplicateObjectInfo, ReplicateTargetDecision, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction,
|
||||
ReplicationState, ReplicationStatusType, ReplicationType, ReplicationWorkerOperation, ResyncDecision, ResyncTargetDecision,
|
||||
VersionPurgeStatusType, get_replication_state, parse_replicate_decision, replication_statuses_map, target_reset_header,
|
||||
version_purge_statuses_map,
|
||||
ReplicationState, ReplicationStatusType, ReplicationType, ReplicationWorkerOperation, ResyncDecision, VersionPurgeStatusType,
|
||||
get_replication_state, parse_replicate_decision, replication_statuses_map, target_reset_header, version_purge_statuses_map,
|
||||
};
|
||||
|
||||
@@ -21,8 +21,8 @@ use super::replication_event_sink::{EventArgs, send_event, send_local_event};
|
||||
use super::replication_filemeta_boundary::{
|
||||
MrfReplicateEntry, REPLICATE_EXISTING, REPLICATE_EXISTING_DELETE, ReplicateDecision, ReplicateObjectInfo,
|
||||
ReplicateTargetDecision, ReplicatedInfos, ReplicatedTargetInfo, ReplicationAction, ReplicationStatusType, ReplicationType,
|
||||
ResyncDecision, ResyncTargetDecision, VersionPurgeStatusType, get_replication_state, parse_replicate_decision,
|
||||
replication_statuses_map, target_reset_header, version_purge_statuses_map,
|
||||
ResyncDecision, VersionPurgeStatusType, get_replication_state, parse_replicate_decision, replication_statuses_map,
|
||||
target_reset_header, version_purge_statuses_map,
|
||||
};
|
||||
use super::replication_lock_boundary::ReplicationLockTiming;
|
||||
use super::replication_metadata_boundary::ReplicationMetadataStore;
|
||||
@@ -55,12 +55,15 @@ use http_body::Frame;
|
||||
use http_body_util::StreamBody;
|
||||
#[cfg(test)]
|
||||
use rmp_serde;
|
||||
#[cfg(test)]
|
||||
use rustfs_replication::content_matches_by_etag;
|
||||
use rustfs_replication::{
|
||||
BucketReplicationResyncStatus, DeletedObjectReplicationInfo, MustReplicateOptions, ReplicationSourceObject,
|
||||
ReplicationTargetObject, ResyncOpts, TargetReplicationResyncStatus, content_matches_by_etag,
|
||||
BucketReplicationResyncStatus, DeletedObjectReplicationInfo, MustReplicateOptions, ReplicationDeleteSource,
|
||||
ReplicationResyncTargetObject, ReplicationSourceObject, ReplicationTargetObject, ResyncOpts, TargetReplicationResyncStatus,
|
||||
delete_replication_missing_source_decision, delete_replication_object_opts, heal_uses_delete_replication_path,
|
||||
is_retryable_delete_replication_head_error, is_version_delete_replication, is_version_id_mismatch,
|
||||
replication_action_for_target, resync_state_accepts_update, should_count_head_proxy_failure,
|
||||
should_retry_delete_marker_purge, target_is_newer_than_source_null_version,
|
||||
replication_action_for_target, replication_etags_match, resync_state_accepts_update, resync_target_for_object,
|
||||
should_count_head_proxy_failure, should_retry_delete_marker_purge, target_is_newer_than_source_null_version,
|
||||
};
|
||||
use rustfs_s3_types::EventName;
|
||||
use rustfs_utils::http::{
|
||||
@@ -69,8 +72,8 @@ use rustfs_utils::http::{
|
||||
SUFFIX_REPLICATION_STATUS, SUFFIX_TAGGING_TIMESTAMP, headers,
|
||||
};
|
||||
use rustfs_utils::http::{
|
||||
SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_RESET_STATUS, SUFFIX_REPLICATION_SSEC_CRC, get_header_map, get_str,
|
||||
has_internal_suffix, insert_header_map, insert_str, is_internal_key,
|
||||
SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, get_str, has_internal_suffix, insert_header_map,
|
||||
insert_str, is_internal_key,
|
||||
};
|
||||
use rustfs_utils::{DEFAULT_SIP_HASH_KEY, sip_hash};
|
||||
use s3s::dto::ReplicationConfiguration;
|
||||
@@ -860,10 +863,6 @@ impl ReplicationResyncer {
|
||||
}
|
||||
}
|
||||
|
||||
fn heal_should_use_check_replicate_delete(oi: &ObjectInfo) -> bool {
|
||||
oi.delete_marker || !oi.version_purge_status.is_empty()
|
||||
}
|
||||
|
||||
pub async fn get_heal_replicate_object_info(oi: &ObjectInfo, rcfg: &ReplicationConfig) -> ReplicateObjectInfo {
|
||||
let mut oi = oi.clone();
|
||||
let mut user_defined = (*oi.user_defined).clone();
|
||||
@@ -891,7 +890,7 @@ pub async fn get_heal_replicate_object_info(oi: &ObjectInfo, rcfg: &ReplicationC
|
||||
}
|
||||
}
|
||||
|
||||
let dsc = if heal_should_use_check_replicate_delete(&oi) {
|
||||
let dsc = if heal_uses_delete_replication_path(oi.delete_marker, &oi.version_purge_status) {
|
||||
check_replicate_delete(
|
||||
oi.bucket.as_str(),
|
||||
&ObjectToDelete {
|
||||
@@ -1064,8 +1063,11 @@ impl ReplicationConfig {
|
||||
{
|
||||
resync_decision.targets.insert(
|
||||
decision.arn.clone(),
|
||||
resync_target(
|
||||
&oi,
|
||||
resync_target_for_object(
|
||||
&ReplicationResyncTargetObject {
|
||||
mod_time: oi.mod_time,
|
||||
user_defined: oi.user_defined.as_ref(),
|
||||
},
|
||||
&target.arn,
|
||||
&target.reset_id,
|
||||
target.reset_before_date,
|
||||
@@ -1079,59 +1081,6 @@ impl ReplicationConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resync_target(
|
||||
oi: &ObjectInfo,
|
||||
arn: &str,
|
||||
reset_id: &str,
|
||||
reset_before_date: Option<OffsetDateTime>,
|
||||
status: ReplicationStatusType,
|
||||
) -> ResyncTargetDecision {
|
||||
let rs = oi
|
||||
.user_defined
|
||||
.get(target_reset_header(arn).as_str())
|
||||
.cloned()
|
||||
.or_else(|| get_header_map(&oi.user_defined, SUFFIX_REPLICATION_RESET_STATUS));
|
||||
|
||||
let mut dec = ResyncTargetDecision::default();
|
||||
|
||||
let mod_time = oi.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
|
||||
if rs.is_none() {
|
||||
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
if !reset_id.is_empty() && mod_time <= reset_before_date {
|
||||
dec.replicate = true;
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = status == ReplicationStatusType::Empty;
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
if reset_id.is_empty() || reset_before_date.is_none() {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let rs = rs.unwrap();
|
||||
let reset_before_date = reset_before_date.unwrap();
|
||||
|
||||
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let new_reset = parts[1] != reset_id;
|
||||
|
||||
if !new_reset && status == ReplicationStatusType::Completed {
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = new_reset && mod_time <= reset_before_date;
|
||||
|
||||
dec
|
||||
}
|
||||
|
||||
pub(crate) fn get_must_replicate_options(
|
||||
user_defined: &HashMap<String, String>,
|
||||
user_tags: String,
|
||||
@@ -1180,7 +1129,15 @@ pub(crate) async fn check_replicate_delete(
|
||||
return ReplicateDecision::default();
|
||||
}
|
||||
|
||||
let opts = delete_replication_object_opts(dobj, oi);
|
||||
let opts = delete_replication_object_opts(
|
||||
dobj,
|
||||
&ReplicationDeleteSource {
|
||||
user_defined: oi.user_defined.as_ref(),
|
||||
user_tags: oi.user_tags.as_str(),
|
||||
delete_marker: oi.delete_marker,
|
||||
replication_status: oi.replication_status.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let tgt_arns = rcfg.filter_target_arns(&opts);
|
||||
let mut dsc = ReplicateDecision::new();
|
||||
@@ -1198,21 +1155,12 @@ pub(crate) async fn check_replicate_delete(
|
||||
// When incoming delete is removal of a delete marker (a.k.a versioned delete),
|
||||
// GetObjectInfo returns extra information even though it returns errFileNotFound
|
||||
if gerr.is_some() {
|
||||
let valid_repl_status = matches!(
|
||||
if let Some(replicate) = delete_replication_missing_source_decision(
|
||||
oi.delete_marker,
|
||||
oi.target_replication_status(&tgt_arn),
|
||||
ReplicationStatusType::Pending | ReplicationStatusType::Completed | ReplicationStatusType::Failed
|
||||
);
|
||||
|
||||
if oi.delete_marker && (valid_repl_status || replicate) {
|
||||
dsc.set(ReplicateTargetDecision::new(tgt_arn, replicate, sync));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can be the case that other cluster is down and duplicate `mc rm --vid`
|
||||
// is issued - this still needs to be replicated back to the other target
|
||||
if oi.version_purge_status != VersionPurgeStatusType::default() {
|
||||
let replicate = oi.version_purge_status == VersionPurgeStatusType::Pending
|
||||
|| oi.version_purge_status == VersionPurgeStatusType::Failed;
|
||||
replicate,
|
||||
&oi.version_purge_status,
|
||||
) {
|
||||
dsc.set(ReplicateTargetDecision::new(tgt_arn, replicate, sync));
|
||||
}
|
||||
continue;
|
||||
@@ -1232,19 +1180,6 @@ pub(crate) async fn check_replicate_delete(
|
||||
dsc
|
||||
}
|
||||
|
||||
fn delete_replication_object_opts(dobj: &ObjectToDelete, oi: &ObjectInfo) -> ObjectOpts {
|
||||
ObjectOpts {
|
||||
name: dobj.object_name.clone(),
|
||||
ssec: rustfs_replication::is_ssec_encrypted(&oi.user_defined),
|
||||
user_tags: (*oi.user_tags).clone(),
|
||||
delete_marker: oi.delete_marker,
|
||||
version_id: dobj.version_id,
|
||||
op_type: ReplicationType::Delete,
|
||||
replica: oi.replication_status == ReplicationStatusType::Replica,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn must_replicate(bucket: &str, object: &str, mopts: MustReplicateOptions) -> ReplicateDecision {
|
||||
if runtime_sources::object_store_handle().is_none() {
|
||||
return ReplicateDecision::default();
|
||||
@@ -2601,12 +2536,7 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
||||
} else if is_version_id_format_mismatch(&e) {
|
||||
// Version-ID format mismatch: retry without versionId and compare ETags.
|
||||
match head_object_fallback(&bucket, &tgt_client, &object).await {
|
||||
Ok(Some(oi))
|
||||
if content_matches_by_etag(
|
||||
&replication_source_object(&object_info),
|
||||
&replication_target_object(&oi),
|
||||
) =>
|
||||
{
|
||||
Ok(Some(oi)) if replication_etags_match(object_info.etag.as_deref(), oi.e_tag.as_deref()) => {
|
||||
rinfo.replication_status = ReplicationStatusType::Completed;
|
||||
rinfo.replication_resynced = true;
|
||||
rinfo.replication_action = ReplicationAction::None;
|
||||
@@ -2973,10 +2903,7 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
||||
// Version-ID format mismatch: retry without versionId and compare ETags.
|
||||
match head_object_fallback(&bucket, &tgt_client, &object).await {
|
||||
Ok(Some(oi)) => {
|
||||
replication_action = if content_matches_by_etag(
|
||||
&replication_source_object(&object_info),
|
||||
&replication_target_object(&oi),
|
||||
) {
|
||||
replication_action = if replication_etags_match(object_info.etag.as_deref(), oi.e_tag.as_deref()) {
|
||||
ReplicationAction::None
|
||||
} else {
|
||||
ReplicationAction::All
|
||||
@@ -3757,81 +3684,6 @@ mod tests {
|
||||
assert_eq!(target.resync_before_date, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_should_use_check_replicate_delete_failed_non_delete_marker() {
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
delete_marker: false,
|
||||
replication_status: ReplicationStatusType::Failed,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
!heal_should_use_check_replicate_delete(&oi),
|
||||
"Failed non-delete-marker object must use must_replicate path so it can be re-queued for heal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_should_use_check_replicate_delete_pending_non_delete_marker_uses_must_replicate_path() {
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
delete_marker: false,
|
||||
replication_status: ReplicationStatusType::Pending,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
!heal_should_use_check_replicate_delete(&oi),
|
||||
"Pending non-delete-marker object must use must_replicate path to evaluate current target set"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_should_use_check_replicate_delete_delete_marker() {
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
delete_marker: true,
|
||||
replication_status: ReplicationStatusType::Failed,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
heal_should_use_check_replicate_delete(&oi),
|
||||
"Delete marker always uses check_replicate_delete path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_should_use_check_replicate_delete_version_purge_status_uses_delete_path() {
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
delete_marker: false,
|
||||
version_purge_status: VersionPurgeStatusType::Pending,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
heal_should_use_check_replicate_delete(&oi),
|
||||
"Version purge entries must use check_replicate_delete path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heal_should_use_check_replicate_delete_completed_non_delete_marker_uses_must_replicate_path() {
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
delete_marker: false,
|
||||
replication_status: ReplicationStatusType::Completed,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
!heal_should_use_check_replicate_delete(&oi),
|
||||
"Completed non-delete-marker object must use must_replicate path so new targets can be evaluated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_replication_action_existing_object_source_newer_null_version_requires_replication() {
|
||||
let source = ObjectInfo {
|
||||
@@ -3872,56 +3724,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resync_target_includes_object_at_reset_before_boundary() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let oi = ObjectInfo {
|
||||
mod_time: Some(reset_before),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let decision = resync_target(&oi, "arn:target", "reset-1", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(
|
||||
decision.replicate,
|
||||
"objects whose mod_time equals reset_before must be included in the reset window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resync_target_replicates_when_reset_id_changes() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let oi = ObjectInfo {
|
||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
||||
user_defined: Arc::new(HashMap::from([(
|
||||
target_reset_header("arn:target"),
|
||||
"1970-01-01T00:00:20Z;old-reset".to_string(),
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let decision = resync_target(&oi, "arn:target", "new-reset", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(decision.replicate, "a new reset id must resync objects marked by an older reset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resync_target_skips_completed_object_for_same_reset_id() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let oi = ObjectInfo {
|
||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
||||
user_defined: Arc::new(HashMap::from([(
|
||||
target_reset_header("arn:target"),
|
||||
"1970-01-01T00:00:20Z;same-reset".to_string(),
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let decision = resync_target(&oi, "arn:target", "same-reset", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(!decision.replicate, "the same completed reset id must not resync again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replicate_object_info_to_object_info_preserves_delete_marker_flag() {
|
||||
let live = ReplicateObjectInfo {
|
||||
@@ -3941,49 +3743,6 @@ mod tests {
|
||||
assert!(delete_marker.to_object_info().delete_marker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_replication_object_opts_marks_replica_deletes() {
|
||||
let dobj = ObjectToDelete {
|
||||
object_name: "obj".to_string(),
|
||||
version_id: Some(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
};
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
replication_status: ReplicationStatusType::Replica,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let opts = delete_replication_object_opts(&dobj, &oi);
|
||||
|
||||
assert!(
|
||||
opts.replica,
|
||||
"replica deletes must preserve replica status for downstream ReplicaModifications rules"
|
||||
);
|
||||
assert_eq!(opts.version_id, dobj.version_id);
|
||||
assert_eq!(opts.name, dobj.object_name);
|
||||
assert_eq!(opts.op_type, ReplicationType::Delete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_replication_object_opts_keeps_non_replica_deletes_local() {
|
||||
let dobj = ObjectToDelete {
|
||||
object_name: "obj".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let oi = ObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "obj".to_string(),
|
||||
replication_status: ReplicationStatusType::Completed,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let opts = delete_replication_object_opts(&dobj, &oi);
|
||||
|
||||
assert!(!opts.replica, "source-originated deletes should not be treated as replica modifications");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_version_delete_replication_for_delete_marker_version_purge() {
|
||||
let dobj = DeletedObject {
|
||||
|
||||
@@ -32,9 +32,12 @@ pub use delete::{
|
||||
pub use mrf::{MrfOpKind, MrfReplicateEntry, decode_mrf_file, encode_mrf_file};
|
||||
pub use object::{
|
||||
ReplicationSourceObject, ReplicationTargetObject, content_matches_by_etag, replication_action_for_target,
|
||||
target_is_newer_than_source_null_version,
|
||||
replication_etags_match, target_is_newer_than_source_null_version,
|
||||
};
|
||||
pub use operation::{
|
||||
MustReplicateOptions, ReplicationDeleteSource, ReplicationResyncTargetObject, delete_replication_missing_source_decision,
|
||||
delete_replication_object_opts, heal_uses_delete_replication_path, is_ssec_encrypted, resync_target_for_object,
|
||||
};
|
||||
pub use operation::{MustReplicateOptions, is_ssec_encrypted};
|
||||
pub use queue::{ReplicationHealQueueResult, ReplicationOperation, ReplicationPriority, ReplicationQueueAdmission};
|
||||
pub use resync::{
|
||||
BucketReplicationResyncStatus, Error, Result, ResyncOpts, ResyncStatusType, TargetReplicationResyncStatus,
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
const AMZ_META_PREFIX: &str = "X-Amz-Meta-";
|
||||
const CONTENT_ENCODING_LOWER: &str = "content-encoding";
|
||||
const REPLICATION_METADATA_COMPARE_KEYS: [&str; 9] = [
|
||||
EXPIRES,
|
||||
CACHE_CONTROL,
|
||||
@@ -62,8 +63,12 @@ pub struct ReplicationTargetObject<'a> {
|
||||
}
|
||||
|
||||
pub fn content_matches_by_etag(source: &ReplicationSourceObject<'_>, target: &ReplicationTargetObject<'_>) -> bool {
|
||||
let source_etag = source.etag.map(trim_etag);
|
||||
let target_etag = target.etag.map(trim_etag);
|
||||
replication_etags_match(source.etag, target.etag)
|
||||
}
|
||||
|
||||
pub fn replication_etags_match(source: Option<&str>, target: Option<&str>) -> bool {
|
||||
let source_etag = source.map(trim_etag);
|
||||
let target_etag = target.map(trim_etag);
|
||||
source_etag.is_some() && source_etag == target_etag
|
||||
}
|
||||
|
||||
@@ -121,7 +126,7 @@ fn content_encoding_differs(source: &ReplicationSourceObject<'_>, target: &Repli
|
||||
.and_then(|metadata| {
|
||||
metadata
|
||||
.get(CONTENT_ENCODING)
|
||||
.or_else(|| metadata.get(&CONTENT_ENCODING.to_lowercase()))
|
||||
.or_else(|| metadata.get(CONTENT_ENCODING_LOWER))
|
||||
})
|
||||
.is_none_or(|enc| enc != content_encoding);
|
||||
}
|
||||
@@ -130,14 +135,11 @@ fn content_encoding_differs(source: &ReplicationSourceObject<'_>, target: &Repli
|
||||
|
||||
fn tag_metadata_differs(source: &ReplicationSourceObject<'_>, target: &ReplicationTargetObject<'_>) -> bool {
|
||||
let source_tags = ReplicationTagFilter::decode_tags_to_map(source.user_tags);
|
||||
let target_tags = ReplicationTagFilter::decode_tags_to_map(
|
||||
target
|
||||
.metadata
|
||||
.and_then(|metadata| metadata.get(AMZ_OBJECT_TAGGING))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
);
|
||||
let target_tagging = target
|
||||
.metadata
|
||||
.and_then(|metadata| metadata.get(AMZ_OBJECT_TAGGING).map(String::as_str))
|
||||
.unwrap_or_default();
|
||||
let target_tags = ReplicationTagFilter::decode_tags_to_map(target_tagging);
|
||||
let source_tag_count = match i32::try_from(source_tags.len()) {
|
||||
Ok(count) => count,
|
||||
Err(_) => i32::MAX,
|
||||
@@ -163,7 +165,7 @@ fn comparable_metadata(metadata: Option<&HashMap<String, String>>) -> HashMap<St
|
||||
mod tests {
|
||||
use super::{
|
||||
ReplicationSourceObject, ReplicationTargetObject, content_matches_by_etag, replication_action_for_target,
|
||||
target_is_newer_than_source_null_version,
|
||||
replication_etags_match, target_is_newer_than_source_null_version,
|
||||
};
|
||||
use rustfs_filemeta::{ReplicationAction, ReplicationType};
|
||||
use std::collections::HashMap;
|
||||
@@ -214,6 +216,7 @@ mod tests {
|
||||
};
|
||||
|
||||
assert!(content_matches_by_etag(&source, &target));
|
||||
assert!(replication_etags_match(source.etag, target.etag));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rustfs_storage_api::ObjectToDelete;
|
||||
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 time::OffsetDateTime;
|
||||
|
||||
use crate::{ReplicationStatusType, ReplicationType};
|
||||
use crate::{
|
||||
ObjectOpts, ReplicationStatusType, ReplicationType, ResyncTargetDecision, VersionPurgeStatusType, target_reset_header,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MustReplicateOptions {
|
||||
@@ -71,12 +76,128 @@ pub fn is_ssec_encrypted(user_defined: &HashMap<String, String>) -> bool {
|
||||
|| user_defined.contains_key(SSEC_KEY_MD5_HEADER)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplicationDeleteSource<'a> {
|
||||
pub user_defined: &'a HashMap<String, String>,
|
||||
pub user_tags: &'a str,
|
||||
pub delete_marker: bool,
|
||||
pub replication_status: ReplicationStatusType,
|
||||
}
|
||||
|
||||
pub fn delete_replication_object_opts(dobj: &ObjectToDelete, source: &ReplicationDeleteSource<'_>) -> ObjectOpts {
|
||||
ObjectOpts {
|
||||
name: dobj.object_name.clone(),
|
||||
ssec: is_ssec_encrypted(source.user_defined),
|
||||
user_tags: source.user_tags.to_string(),
|
||||
delete_marker: source.delete_marker,
|
||||
version_id: dobj.version_id,
|
||||
op_type: ReplicationType::Delete,
|
||||
replica: source.replication_status == ReplicationStatusType::Replica,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heal_uses_delete_replication_path(delete_marker: bool, version_purge_status: &VersionPurgeStatusType) -> bool {
|
||||
delete_marker || !version_purge_status.is_empty()
|
||||
}
|
||||
|
||||
pub fn delete_replication_missing_source_decision(
|
||||
delete_marker: bool,
|
||||
target_status: ReplicationStatusType,
|
||||
rule_replicates: bool,
|
||||
version_purge_status: &VersionPurgeStatusType,
|
||||
) -> Option<bool> {
|
||||
let valid_replication_status = matches!(
|
||||
target_status,
|
||||
ReplicationStatusType::Pending | ReplicationStatusType::Completed | ReplicationStatusType::Failed
|
||||
);
|
||||
|
||||
if delete_marker && (valid_replication_status || rule_replicates) {
|
||||
return Some(rule_replicates);
|
||||
}
|
||||
|
||||
if !version_purge_status.is_empty() {
|
||||
return Some(matches!(
|
||||
version_purge_status,
|
||||
&VersionPurgeStatusType::Pending | &VersionPurgeStatusType::Failed
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplicationResyncTargetObject<'a> {
|
||||
pub mod_time: Option<OffsetDateTime>,
|
||||
pub user_defined: &'a HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn resync_target_for_object(
|
||||
object: &ReplicationResyncTargetObject<'_>,
|
||||
arn: &str,
|
||||
reset_id: &str,
|
||||
reset_before_date: Option<OffsetDateTime>,
|
||||
status: ReplicationStatusType,
|
||||
) -> ResyncTargetDecision {
|
||||
let rs = object
|
||||
.user_defined
|
||||
.get(target_reset_header(arn).as_str())
|
||||
.cloned()
|
||||
.or_else(|| get_header_map(object.user_defined, SUFFIX_REPLICATION_RESET_STATUS));
|
||||
|
||||
let mut dec = ResyncTargetDecision::default();
|
||||
|
||||
let mod_time = object.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
|
||||
let Some(rs) = rs else {
|
||||
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
if !reset_id.is_empty() && mod_time <= reset_before_date {
|
||||
dec.replicate = true;
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = status == ReplicationStatusType::Empty;
|
||||
|
||||
return dec;
|
||||
};
|
||||
|
||||
let Some(reset_before_date) = reset_before_date else {
|
||||
return dec;
|
||||
};
|
||||
|
||||
if reset_id.is_empty() {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return dec;
|
||||
}
|
||||
|
||||
let new_reset = parts[1] != reset_id;
|
||||
|
||||
if !new_reset && status == ReplicationStatusType::Completed {
|
||||
return dec;
|
||||
}
|
||||
|
||||
dec.replicate = new_reset && mod_time <= reset_before_date;
|
||||
|
||||
dec
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MustReplicateOptions, is_ssec_encrypted};
|
||||
use crate::{ReplicationStatusType, ReplicationType};
|
||||
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,
|
||||
};
|
||||
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 std::collections::HashMap;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn must_replicate_options_preserves_user_tags_and_operation_type() {
|
||||
@@ -102,4 +223,113 @@ mod tests {
|
||||
assert!(is_ssec_encrypted(&meta));
|
||||
assert!(!is_ssec_encrypted(&HashMap::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heal_delete_path_is_limited_to_delete_markers_and_version_purges() {
|
||||
assert!(heal_uses_delete_replication_path(true, &VersionPurgeStatusType::Empty));
|
||||
assert!(heal_uses_delete_replication_path(false, &VersionPurgeStatusType::Pending));
|
||||
assert!(!heal_uses_delete_replication_path(false, &VersionPurgeStatusType::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_source_delete_decision_keeps_delete_marker_and_purge_rules() {
|
||||
assert_eq!(
|
||||
delete_replication_missing_source_decision(
|
||||
true,
|
||||
ReplicationStatusType::Completed,
|
||||
false,
|
||||
&VersionPurgeStatusType::Empty,
|
||||
),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
delete_replication_missing_source_decision(
|
||||
false,
|
||||
ReplicationStatusType::Empty,
|
||||
false,
|
||||
&VersionPurgeStatusType::Pending,
|
||||
),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
delete_replication_missing_source_decision(
|
||||
false,
|
||||
ReplicationStatusType::Empty,
|
||||
false,
|
||||
&VersionPurgeStatusType::Empty
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_replication_object_opts_marks_replica_deletes() {
|
||||
let version_id = Uuid::new_v4();
|
||||
let user_defined = HashMap::from([(SSEC_ALGORITHM_HEADER.to_string(), "AES256".to_string())]);
|
||||
let dobj = ObjectToDelete {
|
||||
object_name: "obj".to_string(),
|
||||
version_id: Some(version_id),
|
||||
..Default::default()
|
||||
};
|
||||
let source = ReplicationDeleteSource {
|
||||
user_defined: &user_defined,
|
||||
user_tags: "env=prod",
|
||||
delete_marker: true,
|
||||
replication_status: ReplicationStatusType::Replica,
|
||||
};
|
||||
|
||||
let opts = delete_replication_object_opts(&dobj, &source);
|
||||
|
||||
assert!(opts.replica);
|
||||
assert!(opts.ssec);
|
||||
assert_eq!(opts.user_tags, "env=prod");
|
||||
assert_eq!(opts.version_id, Some(version_id));
|
||||
assert_eq!(opts.name, "obj");
|
||||
assert_eq!(opts.op_type, ReplicationType::Delete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resync_target_includes_object_at_reset_before_boundary() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let user_defined = HashMap::new();
|
||||
let object = ReplicationResyncTargetObject {
|
||||
mod_time: Some(reset_before),
|
||||
user_defined: &user_defined,
|
||||
};
|
||||
|
||||
let decision =
|
||||
resync_target_for_object(&object, "arn:target", "reset-1", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(decision.replicate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resync_target_replicates_when_reset_id_changes() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let user_defined = HashMap::from([(target_reset_header("arn:target"), "1970-01-01T00:00:20Z;old-reset".to_string())]);
|
||||
let object = ReplicationResyncTargetObject {
|
||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
||||
user_defined: &user_defined,
|
||||
};
|
||||
|
||||
let decision =
|
||||
resync_target_for_object(&object, "arn:target", "new-reset", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(decision.replicate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resync_target_skips_completed_object_for_same_reset_id() {
|
||||
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
||||
let user_defined = HashMap::from([(target_reset_header("arn:target"), "1970-01-01T00:00:20Z;same-reset".to_string())]);
|
||||
let object = ReplicationResyncTargetObject {
|
||||
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
||||
user_defined: &user_defined,
|
||||
};
|
||||
|
||||
let decision =
|
||||
resync_target_for_object(&object, "arn:target", "same-reset", Some(reset_before), ReplicationStatusType::Completed);
|
||||
|
||||
assert!(!decision.replicate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2568,7 +2568,7 @@ fi
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
replication_operation_status=0
|
||||
rg -n --with-filename '^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct\s+MustReplicateOptions|fn\s+is_ssec_encrypted)\b' \
|
||||
rg -n --with-filename '^\s*(?:pub(?:\([^)]*\))?\s+)?(?:(?:struct)\s+(?:MustReplicateOptions|ReplicationDeleteSource|ReplicationResyncTargetObject)|fn\s+(?:is_ssec_encrypted|delete_replication_object_opts|heal_uses_delete_replication_path|delete_replication_missing_source_decision|resync_target_for_object|resync_target))\b' \
|
||||
crates/ecstore/src/bucket/replication \
|
||||
--glob '*.rs' >"$REPLICATION_OPERATION_CONTRACT_BACKSLIDE_HITS_FILE" || replication_operation_status=$?
|
||||
if [[ "$replication_operation_status" -ne 0 && "$replication_operation_status" -ne 1 ]]; then
|
||||
@@ -2658,7 +2658,7 @@ fi
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
replication_object_compare_status=0
|
||||
rg -n --with-filename '^\s*(?:pub(?:\([^)]*\))?\s+)?(?:(?:struct)\s+(?:ReplicationSourceObject|ReplicationTargetObject)|fn\s+(?:content_matches_by_etag|content_matches|target_is_newer_than_source_null_version|replication_action_for_target|get_replication_action))\b' \
|
||||
rg -n --with-filename '^\s*(?:pub(?:\([^)]*\))?\s+)?(?:(?:struct)\s+(?:ReplicationSourceObject|ReplicationTargetObject)|fn\s+(?:replication_etags_match|content_matches_by_etag|content_matches|target_is_newer_than_source_null_version|replication_action_for_target|get_replication_action))\b' \
|
||||
crates/ecstore/src/bucket/replication \
|
||||
--glob '*.rs' >"$REPLICATION_OBJECT_COMPARE_CONTRACT_BACKSLIDE_HITS_FILE" || replication_object_compare_status=$?
|
||||
if [[ "$replication_object_compare_status" -ne 0 && "$replication_object_compare_status" -ne 1 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user