Expose target-scoped durable MRF backlog metrics (#5584)

* feat(replication): expose target durable mrf backlog

Add target ARN attribution to durable MRF entries and surface target-scoped durable backlog metrics without changing existing bucket-only metric labels.

Keep legacy MRF files bucket-only by defaulting missing targetARNs to an empty list, and expose target snapshots through an additive API so existing DurableMrfBacklogSummary callers remain source-compatible.

Co-Authored-By: heihutu <heihutu@gmail.com>

* feat(replication): expose runtime target backlog (#5586)

Track runtime replication backlog by target ARN for regular, large, delete, and MRF admission paths while preserving the existing bucket-level backlog semantics.

Add target-scoped current backlog metrics and merge them with durable target backlog snapshots for observability.

Co-authored-by: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-02 01:31:50 +08:00
committed by GitHub
parent 4c83bff81c
commit fbec33bd29
14 changed files with 1241 additions and 112 deletions
+8
View File
@@ -49,6 +49,11 @@ impl ReplicationWorkerOperation for DeletedObjectReplicationInfo {
.delete_object
.delete_marker_mtime
.and_then(|t| i64::try_from(t.unix_timestamp_nanos()).ok()),
target_arns: if self.target_arn.is_empty() {
Vec::new()
} else {
vec![self.target_arn.clone()]
},
}
}
@@ -111,6 +116,7 @@ mod tests {
delete_marker_mtime: Some(mtime),
..Default::default()
},
target_arn: "arn:target-a".to_string(),
..Default::default()
};
@@ -129,6 +135,7 @@ mod tests {
Some(mtime.unix_timestamp_nanos() as i64),
"delete-marker mtime must be persisted in the MRF entry"
);
assert_eq!(entry.target_arns, vec!["arn:target-a".to_string()]);
assert_eq!(info.get_object(), "object");
}
@@ -148,6 +155,7 @@ mod tests {
};
assert_eq!(info.to_mrf_entry().delete_marker_mtime, None);
assert!(info.to_mrf_entry().target_arns.is_empty());
}
#[test]
+73
View File
@@ -593,6 +593,9 @@ pub struct MrfReplicateEntry {
// to preserve pre-existing behaviour (backlog#867).
#[serde(rename = "deleteMarkerMtime", skip_serializing_if = "Option::is_none", default)]
pub delete_marker_mtime: Option<i64>,
#[serde(rename = "targetARNs", skip_serializing_if = "Vec::is_empty", default)]
pub target_arns: Vec<String>,
}
fn retry_count_to_mrf(retry_count: u32) -> i32 {
@@ -672,6 +675,18 @@ impl ReplicateDecision {
}
if result.is_empty() { None } else { Some(result) }
}
pub fn replicate_target_arns(&self) -> Vec<String> {
let mut arns = self
.targets_map
.values()
.filter(|target| target.replicate && !target.arn.is_empty())
.map(|target| target.arn.clone())
.collect::<Vec<_>>();
arns.sort();
arns.dedup();
arns
}
}
impl fmt::Display for ReplicateDecision {
@@ -799,6 +814,7 @@ impl ReplicationWorkerOperation for ReplicateObjectInfo {
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
target_arns: self.dsc.replicate_target_arns(),
}
}
@@ -853,6 +869,7 @@ impl ReplicateObjectInfo {
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
target_arns: self.dsc.replicate_target_arns(),
}
}
}
@@ -994,6 +1011,62 @@ impl Default for ResyncDecision {
mod tests {
use super::*;
#[test]
fn replicate_decision_returns_sorted_unique_replicating_target_arns() {
let mut decision = ReplicateDecision::new();
decision.set(ReplicateTargetDecision {
arn: "arn:target-b".to_string(),
replicate: true,
..Default::default()
});
decision.set(ReplicateTargetDecision {
arn: "arn:target-a".to_string(),
replicate: true,
..Default::default()
});
decision.set(ReplicateTargetDecision {
arn: "arn:target-c".to_string(),
replicate: false,
..Default::default()
});
decision.set(ReplicateTargetDecision {
arn: String::new(),
replicate: true,
..Default::default()
});
assert_eq!(
decision.replicate_target_arns(),
vec!["arn:target-a".to_string(), "arn:target-b".to_string()]
);
}
#[test]
fn replicate_object_info_mrf_entry_carries_replicating_targets() {
let mut decision = ReplicateDecision::new();
decision.set(ReplicateTargetDecision {
arn: "arn:target-a".to_string(),
replicate: true,
..Default::default()
});
decision.set(ReplicateTargetDecision {
arn: "arn:target-b".to_string(),
replicate: false,
..Default::default()
});
let info = ReplicateObjectInfo {
bucket: "bucket".to_string(),
name: "object".to_string(),
size: 42,
dsc: decision,
..Default::default()
};
let entry = info.to_mrf_entry();
assert_eq!(entry.target_arns, vec!["arn:target-a".to_string()]);
}
#[test]
fn target_state_reads_resync_timestamp_from_target_reset_header_key() {
let arn = "arn:rustfs:replication:us-east-1:target:bucket";
+5
View File
@@ -71,6 +71,7 @@ mod tests {
delete_marker_version_id: None,
delete_marker: false,
delete_marker_mtime: None,
target_arns: vec!["arn:target-a".to_string(), "arn:target-b".to_string()],
},
MrfReplicateEntry {
bucket: "bucket-a".to_string(),
@@ -82,6 +83,7 @@ mod tests {
delete_marker_version_id: Some(del_vid),
delete_marker: true,
delete_marker_mtime: Some(1_705_312_200_123_456_789),
target_arns: vec!["arn:target-a".to_string()],
},
];
@@ -91,9 +93,11 @@ mod tests {
assert_eq!(decoded.len(), 2);
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].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].delete_marker_mtime,
@@ -129,6 +133,7 @@ mod tests {
assert_eq!(decoded[0].retry_count, 2);
assert_eq!(decoded[0].size, 100);
assert_eq!(decoded[0].op, MrfOpKind::Object);
assert!(decoded[0].target_arns.is_empty());
// Old files lack the deleteMarkerMtime key; it must default to None so replay keeps the
// pre-#867 fallback to the current time.
assert_eq!(decoded[0].delete_marker_mtime, None);