mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-17 10:17:55 +00:00
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:
@@ -173,8 +173,9 @@ pub mod bucket {
|
||||
|
||||
pub mod replication {
|
||||
pub use crate::bucket::replication::replication_pool::{
|
||||
DurableMrfBacklogSummary, DurableMrfBucketBacklog, MrfBacklogObservabilitySummary, MrfBucketBacklogObservability,
|
||||
durable_mrf_backlog_summary_snapshot, mrf_backlog_observability_snapshot,
|
||||
DurableMrfBacklogSummary, DurableMrfBucketBacklog, DurableMrfTargetBacklog, MrfBacklogObservabilitySummary,
|
||||
MrfBucketBacklogObservability, durable_mrf_backlog_summary_snapshot, durable_mrf_target_backlog_snapshot,
|
||||
mrf_backlog_observability_snapshot,
|
||||
};
|
||||
pub use crate::bucket::replication::{
|
||||
BucketReplicationResyncStatus, BucketStats, DeletedObjectReplicationInfo, DurableMrfBacklog, DynReplicationPool,
|
||||
@@ -183,13 +184,13 @@ pub mod bucket {
|
||||
ReplicationDeleteStateSource, ReplicationHealQueueResult, ReplicationObjectBridge, ReplicationObjectIO,
|
||||
ReplicationOperation, ReplicationPoolTrait, ReplicationPriority, ReplicationQueueAdmission, ReplicationScannerBridge,
|
||||
ReplicationState, ReplicationStats, ReplicationStatusType, ReplicationStorage, ReplicationTargetValidationError,
|
||||
ReplicationType, ResyncOpts, ResyncStatusType, TargetReplicationResyncStatus, VersionPurgeStatusType,
|
||||
delete_replication_state_from_config, delete_replication_version_id, get_global_replication_pool,
|
||||
get_global_replication_stats, init_background_replication, read_durable_mrf_backlog, replication_state_to_filemeta,
|
||||
replication_status_to_filemeta, replication_statuses_map, replication_target_arns, resync_start_conflict_id,
|
||||
should_remove_replication_target, should_schedule_delete_replication, should_use_existing_delete_replication_info,
|
||||
should_use_existing_delete_replication_source, unsupported_replication_config_field,
|
||||
validate_replication_config_target_arns, version_purge_status_to_filemeta,
|
||||
ReplicationType, ResyncOpts, ResyncStatusType, RuntimeReplicationTargetBacklog, TargetReplicationResyncStatus,
|
||||
VersionPurgeStatusType, delete_replication_state_from_config, delete_replication_version_id,
|
||||
get_global_replication_pool, get_global_replication_stats, init_background_replication, read_durable_mrf_backlog,
|
||||
replication_state_to_filemeta, replication_status_to_filemeta, replication_statuses_map, replication_target_arns,
|
||||
resync_start_conflict_id, should_remove_replication_target, should_schedule_delete_replication,
|
||||
should_use_existing_delete_replication_info, should_use_existing_delete_replication_source,
|
||||
unsupported_replication_config_field, validate_replication_config_target_arns, version_purge_status_to_filemeta,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use replication_queue_boundary::{
|
||||
};
|
||||
pub use replication_resync_boundary::{BucketReplicationResyncStatus, ResyncOpts, TargetReplicationResyncStatus};
|
||||
pub use replication_scanner_bridge::ReplicationScannerBridge;
|
||||
pub use replication_state::ReplicationStats;
|
||||
pub use replication_state::{ReplicationStats, RuntimeReplicationTargetBacklog};
|
||||
pub use replication_stats_boundary::BucketStats;
|
||||
pub use replication_storage_boundary::{ReplicationObjectIO, ReplicationStorage};
|
||||
pub(crate) use replication_target_config_bridge::ReplicationTargetConfigBridge;
|
||||
|
||||
@@ -86,12 +86,26 @@ pub struct DurableMrfBucketBacklog {
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DurableMrfTargetBacklog {
|
||||
pub bucket: String,
|
||||
pub target_arn: String,
|
||||
pub count: u64,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DurableMrfBacklogSummary {
|
||||
pub available: bool,
|
||||
pub buckets: Vec<DurableMrfBucketBacklog>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct DurableMrfBacklogSnapshot {
|
||||
summary: DurableMrfBacklogSummary,
|
||||
targets: Vec<DurableMrfTargetBacklog>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct MrfBucketBacklogObservability {
|
||||
pub bucket: String,
|
||||
@@ -110,6 +124,8 @@ pub struct MrfBacklogObservabilitySummary {
|
||||
|
||||
static DURABLE_MRF_BACKLOG_SUMMARY: LazyLock<StdRwLock<DurableMrfBacklogSummary>> =
|
||||
LazyLock::new(|| StdRwLock::new(DurableMrfBacklogSummary::default()));
|
||||
static DURABLE_MRF_TARGET_BACKLOG: LazyLock<StdRwLock<Vec<DurableMrfTargetBacklog>>> =
|
||||
LazyLock::new(|| StdRwLock::new(Vec::new()));
|
||||
static MRF_BACKLOG_OBSERVABILITY: LazyLock<StdRwLock<MrfBacklogObservabilityTracker>> =
|
||||
LazyLock::new(|| StdRwLock::new(MrfBacklogObservabilityTracker::default()));
|
||||
|
||||
@@ -117,13 +133,15 @@ static MRF_BACKLOG_OBSERVABILITY: LazyLock<StdRwLock<MrfBacklogObservabilityTrac
|
||||
struct DurableMrfBacklogTracker {
|
||||
available: bool,
|
||||
buckets: HashMap<String, DurableMrfBucketBacklog>,
|
||||
targets: HashMap<(String, String), DurableMrfTargetBacklog>,
|
||||
}
|
||||
|
||||
impl DurableMrfBacklogTracker {
|
||||
fn add_entry(&mut self, bucket_name: String, entry_size: i64) {
|
||||
let Ok(size) = u64::try_from(entry_size) else {
|
||||
fn add_entry(&mut self, entry: &MrfReplicateEntry) {
|
||||
let Ok(size) = u64::try_from(entry.size) else {
|
||||
self.available = false;
|
||||
self.buckets.clear();
|
||||
self.targets.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -131,6 +149,7 @@ impl DurableMrfBacklogTracker {
|
||||
return;
|
||||
}
|
||||
|
||||
let bucket_name = entry.bucket.clone();
|
||||
let bucket = match self.buckets.entry(bucket_name) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
@@ -143,16 +162,39 @@ impl DurableMrfBacklogTracker {
|
||||
};
|
||||
bucket.count = bucket.count.saturating_add(1);
|
||||
bucket.bytes = bucket.bytes.saturating_add(size);
|
||||
|
||||
for target_arn in &entry.target_arns {
|
||||
if target_arn.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let key = (entry.bucket.clone(), target_arn.clone());
|
||||
let target = match self.targets.entry(key) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
let (bucket, target_arn) = entry.key().clone();
|
||||
entry.insert(DurableMrfTargetBacklog {
|
||||
bucket,
|
||||
target_arn,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
};
|
||||
target.count = target.count.saturating_add(1);
|
||||
target.bytes = target.bytes.saturating_add(size);
|
||||
}
|
||||
}
|
||||
|
||||
fn into_summary(self) -> DurableMrfBacklogSummary {
|
||||
fn into_snapshot(self) -> DurableMrfBacklogSnapshot {
|
||||
if !self.available {
|
||||
return DurableMrfBacklogSummary::default();
|
||||
return DurableMrfBacklogSnapshot::default();
|
||||
}
|
||||
|
||||
DurableMrfBacklogSummary {
|
||||
available: true,
|
||||
buckets: self.buckets.into_values().collect(),
|
||||
DurableMrfBacklogSnapshot {
|
||||
summary: DurableMrfBacklogSummary {
|
||||
available: true,
|
||||
buckets: self.buckets.into_values().collect(),
|
||||
},
|
||||
targets: self.targets.into_values().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +260,21 @@ impl MrfBacklogObservabilityTracker {
|
||||
}
|
||||
}
|
||||
|
||||
fn durable_mrf_backlog_summary_from_sizes<I>(entries: I) -> DurableMrfBacklogSummary
|
||||
fn durable_mrf_backlog_summary_from_entries<'a>(
|
||||
entries: impl IntoIterator<Item = &'a MrfReplicateEntry>,
|
||||
) -> DurableMrfBacklogSnapshot {
|
||||
let mut tracker = DurableMrfBacklogTracker {
|
||||
available: true,
|
||||
..Default::default()
|
||||
};
|
||||
for entry in entries {
|
||||
tracker.add_entry(entry);
|
||||
}
|
||||
tracker.into_snapshot()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn durable_mrf_backlog_summary_from_sizes<I>(entries: I) -> DurableMrfBacklogSnapshot
|
||||
where
|
||||
I: IntoIterator<Item = (String, i64)>,
|
||||
{
|
||||
@@ -227,16 +283,38 @@ where
|
||||
..Default::default()
|
||||
};
|
||||
for (bucket_name, entry_size) in entries {
|
||||
tracker.add_entry(bucket_name, entry_size);
|
||||
tracker.add_entry(&MrfReplicateEntry {
|
||||
bucket: bucket_name,
|
||||
object: String::new(),
|
||||
version_id: None,
|
||||
retry_count: 0,
|
||||
size: entry_size,
|
||||
op: MrfOpKind::Object,
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
});
|
||||
}
|
||||
tracker.into_snapshot()
|
||||
}
|
||||
|
||||
fn set_durable_mrf_backlog_snapshot(snapshot: DurableMrfBacklogSnapshot) {
|
||||
match DURABLE_MRF_BACKLOG_SUMMARY.write() {
|
||||
Ok(mut guard) => *guard = snapshot.summary,
|
||||
Err(poisoned) => *poisoned.into_inner() = snapshot.summary,
|
||||
}
|
||||
match DURABLE_MRF_TARGET_BACKLOG.write() {
|
||||
Ok(mut guard) => *guard = snapshot.targets,
|
||||
Err(poisoned) => *poisoned.into_inner() = snapshot.targets,
|
||||
}
|
||||
tracker.into_summary()
|
||||
}
|
||||
|
||||
fn set_durable_mrf_backlog_summary(summary: DurableMrfBacklogSummary) {
|
||||
match DURABLE_MRF_BACKLOG_SUMMARY.write() {
|
||||
Ok(mut guard) => *guard = summary,
|
||||
Err(poisoned) => *poisoned.into_inner() = summary,
|
||||
}
|
||||
set_durable_mrf_backlog_snapshot(DurableMrfBacklogSnapshot {
|
||||
summary,
|
||||
targets: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn durable_mrf_backlog_summary_snapshot() -> DurableMrfBacklogSummary {
|
||||
@@ -246,6 +324,13 @@ pub fn durable_mrf_backlog_summary_snapshot() -> DurableMrfBacklogSummary {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn durable_mrf_target_backlog_snapshot() -> Vec<DurableMrfTargetBacklog> {
|
||||
match DURABLE_MRF_TARGET_BACKLOG.read() {
|
||||
Ok(guard) => guard.clone(),
|
||||
Err(poisoned) => poisoned.into_inner().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mrf_backlog_observability_snapshot() -> MrfBacklogObservabilitySummary {
|
||||
match MRF_BACKLOG_OBSERVABILITY.read() {
|
||||
Ok(guard) => guard.snapshot(),
|
||||
@@ -675,6 +760,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
|
||||
/// Queues a replica task
|
||||
pub async fn queue_replica_task(&self, ri: ReplicateObjectInfo) -> ReplicationQueueAdmission {
|
||||
let target_arns = ri.dsc.replicate_target_arns();
|
||||
// If object is large, queue it to a static set of large workers
|
||||
if should_queue_large_object(ri.size) {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -691,10 +777,12 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
|
||||
if let Some(worker) = lrg_workers.get(index) {
|
||||
self.stats.inc_q(&ri.bucket, ri.size, ri.delete_marker, ri.op_type);
|
||||
self.stats.inc_target_q(&ri.bucket, &target_arns, ri.size);
|
||||
if worker.try_send(ReplicationOperation::Object(Box::new(ri.clone()))).is_ok() {
|
||||
return ReplicationQueueAdmission::Queued;
|
||||
}
|
||||
self.stats.dec_q(&ri.bucket, ri.size, ri.delete_marker, ri.op_type);
|
||||
self.stats.dec_target_q(&ri.bucket, &target_arns, ri.size);
|
||||
|
||||
// Try to add more workers if possible
|
||||
let max_l_workers = *self.max_l_workers.read().await;
|
||||
@@ -723,10 +811,12 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
};
|
||||
|
||||
self.stats.inc_q(&ri.bucket, ri.size, ri.delete_marker, ri.op_type);
|
||||
self.stats.inc_target_q(&ri.bucket, &target_arns, ri.size);
|
||||
if channel.try_send(ReplicationOperation::Object(Box::new(ri.clone()))).is_ok() {
|
||||
return ReplicationQueueAdmission::Queued;
|
||||
}
|
||||
self.stats.dec_q(&ri.bucket, ri.size, ri.delete_marker, ri.op_type);
|
||||
self.stats.dec_target_q(&ri.bucket, &target_arns, ri.size);
|
||||
|
||||
// Queue to MRF if all workers are busy.
|
||||
let admission = self.queue_mrf_save_admission(ri.to_mrf_entry(), "object").await;
|
||||
@@ -740,6 +830,11 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
|
||||
/// Queues a replica delete task
|
||||
pub async fn queue_replica_delete_task(&self, doi: DeletedObjectReplicationInfo) -> ReplicationQueueAdmission {
|
||||
let target_arns = if doi.target_arn.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![doi.target_arn.clone()]
|
||||
};
|
||||
let ch = self
|
||||
.worker_queue_channel(&doi.op_type, &doi.bucket, &doi.delete_object.object_name, 0)
|
||||
.await;
|
||||
@@ -749,10 +844,12 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
};
|
||||
|
||||
self.stats.inc_q(&doi.bucket, 0, true, doi.op_type);
|
||||
self.stats.inc_target_q(&doi.bucket, &target_arns, 0);
|
||||
if channel.try_send(ReplicationOperation::Delete(Box::new(doi.clone()))).is_ok() {
|
||||
return ReplicationQueueAdmission::Queued;
|
||||
}
|
||||
self.stats.dec_q(&doi.bucket, 0, true, doi.op_type);
|
||||
self.stats.dec_target_q(&doi.bucket, &target_arns, 0);
|
||||
|
||||
let admission = self.queue_mrf_save_admission(doi.to_mrf_entry(), "delete").await;
|
||||
|
||||
@@ -771,9 +868,11 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
let bucket = entry.bucket.clone();
|
||||
let size = entry.size;
|
||||
let is_delete = matches!(entry.op, MrfOpKind::Delete);
|
||||
let target_arns = entry.target_arns.clone();
|
||||
let admission = queue_mrf_save_entry(&self.mrf_save_tx, entry, queue_type).await;
|
||||
if admission == ReplicationQueueAdmission::Queued {
|
||||
self.stats.inc_q(&bucket, size, is_delete, ReplicationType::Heal);
|
||||
self.stats.inc_target_q(&bucket, &target_arns, size);
|
||||
}
|
||||
admission
|
||||
}
|
||||
@@ -827,9 +926,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
set_durable_mrf_backlog_summary(durable_mrf_backlog_summary_from_sizes(
|
||||
entries.iter().map(|entry| (entry.bucket.clone(), entry.size)),
|
||||
));
|
||||
set_durable_mrf_backlog_snapshot(durable_mrf_backlog_summary_from_entries(&entries));
|
||||
|
||||
let total = entries.len();
|
||||
let mut queued_count = 0usize;
|
||||
@@ -1018,7 +1115,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
durable_tracker.add_entry(e.bucket.clone(), e.size);
|
||||
durable_tracker.add_entry(&e);
|
||||
observe_mrf_pending(&e);
|
||||
pending.push(e);
|
||||
dirty = true;
|
||||
@@ -1029,7 +1126,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
if pending.len() - flushed_len >= 1000
|
||||
&& let Some(duration_millis) = flush_mrf_to_disk(&pending, &storage).await
|
||||
{
|
||||
set_durable_mrf_backlog_summary(durable_tracker.clone().into_summary());
|
||||
set_durable_mrf_backlog_snapshot(durable_tracker.clone().into_snapshot());
|
||||
observe_mrf_pending_flushed(&pending[flushed_len..], duration_millis);
|
||||
dec_mrf_entries(stats.as_ref(), &pending[flushed_len..]);
|
||||
flushed_len = pending.len();
|
||||
@@ -1039,7 +1136,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
None => {
|
||||
// Channel closed (pool shutting down) — final flush.
|
||||
if dirty && let Some(duration_millis) = flush_mrf_to_disk(&pending, &storage).await {
|
||||
set_durable_mrf_backlog_summary(durable_tracker.clone().into_summary());
|
||||
set_durable_mrf_backlog_snapshot(durable_tracker.clone().into_snapshot());
|
||||
observe_mrf_pending_flushed(&pending[flushed_len..], duration_millis);
|
||||
dec_mrf_entries(stats.as_ref(), &pending[flushed_len..]);
|
||||
}
|
||||
@@ -1048,7 +1145,7 @@ impl<S: ReplicationStorage> ReplicationPool<S> {
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
if dirty && let Some(duration_millis) = flush_mrf_to_disk(&pending, &storage).await {
|
||||
set_durable_mrf_backlog_summary(durable_tracker.clone().into_summary());
|
||||
set_durable_mrf_backlog_snapshot(durable_tracker.clone().into_snapshot());
|
||||
observe_mrf_pending_flushed(&pending[flushed_len..], duration_millis);
|
||||
dec_mrf_entries(stats.as_ref(), &pending[flushed_len..]);
|
||||
flushed_len = pending.len();
|
||||
@@ -1451,6 +1548,7 @@ struct ReplicationBacklogGuard {
|
||||
size: i64,
|
||||
is_delete_marker: bool,
|
||||
op_type: ReplicationType,
|
||||
target_arns: Vec<String>,
|
||||
}
|
||||
|
||||
impl ReplicationBacklogGuard {
|
||||
@@ -1461,16 +1559,23 @@ impl ReplicationBacklogGuard {
|
||||
size: object.size,
|
||||
is_delete_marker: object.delete_marker,
|
||||
op_type: object.op_type,
|
||||
target_arns: object.dsc.replicate_target_arns(),
|
||||
}
|
||||
}
|
||||
|
||||
fn for_delete(stats: Arc<ReplicationStats>, delete: &DeletedObjectReplicationInfo) -> Self {
|
||||
let target_arns = if delete.target_arn.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![delete.target_arn.clone()]
|
||||
};
|
||||
Self {
|
||||
stats,
|
||||
bucket: delete.bucket.clone(),
|
||||
size: 0,
|
||||
is_delete_marker: true,
|
||||
op_type: delete.op_type,
|
||||
target_arns,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1478,6 +1583,7 @@ impl ReplicationBacklogGuard {
|
||||
impl Drop for ReplicationBacklogGuard {
|
||||
fn drop(&mut self) {
|
||||
self.stats.dec_q(&self.bucket, self.size, self.is_delete_marker, self.op_type);
|
||||
self.stats.dec_target_q(&self.bucket, &self.target_arns, self.size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1524,6 +1630,7 @@ async fn queue_mrf_save_entry(
|
||||
fn dec_mrf_entries(stats: &ReplicationStats, entries: &[MrfReplicateEntry]) {
|
||||
for entry in entries {
|
||||
stats.dec_q(&entry.bucket, entry.size, matches!(entry.op, MrfOpKind::Delete), ReplicationType::Heal);
|
||||
stats.dec_target_q(&entry.bucket, &entry.target_arns, entry.size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1915,6 +2022,7 @@ async fn queue_replicate_deletes(batch: ReplicationHealResyncDeletes) -> Replica
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::replication_filemeta_boundary::ReplicateTargetDecision;
|
||||
use super::super::replication_resync_boundary::{decode_mrf_file, encode_mrf_file, encode_resync_file};
|
||||
use super::super::replication_storage_boundary::{
|
||||
DeletedObject, FileInfo, GetObjectReader, HTTPRangeSpec, ListOperations, ObjectIO, ObjectOperations, PutObjReader,
|
||||
@@ -2260,6 +2368,22 @@ mod tests {
|
||||
(stats.replication_stats.q_stat.curr.count, stats.replication_stats.q_stat.curr.bytes)
|
||||
}
|
||||
|
||||
fn current_target_queue(pool: &ReplicationPool<LoadResyncNodeStore>, bucket: &str, target_arn: &str) -> Option<(u64, u64)> {
|
||||
pool.stats
|
||||
.runtime_target_backlog_snapshot()
|
||||
.into_iter()
|
||||
.find(|target| target.bucket == bucket && target.target_arn == target_arn)
|
||||
.map(|target| (target.count, target.bytes))
|
||||
}
|
||||
|
||||
fn test_replicate_decision(target_arns: &[&str]) -> ReplicateDecision {
|
||||
let mut decision = ReplicateDecision::default();
|
||||
for target_arn in target_arns {
|
||||
decision.set(ReplicateTargetDecision::new((*target_arn).to_string(), true, false));
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
async fn wait_for_current_queue(pool: &ReplicationPool<LoadResyncNodeStore>, bucket: &str, expected: (i64, i64)) {
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
@@ -2293,6 +2417,35 @@ mod tests {
|
||||
assert_eq!(current_queue(&pool, "admission-bucket").await, (1, 4096));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_worker_admission_counts_target_backlog_before_receive() {
|
||||
let pool = new_test_replication_pool(Arc::new(LoadResyncNodeStore::new("node-a", empty_resync_shared_state()))).await;
|
||||
let (tx, _rx) = mpsc::channel(1);
|
||||
pool.workers.write().await.push(tx);
|
||||
|
||||
let admission = pool
|
||||
.queue_replica_task(ReplicateObjectInfo {
|
||||
bucket: "target-admission-bucket".to_string(),
|
||||
name: "object".to_string(),
|
||||
size: 4096,
|
||||
op_type: ReplicationType::Object,
|
||||
dsc: test_replicate_decision(&["arn:rustfs:replication:target-b", "arn:rustfs:replication:target-a"]),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(admission, ReplicationQueueAdmission::Queued);
|
||||
assert_eq!(current_queue(&pool, "target-admission-bucket").await, (1, 4096));
|
||||
assert_eq!(
|
||||
current_target_queue(&pool, "target-admission-bucket", "arn:rustfs:replication:target-a"),
|
||||
Some((1, 4096))
|
||||
);
|
||||
assert_eq!(
|
||||
current_target_queue(&pool, "target-admission-bucket", "arn:rustfs:replication:target-b"),
|
||||
Some((1, 4096))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn large_worker_admission_counts_channel_backlog_before_receive() {
|
||||
let pool = new_test_replication_pool(Arc::new(LoadResyncNodeStore::new("node-a", empty_resync_shared_state()))).await;
|
||||
@@ -2336,6 +2489,33 @@ mod tests {
|
||||
assert_eq!(current_queue(&pool, "delete-admission-bucket").await, (1, 0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_admission_counts_target_backlog_before_receive() {
|
||||
let pool = new_test_replication_pool(Arc::new(LoadResyncNodeStore::new("node-a", empty_resync_shared_state()))).await;
|
||||
let (tx, _rx) = mpsc::channel(1);
|
||||
pool.workers.write().await.push(tx);
|
||||
|
||||
let admission = pool
|
||||
.queue_replica_delete_task(DeletedObjectReplicationInfo {
|
||||
bucket: "delete-target-admission-bucket".to_string(),
|
||||
target_arn: "arn:rustfs:replication:target-a".to_string(),
|
||||
delete_object: ReplicationDeletedObject {
|
||||
object_name: "deleted-object".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
op_type: ReplicationType::Delete,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(admission, ReplicationQueueAdmission::Queued);
|
||||
assert_eq!(current_queue(&pool, "delete-target-admission-bucket").await, (1, 0));
|
||||
assert_eq!(
|
||||
current_target_queue(&pool, "delete-target-admission-bucket", "arn:rustfs:replication:target-a"),
|
||||
Some((1, 0))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_worker_drains_current_backlog_after_processing() {
|
||||
let pool = new_test_replication_pool(Arc::new(LoadResyncNodeStore::new("node-a", empty_resync_shared_state()))).await;
|
||||
@@ -2702,6 +2882,7 @@ mod tests {
|
||||
name: "fallback-object".to_string(),
|
||||
size: 2048,
|
||||
op_type: ReplicationType::Object,
|
||||
dsc: test_replicate_decision(&["arn:rustfs:replication:target-a"]),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
@@ -2710,6 +2891,10 @@ mod tests {
|
||||
let queued = pool.stats.get_latest_replication_stats("runtime-backlog").await;
|
||||
assert_eq!(queued.replication_stats.q_stat.curr.count, 1);
|
||||
assert_eq!(queued.replication_stats.q_stat.curr.bytes, 2048);
|
||||
assert_eq!(
|
||||
current_target_queue(&pool, "runtime-backlog", "arn:rustfs:replication:target-a"),
|
||||
Some((1, 2048))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2745,6 +2930,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
let second = MrfReplicateEntry {
|
||||
object: "second".to_string(),
|
||||
@@ -2794,6 +2980,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
},
|
||||
"test",
|
||||
)
|
||||
@@ -2824,6 +3011,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
observe_mrf_pending(&entry);
|
||||
|
||||
@@ -2845,12 +3033,14 @@ mod tests {
|
||||
async fn replication_backlog_guard_decrements_on_drop() {
|
||||
let stats = Arc::new(ReplicationStats::new());
|
||||
stats.inc_q("guard-bucket", 256, false, ReplicationType::Object);
|
||||
stats.inc_target_q("guard-bucket", &["arn:rustfs:replication:target-a".to_string()], 256);
|
||||
|
||||
{
|
||||
let object = ReplicateObjectInfo {
|
||||
bucket: "guard-bucket".to_string(),
|
||||
size: 256,
|
||||
op_type: ReplicationType::Object,
|
||||
dsc: test_replicate_decision(&["arn:rustfs:replication:target-a"]),
|
||||
..Default::default()
|
||||
};
|
||||
let _guard = ReplicationBacklogGuard::for_object(stats.clone(), &object);
|
||||
@@ -2859,6 +3049,30 @@ mod tests {
|
||||
let queued = stats.get_latest_replication_stats("guard-bucket").await;
|
||||
assert_eq!(queued.replication_stats.q_stat.curr.count, 0);
|
||||
assert_eq!(queued.replication_stats.q_stat.curr.bytes, 0);
|
||||
assert!(stats.runtime_target_backlog_snapshot().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dec_mrf_entries_decrements_target_backlog() {
|
||||
let stats = ReplicationStats::new();
|
||||
let entry = MrfReplicateEntry {
|
||||
bucket: "mrf-target-drain-bucket".to_string(),
|
||||
object: "object".to_string(),
|
||||
version_id: None,
|
||||
retry_count: 1,
|
||||
size: 1024,
|
||||
op: MrfOpKind::Object,
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: vec!["arn:rustfs:replication:target-a".to_string()],
|
||||
};
|
||||
|
||||
stats.inc_q(&entry.bucket, entry.size, false, ReplicationType::Heal);
|
||||
stats.inc_target_q(&entry.bucket, &entry.target_arns, entry.size);
|
||||
dec_mrf_entries(&stats, std::slice::from_ref(&entry));
|
||||
|
||||
assert!(stats.runtime_target_backlog_snapshot().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2873,6 +3087,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
let second = MrfReplicateEntry {
|
||||
object: "second".to_string(),
|
||||
@@ -2984,6 +3199,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
|
||||
let encoded = encode_mrf_file(std::slice::from_ref(&entry)).expect("encode");
|
||||
@@ -3017,6 +3233,7 @@ mod tests {
|
||||
delete_marker_version_id: Some(dm_vid),
|
||||
delete_marker: true,
|
||||
delete_marker_mtime: Some(mtime_nanos),
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
|
||||
let encoded = encode_mrf_file(std::slice::from_ref(&entry)).expect("encode");
|
||||
@@ -3050,6 +3267,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
|
||||
let encoded = encode_mrf_file(&[entry]).expect("encode");
|
||||
@@ -3078,6 +3296,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
},
|
||||
MrfReplicateEntry {
|
||||
bucket: "b".to_string(),
|
||||
@@ -3089,6 +3308,7 @@ mod tests {
|
||||
delete_marker_version_id: Some(del_dm_vid),
|
||||
delete_marker: true,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3118,6 +3338,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
assert_eq!(obj_entry.op, MrfOpKind::Object);
|
||||
|
||||
@@ -3132,6 +3353,7 @@ mod tests {
|
||||
delete_marker_version_id: Some(Uuid::new_v4()),
|
||||
delete_marker: true,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
assert_eq!(del_entry.op, MrfOpKind::Delete);
|
||||
|
||||
@@ -3147,6 +3369,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
};
|
||||
assert_eq!(legacy_entry.op, MrfOpKind::Object, "legacy default must be Object");
|
||||
}
|
||||
@@ -3196,6 +3419,7 @@ mod tests {
|
||||
// The "deleteMarkerMtime" key was absent in old files — #[serde(default)] must fill in
|
||||
// None so replay falls back to the current time (backlog#867 backward compatibility).
|
||||
assert_eq!(entry.delete_marker_mtime, None, "missing deleteMarkerMtime key must default to None");
|
||||
assert!(entry.target_arns.is_empty(), "old MRF entries must not be attributed to a target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3210,6 +3434,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
}];
|
||||
let encoded = encode_mrf_file(&entries).expect("durable MRF backlog should encode");
|
||||
|
||||
@@ -3226,9 +3451,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn durable_mrf_summary_aggregates_entries_by_bucket_for_obs() {
|
||||
let summary =
|
||||
let snapshot =
|
||||
durable_mrf_backlog_summary_from_sizes([("b1".to_string(), 1024), ("b1".to_string(), 512), ("b2".to_string(), 0)]);
|
||||
|
||||
let summary = snapshot.summary;
|
||||
assert!(summary.available);
|
||||
let buckets = summary
|
||||
.buckets
|
||||
@@ -3239,13 +3465,82 @@ mod tests {
|
||||
assert_eq!(buckets["b1"].bytes, 1536);
|
||||
assert_eq!(buckets["b2"].count, 1);
|
||||
assert_eq!(buckets["b2"].bytes, 0);
|
||||
assert!(snapshot.targets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn durable_mrf_summary_aggregates_target_backlog_without_attributing_legacy_entries() {
|
||||
let entries = vec![
|
||||
MrfReplicateEntry {
|
||||
bucket: "b1".to_string(),
|
||||
object: "object-a".to_string(),
|
||||
version_id: None,
|
||||
retry_count: 0,
|
||||
size: 1024,
|
||||
op: MrfOpKind::Object,
|
||||
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: "b1".to_string(),
|
||||
object: "object-b".to_string(),
|
||||
version_id: None,
|
||||
retry_count: 0,
|
||||
size: 512,
|
||||
op: MrfOpKind::Object,
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: vec!["arn:target-a".to_string()],
|
||||
},
|
||||
MrfReplicateEntry {
|
||||
bucket: "b1".to_string(),
|
||||
object: "legacy-object".to_string(),
|
||||
version_id: None,
|
||||
retry_count: 0,
|
||||
size: 256,
|
||||
op: MrfOpKind::Object,
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
},
|
||||
];
|
||||
|
||||
let snapshot = durable_mrf_backlog_summary_from_entries(&entries);
|
||||
|
||||
let summary = snapshot.summary;
|
||||
assert!(summary.available);
|
||||
let buckets = summary
|
||||
.buckets
|
||||
.into_iter()
|
||||
.map(|bucket| (bucket.bucket.clone(), bucket))
|
||||
.collect::<HashMap<_, _>>();
|
||||
assert_eq!(buckets["b1"].count, 3);
|
||||
assert_eq!(buckets["b1"].bytes, 1792);
|
||||
|
||||
let targets = snapshot
|
||||
.targets
|
||||
.into_iter()
|
||||
.map(|target| ((target.bucket.clone(), target.target_arn.clone()), target))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let target_a = &targets[&("b1".to_string(), "arn:target-a".to_string())];
|
||||
assert_eq!(target_a.count, 2);
|
||||
assert_eq!(target_a.bytes, 1536);
|
||||
let target_b = &targets[&("b1".to_string(), "arn:target-b".to_string())];
|
||||
assert_eq!(target_b.count, 1);
|
||||
assert_eq!(target_b.bytes, 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn durable_mrf_summary_marks_invalid_sizes_unavailable() {
|
||||
let invalid = durable_mrf_backlog_summary_from_sizes([("bucket".to_string(), -1)]);
|
||||
assert!(!invalid.available);
|
||||
assert!(invalid.buckets.is_empty());
|
||||
let summary = invalid.summary;
|
||||
assert!(!summary.available);
|
||||
assert!(summary.buckets.is_empty());
|
||||
assert!(invalid.targets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3264,6 +3559,7 @@ mod tests {
|
||||
delete_marker_version_id: None,
|
||||
delete_marker: false,
|
||||
delete_marker_mtime: None,
|
||||
target_arns: Vec::new(),
|
||||
}])
|
||||
.expect("invalid persisted entry should still encode for boundary testing");
|
||||
let invalid = durable_mrf_backlog_from_read(Ok(negative));
|
||||
|
||||
@@ -22,9 +22,9 @@ use super::replication_stats_boundary::{
|
||||
QueueCache, ReplicationMetricScope, SRMetricsSummary, XferStats,
|
||||
};
|
||||
use super::runtime_boundary as runtime_sources;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, hash_map::Entry};
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::{Arc, Mutex as StdMutex};
|
||||
use std::sync::{Arc, LazyLock, Mutex as StdMutex, Weak};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::time::interval;
|
||||
@@ -153,6 +153,83 @@ pub struct ReplicationStats {
|
||||
pub most_recent_stats: Arc<Mutex<HashMap<String, BucketStats>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct RuntimeReplicationTargetBacklog {
|
||||
pub bucket: String,
|
||||
pub target_arn: String,
|
||||
pub count: u64,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
type TargetQueueKey = (String, String);
|
||||
type TargetQueueCache = HashMap<TargetQueueKey, InQueueMetric>;
|
||||
|
||||
struct TargetQueueCacheSlot {
|
||||
owner: Weak<StdMutex<QueueCache>>,
|
||||
metrics: TargetQueueCache,
|
||||
}
|
||||
|
||||
impl TargetQueueCacheSlot {
|
||||
fn new(owner: &Arc<StdMutex<QueueCache>>) -> Self {
|
||||
Self {
|
||||
owner: Arc::downgrade(owner),
|
||||
metrics: TargetQueueCache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn belongs_to(&self, owner: &Arc<StdMutex<QueueCache>>) -> bool {
|
||||
self.owner.upgrade().is_some_and(|current| Arc::ptr_eq(¤t, owner))
|
||||
}
|
||||
}
|
||||
|
||||
// Keep runtime target counters outside ReplicationStats to preserve its public struct shape.
|
||||
static TARGET_QUEUE_CACHES: LazyLock<StdMutex<Vec<TargetQueueCacheSlot>>> = LazyLock::new(|| StdMutex::new(Vec::new()));
|
||||
|
||||
fn i64_to_u64_floor_zero(value: i64) -> u64 {
|
||||
u64::try_from(value.max(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn normalized_target_arns(target_arns: &[String]) -> Vec<&str> {
|
||||
let mut target_arns = target_arns
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.filter(|target_arn| !target_arn.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
target_arns.sort_unstable();
|
||||
target_arns.dedup();
|
||||
target_arns
|
||||
}
|
||||
|
||||
fn target_queue_cache_snapshot(cache: &TargetQueueCache) -> Vec<RuntimeReplicationTargetBacklog> {
|
||||
cache
|
||||
.iter()
|
||||
.filter_map(|((bucket, target_arn), metric)| {
|
||||
let count = i64_to_u64_floor_zero(metric.curr.get_current_count());
|
||||
let bytes = i64_to_u64_floor_zero(metric.curr.get_current_bytes());
|
||||
(count > 0 || bytes > 0).then(|| RuntimeReplicationTargetBacklog {
|
||||
bucket: bucket.clone(),
|
||||
target_arn: target_arn.clone(),
|
||||
count,
|
||||
bytes,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prune_stale_target_queue_caches(caches: &mut Vec<TargetQueueCacheSlot>) {
|
||||
caches.retain(|slot| slot.owner.strong_count() > 0);
|
||||
}
|
||||
|
||||
fn with_target_queue_caches<T>(f: impl FnOnce(&mut Vec<TargetQueueCacheSlot>) -> T) -> T {
|
||||
match TARGET_QUEUE_CACHES.lock() {
|
||||
Ok(mut caches) => f(&mut caches),
|
||||
Err(poisoned) => {
|
||||
let mut caches = poisoned.into_inner();
|
||||
f(&mut caches)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplicationStats {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -684,6 +761,68 @@ impl ReplicationStats {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inc_target_q(&self, bucket: &str, target_arns: &[String], size: i64) {
|
||||
let target_arns = normalized_target_arns(target_arns);
|
||||
if target_arns.is_empty() {
|
||||
return;
|
||||
}
|
||||
with_target_queue_caches(|caches| {
|
||||
prune_stale_target_queue_caches(caches);
|
||||
let slot_index = match caches.iter().position(|slot| slot.belongs_to(&self.q_cache)) {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
caches.push(TargetQueueCacheSlot::new(&self.q_cache));
|
||||
caches.len() - 1
|
||||
}
|
||||
};
|
||||
let slot = &mut caches[slot_index];
|
||||
let bucket = bucket.to_string();
|
||||
for target_arn in target_arns {
|
||||
let metric = match slot.metrics.entry((bucket.clone(), target_arn.to_string())) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => entry.insert(InQueueMetric::default()),
|
||||
};
|
||||
metric.curr.add_current(size, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn dec_target_q(&self, bucket: &str, target_arns: &[String], size: i64) {
|
||||
let target_arns = normalized_target_arns(target_arns);
|
||||
if target_arns.is_empty() {
|
||||
return;
|
||||
}
|
||||
with_target_queue_caches(|caches| {
|
||||
prune_stale_target_queue_caches(caches);
|
||||
if let Some(slot) = caches.iter_mut().find(|slot| slot.belongs_to(&self.q_cache)) {
|
||||
let bucket = bucket.to_string();
|
||||
for target_arn in target_arns {
|
||||
if let Some(metric) = slot.metrics.get_mut(&(bucket.clone(), target_arn.to_string())) {
|
||||
metric.curr.subtract_current(size, 1);
|
||||
}
|
||||
}
|
||||
slot.metrics
|
||||
.retain(|_, metric| metric.curr.get_current_count() > 0 || metric.curr.get_current_bytes() > 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn runtime_target_backlog_snapshot(&self) -> Vec<RuntimeReplicationTargetBacklog> {
|
||||
let mut snapshot = with_target_queue_caches(|caches| {
|
||||
caches
|
||||
.iter()
|
||||
.find(|slot| slot.belongs_to(&self.q_cache))
|
||||
.map(|slot| target_queue_cache_snapshot(&slot.metrics))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
snapshot.sort_by(|left, right| {
|
||||
left.bucket
|
||||
.cmp(&right.bucket)
|
||||
.then_with(|| left.target_arn.cmp(&right.target_arn))
|
||||
});
|
||||
snapshot
|
||||
}
|
||||
|
||||
/// Increase proxy metrics
|
||||
pub async fn inc_proxy(&self, bucket: &str, api: &str, is_err: bool) {
|
||||
let mut p_cache = self.p_cache.lock().await;
|
||||
@@ -707,6 +846,94 @@ impl Default for ReplicationStats {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_target_backlog_snapshot_tracks_targets() {
|
||||
let stats = ReplicationStats::new();
|
||||
stats.inc_target_q(
|
||||
"photos",
|
||||
&[
|
||||
"arn:rustfs:replication:target-b".to_string(),
|
||||
"arn:rustfs:replication:target-a".to_string(),
|
||||
"arn:rustfs:replication:target-a".to_string(),
|
||||
],
|
||||
1024,
|
||||
);
|
||||
|
||||
let snapshot = stats.runtime_target_backlog_snapshot();
|
||||
|
||||
assert_eq!(
|
||||
snapshot,
|
||||
vec![
|
||||
RuntimeReplicationTargetBacklog {
|
||||
bucket: "photos".to_string(),
|
||||
target_arn: "arn:rustfs:replication:target-a".to_string(),
|
||||
count: 1,
|
||||
bytes: 1024,
|
||||
},
|
||||
RuntimeReplicationTargetBacklog {
|
||||
bucket: "photos".to_string(),
|
||||
target_arn: "arn:rustfs:replication:target-b".to_string(),
|
||||
count: 1,
|
||||
bytes: 1024,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_target_backlog_ignores_empty_targets() {
|
||||
let stats = ReplicationStats::new();
|
||||
stats.inc_target_q("photos", &["".to_string()], 1024);
|
||||
|
||||
assert!(stats.runtime_target_backlog_snapshot().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_target_backlog_is_scoped_to_stats_instance() {
|
||||
let first = ReplicationStats::new();
|
||||
let second = ReplicationStats::new();
|
||||
first.inc_target_q("photos", &["arn:rustfs:replication:target-a".to_string()], 1024);
|
||||
|
||||
assert!(second.runtime_target_backlog_snapshot().is_empty());
|
||||
assert_eq!(first.runtime_target_backlog_snapshot()[0].count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_target_backlog_decrements_with_saturation() {
|
||||
let stats = ReplicationStats::new();
|
||||
let target_arns = ["arn:rustfs:replication:target-a".to_string()];
|
||||
stats.inc_target_q("photos", &target_arns, 1024);
|
||||
stats.dec_target_q("photos", &target_arns, 2048);
|
||||
|
||||
assert!(stats.runtime_target_backlog_snapshot().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_target_backlog_prunes_stale_sidecar_on_next_access() {
|
||||
{
|
||||
let stats = ReplicationStats::new();
|
||||
stats.inc_target_q("photos", &["arn:rustfs:replication:target-a".to_string()], 1024);
|
||||
assert!(
|
||||
TARGET_QUEUE_CACHES
|
||||
.lock()
|
||||
.expect("target queue cache mutex")
|
||||
.iter()
|
||||
.any(|slot| slot.owner.strong_count() > 0)
|
||||
);
|
||||
}
|
||||
|
||||
let stats = ReplicationStats::new();
|
||||
stats.inc_target_q("photos", &["arn:rustfs:replication:target-b".to_string()], 1024);
|
||||
|
||||
assert!(
|
||||
TARGET_QUEUE_CACHES
|
||||
.lock()
|
||||
.expect("target queue cache mutex")
|
||||
.iter()
|
||||
.all(|slot| slot.owner.strong_count() > 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_replication_stats_new() {
|
||||
let stats = ReplicationStats::new();
|
||||
|
||||
Reference in New Issue
Block a user