fix(replication): propagate metadata changes (#5635)

Preserve metadata replication operations in the durable MRF and route tagging, retention, and legal-hold updates through the existing full-object replication transport. Keep ACL propagation outside the contract because the current object model has no durable object ACL state.

Refs #1616
This commit is contained in:
cxymds
2026-08-02 21:50:24 +08:00
committed by GitHub
parent 2cc7443067
commit 779b5a49ea
8 changed files with 294 additions and 156 deletions
+24 -2
View File
@@ -550,6 +550,8 @@ pub enum MrfOpKind {
#[default]
#[serde(rename = "object")]
Object,
#[serde(rename = "metadata")]
Metadata,
#[serde(rename = "delete")]
Delete,
}
@@ -831,7 +833,11 @@ impl ReplicationWorkerOperation for ReplicateObjectInfo {
version_id: self.version_id,
retry_count: retry_count_to_mrf(self.retry_count),
size: self.size,
op: MrfOpKind::Object,
op: if self.op_type == ReplicationType::Metadata {
MrfOpKind::Metadata
} else {
MrfOpKind::Object
},
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
@@ -886,7 +892,11 @@ impl ReplicateObjectInfo {
version_id: self.version_id,
retry_count: retry_count_to_mrf(self.retry_count),
size: self.size,
op: MrfOpKind::Object,
op: if self.op_type == ReplicationType::Metadata {
MrfOpKind::Metadata
} else {
MrfOpKind::Object
},
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
@@ -1114,6 +1124,18 @@ mod tests {
assert_eq!(asynchronous.replicate_target_arns(), vec!["arn:async".to_string()]);
}
#[test]
fn metadata_replication_mrf_entry_preserves_operation_kind() {
let info = ReplicateObjectInfo {
bucket: "bucket".to_string(),
name: "object".to_string(),
op_type: ReplicationType::Metadata,
..Default::default()
};
assert_eq!(info.to_mrf_entry().op, MrfOpKind::Metadata);
}
#[test]
fn target_state_reads_resync_timestamp_from_target_reset_header_key() {
let arn = "arn:rustfs:replication:us-east-1:target:bucket";
+24 -9
View File
@@ -57,10 +57,22 @@ mod tests {
use uuid::Uuid;
#[test]
fn mrf_file_round_trips_object_and_delete_entries() {
fn mrf_file_round_trips_object_metadata_and_delete_entries() {
let obj_vid = Uuid::new_v4();
let del_vid = Uuid::new_v4();
let entries = vec![
MrfReplicateEntry {
bucket: "bucket-a".to_string(),
object: "metadata-a".to_string(),
version_id: Some(obj_vid),
retry_count: 1,
size: 1024,
op: MrfOpKind::Metadata,
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
target_arns: vec!["arn:target-a".to_string()],
},
MrfReplicateEntry {
bucket: "bucket-a".to_string(),
object: "object-a".to_string(),
@@ -90,17 +102,20 @@ mod tests {
let encoded = encode_mrf_file(&entries).expect("mrf file should encode");
let decoded = decode_mrf_file(&encoded).expect("mrf file should decode");
assert_eq!(decoded.len(), 2);
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].version_id, Some(obj_vid));
assert_eq!(decoded[0].op, MrfOpKind::Object);
assert_eq!(decoded[0].target_arns, vec!["arn:target-a".to_string(), "arn:target-b".to_string()]);
assert_eq!(decoded[0].op, MrfOpKind::Metadata);
assert_eq!(decoded[0].target_arns, vec!["arn:target-a".to_string()]);
assert_eq!(decoded[0].delete_marker_mtime, None);
assert_eq!(decoded[1].delete_marker_version_id, Some(del_vid));
assert_eq!(decoded[1].op, MrfOpKind::Delete);
assert_eq!(decoded[1].target_arns, vec!["arn:target-a".to_string()]);
assert!(decoded[1].delete_marker);
assert_eq!(decoded[1].op, MrfOpKind::Object);
assert_eq!(decoded[1].target_arns, vec!["arn:target-a".to_string(), "arn:target-b".to_string()]);
assert_eq!(decoded[1].delete_marker_mtime, None);
assert_eq!(decoded[2].delete_marker_version_id, Some(del_vid));
assert_eq!(decoded[2].op, MrfOpKind::Delete);
assert_eq!(decoded[2].target_arns, vec!["arn:target-a".to_string()]);
assert!(decoded[2].delete_marker);
assert_eq!(
decoded[1].delete_marker_mtime,
decoded[2].delete_marker_mtime,
Some(1_705_312_200_123_456_789),
"delete-marker mtime must survive the MRF disk round-trip"
);
+24
View File
@@ -164,6 +164,7 @@ mod tests {
replication_etags_match, target_is_newer_than_source_null_version,
};
use crate::filemeta::{ReplicationAction, ReplicationType};
use crate::http::AMZ_OBJECT_LOCK_MODE;
use std::collections::HashMap;
use time::{Duration, OffsetDateTime};
@@ -267,4 +268,27 @@ mod tests {
ReplicationAction::Metadata
);
}
#[test]
fn replication_action_detects_tags_and_object_lock_metadata_differences() {
let mut source_metadata = HashMap::new();
source_metadata.insert(AMZ_OBJECT_LOCK_MODE.to_string(), "GOVERNANCE".to_string());
let source = ReplicationSourceObject {
user_tags: "a=1&b=2",
user_defined: &source_metadata,
..source_object(&source_metadata)
};
let target_metadata = HashMap::new();
let target = ReplicationTargetObject {
tag_count: 1,
metadata: Some(&target_metadata),
..target_object(&target_metadata)
};
assert_eq!(
replication_action_for_target(&source, &target, ReplicationType::Metadata),
ReplicationAction::Metadata
);
}
}