mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 22:33:22 +00:00
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:
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user