From 34b4cfb466998948063b236b82d84dc8dd2d252d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Sat, 15 Aug 2026 08:41:15 +0800 Subject: [PATCH] test(admin): pin minio-go Metrics/MetricsV2 wire contract for replication metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red-light evidence for backlog#1675 P1-11: ?replication-metrics[=2] serializes the internal snake_case BucketStats family straight onto the wire, while minio-go's replication.Metrics/MetricsV2 expect camelCase tags (currStats/queueStats/replicaCount/queued/...). Go's decoder is case-insensitive but does not ignore underscores, so 'mc replicate status' shows all zeros without any error. The rewritten snapshot tests assert the minio-go tags (plus a synthesized queueStats node — the aggregation path leaves queue_stats.nodes empty today) and fail against the current pass-through serialization. --- rustfs/src/admin/router.rs | 51 ++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/rustfs/src/admin/router.rs b/rustfs/src/admin/router.rs index 1f3ac8856..70a3b93ab 100644 --- a/rustfs/src/admin/router.rs +++ b/rustfs/src/admin/router.rs @@ -4147,22 +4147,37 @@ mod tests { assert!(err.message().unwrap_or_default().contains("rule-stale")); } + /// The v1 body must decode into minio-go `replication.Metrics` (exact + /// json tags); Go's decoder matches case-insensitively but does not + /// ignore underscores, so the internal snake_case names read as all-zero. #[test] - fn serialize_replication_metrics_body_v1_returns_replication_stats_only() { + fn serialize_replication_metrics_body_v1_returns_minio_go_metrics_shape() { let mut stats = BucketStats { uptime: 99, ..Default::default() }; stats.replication_stats.replica_count = 7; + stats.replication_stats.replicated_size = 2048; + stats + .replication_stats + .stats + .entry("arn:minio:replication::t:b".to_string()) + .or_default() + .replicated_count = 5; stats.proxy_stats.put_total = 3; let body = serialize_replication_metrics_body(&stats, ReplicationExtRoute::MetricsV1).expect("metrics v1 body should serialize"); let payload: serde_json::Value = serde_json::from_slice(&body).expect("body should be json"); - assert_eq!(payload["replica_count"], 7); + assert_eq!(payload["replicaCount"], 7); + assert_eq!(payload["completedReplicationSize"], 2048); + assert_eq!(payload["Stats"]["arn:minio:replication::t:b"]["replicationCount"], 5); assert!(payload.get("uptime").is_none()); assert!(payload.get("proxy_stats").is_none()); + // The internal snake_case names must not leak into the wire body. + assert!(payload.get("replica_count").is_none()); + assert!(payload.get("q_stat").is_none()); } #[test] @@ -4248,13 +4263,30 @@ mod tests { assert_eq!(target.current_bandwidth_bytes_per_sec, 3000.0); } + /// The v2 body must decode into minio-go `replication.MetricsV2` + /// (`uptime`/`currStats`/`queueStats`); `mc replicate status` reads + /// `currStats` and `queueStats.nodes` and silently shows zeros when the + /// keys do not match. #[test] - fn serialize_replication_metrics_body_v2_returns_full_bucket_stats() { + fn serialize_replication_metrics_body_v2_returns_minio_go_metrics_v2_shape() { let mut stats = BucketStats { uptime: 99, ..Default::default() }; stats.replication_stats.replica_count = 7; + stats + .replication_stats + .q_stat + .curr + .now_count + .store(4, std::sync::atomic::Ordering::Relaxed); + stats + .replication_stats + .q_stat + .curr + .now_bytes + .store(1200, std::sync::atomic::Ordering::Relaxed); + stats.replication_stats.q_stat = stats.replication_stats.q_stat.snapshot(); stats.proxy_stats.put_total = 3; let body = @@ -4262,8 +4294,17 @@ mod tests { let payload: serde_json::Value = serde_json::from_slice(&body).expect("body should be json"); assert_eq!(payload["uptime"], 99); - assert_eq!(payload["replication_stats"]["replica_count"], 7); - assert_eq!(payload["proxy_stats"]["put_total"], 3); + assert_eq!(payload["currStats"]["replicaCount"], 7); + assert_eq!(payload["currStats"]["queued"]["curr"]["count"], 4.0); + // The queue snapshot must surface at least one node: mc derives the + // worker/queue panels from queueStats.nodes and treats an empty list + // as "no data". + assert_eq!(payload["queueStats"]["nodes"][0]["queueStats"]["curr"]["count"], 4.0); + assert_eq!(payload["queueStats"]["nodes"][0]["uptime"], 99); + // The internal snake_case names must not leak into the wire body. + assert!(payload.get("replication_stats").is_none()); + assert!(payload.get("queue_stats").is_none()); + assert!(payload.get("proxy_stats").is_none()); } #[test]