refactor(obs): hide replication stats handle (#4149)

This commit is contained in:
Zhengchao An
2026-07-01 23:37:23 +08:00
committed by GitHub
parent 2f5e4a8de4
commit 644a472e52
7 changed files with 281 additions and 141 deletions
+4 -4
View File
@@ -31,9 +31,9 @@ pub use scheduler::{
init_metrics_runtime, metrics_runtime_controller_snapshot, metrics_runtime_status_snapshot, init_metrics_runtime, metrics_runtime_controller_snapshot, metrics_runtime_status_snapshot,
}; };
pub(crate) use storage_api::metrics::{ pub(crate) use storage_api::metrics::{
BucketOperations, BucketOptions, ObsBucketBandwidthMonitor, ObsEcstoreResult, ObsReplicationStats, ObsStore, StorageAdminApi, BucketOperations, BucketOptions, ObsBucketBandwidthMonitor, ObsEcstoreResult, ObsStore, StorageAdminApi,
obs_expiry_state_handle, obs_get_global_bucket_monitor, obs_get_global_replication_stats, obs_get_quota_config, obs_bucket_replication_stats_snapshot, obs_expiry_state_handle, obs_get_global_bucket_monitor, obs_get_quota_config,
obs_get_total_usable_capacity, obs_get_total_usable_capacity_free, obs_is_disk_compression_enabled, obs_get_total_usable_capacity, obs_get_total_usable_capacity_free, obs_is_disk_compression_enabled,
obs_load_compression_total_from_memory, obs_load_data_usage_from_backend, obs_resolve_object_store_handle, obs_load_compression_total_from_memory, obs_load_data_usage_from_backend, obs_replication_site_stats_snapshot,
obs_transition_state_handle, obs_resolve_object_store_handle, obs_transition_state_handle,
}; };
+1 -8
View File
@@ -13,14 +13,11 @@
// limitations under the License. // limitations under the License.
use crate::metrics::{ use crate::metrics::{
ObsBucketBandwidthMonitor, ObsReplicationStats, obs_expiry_state_handle, obs_get_global_bucket_monitor, ObsBucketBandwidthMonitor, obs_expiry_state_handle, obs_get_global_bucket_monitor, obs_transition_state_handle,
obs_get_global_replication_stats, obs_transition_state_handle,
}; };
use rustfs_iam::{get_global_iam_sys, oidc::oidc_plugin_authn_metrics_snapshot}; use rustfs_iam::{get_global_iam_sys, oidc::oidc_plugin_authn_metrics_snapshot};
use std::sync::Arc; use std::sync::Arc;
pub(crate) type ObsReplicationStatsHandle = Arc<ObsReplicationStats>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ObsIlmRuntimeSnapshot { pub(crate) struct ObsIlmRuntimeSnapshot {
pub(crate) expiry_pending_tasks: u64, pub(crate) expiry_pending_tasks: u64,
@@ -63,10 +60,6 @@ pub(crate) fn bucket_monitor_available() -> bool {
bucket_monitor_handle().is_some() bucket_monitor_handle().is_some()
} }
pub(crate) fn replication_stats_handle() -> Option<ObsReplicationStatsHandle> {
obs_get_global_replication_stats()
}
pub(crate) async fn ilm_runtime_snapshot() -> ObsIlmRuntimeSnapshot { pub(crate) async fn ilm_runtime_snapshot() -> ObsIlmRuntimeSnapshot {
let expiry_state = obs_expiry_state_handle(); let expiry_state = obs_expiry_state_handle();
let transition_state = obs_transition_state_handle(); let transition_state = obs_transition_state_handle();
+57 -115
View File
@@ -26,20 +26,19 @@ use crate::metrics::collectors::{
DriveCountStats, DriveDetailedStats, ErasureSetStats, HostNetworkStats, IamStats, IlmStats, MemoryStats, NetworkStats, DriveCountStats, DriveDetailedStats, ErasureSetStats, HostNetworkStats, IamStats, IlmStats, MemoryStats, NetworkStats,
ProcessStats, ProcessStatusType, ReplicationStats, ResourceStats, ScannerStats, ProcessStats, ProcessStatusType, ReplicationStats, ResourceStats, ScannerStats,
}; };
use crate::metrics::runtime_sources::{ use crate::metrics::runtime_sources::{ObsIlmRuntimeSnapshot, bucket_monitor_handle, iam_metrics_snapshot, ilm_runtime_snapshot};
ObsIlmRuntimeSnapshot, bucket_monitor_handle, iam_metrics_snapshot, ilm_runtime_snapshot, replication_stats_handle,
};
use crate::metrics::{ use crate::metrics::{
BucketOperations, BucketOptions, ObsEcstoreResult, ObsStore, StorageAdminApi, obs_get_quota_config, BucketOperations, BucketOptions, ObsEcstoreResult, ObsStore, StorageAdminApi, obs_bucket_replication_stats_snapshot,
obs_get_total_usable_capacity, obs_get_total_usable_capacity_free, obs_load_compression_total_from_memory, obs_get_quota_config, obs_get_total_usable_capacity, obs_get_total_usable_capacity_free,
obs_load_data_usage_from_backend, obs_resolve_object_store_handle, obs_load_compression_total_from_memory, obs_load_data_usage_from_backend, obs_replication_site_stats_snapshot,
obs_resolve_object_store_handle,
}; };
use chrono::Utc; use chrono::Utc;
use rustfs_common::heal_channel::HealScanMode; use rustfs_common::heal_channel::HealScanMode;
use rustfs_common::metrics::global_metrics; use rustfs_common::metrics::global_metrics;
use rustfs_io_metrics::internode_metrics::global_internode_metrics; use rustfs_io_metrics::internode_metrics::global_internode_metrics;
use rustfs_io_metrics::{ProcessStatusSnapshot, snapshot_process_resource_and_system}; use rustfs_io_metrics::{ProcessStatusSnapshot, snapshot_process_resource_and_system};
use std::{collections::HashMap, sync::Arc, time::Duration}; use std::{collections::HashMap, sync::Arc};
use sysinfo::{Networks, System}; use sysinfo::{Networks, System};
use tracing::{instrument, warn}; use tracing::{instrument, warn};
@@ -79,14 +78,6 @@ fn usize_to_u64_saturating(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX) u64::try_from(value).unwrap_or(u64::MAX)
} }
fn i64_to_u64_floor_zero(value: i64) -> u64 {
u64::try_from(value.max(0)).unwrap_or(0)
}
fn i32_to_u64_floor_zero(value: i32) -> u64 {
u64::try_from(value.max(0)).unwrap_or(0)
}
async fn load_obs_data_usage_from_backend(store: Arc<ObsStore>) -> ObsEcstoreResult<ObsDataUsageInfo> { async fn load_obs_data_usage_from_backend(store: Arc<ObsStore>) -> ObsEcstoreResult<ObsDataUsageInfo> {
let data_usage = obs_load_data_usage_from_backend(store).await?; let data_usage = obs_load_data_usage_from_backend(store).await?;
@@ -158,116 +149,67 @@ async fn obs_ilm_runtime_snapshot() -> ObsIlmRuntimeSnapshot {
} }
async fn obs_bucket_replication_detail_stats() -> Vec<BucketReplicationStats> { async fn obs_bucket_replication_detail_stats() -> Vec<BucketReplicationStats> {
let Some(stats) = replication_stats_handle() else { obs_bucket_replication_stats_snapshot()
return Vec::new(); .await
}; .into_iter()
.map(|stats| BucketReplicationStats {
let all_bucket_stats = stats.get_all().await; bucket: stats.bucket,
let mut buckets = Vec::with_capacity(all_bucket_stats.len()); total_failed_bytes: stats.total_failed_bytes,
total_failed_count: stats.total_failed_count,
for (bucket, bucket_stats) in all_bucket_stats { last_min_failed_bytes: stats.last_min_failed_bytes,
let proxy = stats.get_proxy_stats(&bucket).await; last_min_failed_count: stats.last_min_failed_count,
let mut total_failed_bytes = 0u64; last_hour_failed_bytes: stats.last_hour_failed_bytes,
let mut total_failed_count = 0u64; last_hour_failed_count: stats.last_hour_failed_count,
let mut last_min_failed_bytes = 0u64; sent_bytes: stats.sent_bytes,
let mut last_min_failed_count = 0u64; sent_count: stats.sent_count,
let mut last_hour_failed_bytes = 0u64; proxied_get_requests_total: stats.proxied_get_requests_total,
let mut last_hour_failed_count = 0u64; proxied_get_requests_failures: stats.proxied_get_requests_failures,
let mut sent_bytes = 0u64; proxied_head_requests_total: stats.proxied_head_requests_total,
let mut sent_count = 0u64; proxied_head_requests_failures: stats.proxied_head_requests_failures,
let mut targets = Vec::with_capacity(bucket_stats.stats.len()); proxied_put_requests_total: stats.proxied_put_requests_total,
proxied_put_requests_failures: stats.proxied_put_requests_failures,
for (target_arn, target_stats) in bucket_stats.stats { proxied_put_tagging_requests_total: stats.proxied_put_tagging_requests_total,
total_failed_bytes += i64_to_u64_floor_zero(target_stats.fail_stats.size); proxied_put_tagging_requests_failures: stats.proxied_put_tagging_requests_failures,
total_failed_count += i64_to_u64_floor_zero(target_stats.fail_stats.count); proxied_get_tagging_requests_total: stats.proxied_get_tagging_requests_total,
proxied_get_tagging_requests_failures: stats.proxied_get_tagging_requests_failures,
let last_min = target_stats.fail_stats.recent_since(Duration::from_secs(60)); proxied_delete_tagging_requests_total: stats.proxied_delete_tagging_requests_total,
last_min_failed_bytes += i64_to_u64_floor_zero(last_min.size); proxied_delete_tagging_requests_failures: stats.proxied_delete_tagging_requests_failures,
last_min_failed_count += i64_to_u64_floor_zero(last_min.count); targets: stats
.targets
let last_hour = target_stats.fail_stats.recent_since(Duration::from_secs(60 * 60)); .into_iter()
last_hour_failed_bytes += i64_to_u64_floor_zero(last_hour.size); .map(|target| BucketReplicationTargetStats {
last_hour_failed_count += i64_to_u64_floor_zero(last_hour.count); target_arn: target.target_arn,
bandwidth_limit_bytes_per_sec: target.bandwidth_limit_bytes_per_sec,
sent_bytes += i64_to_u64_floor_zero(target_stats.replicated_size); current_bandwidth_bytes_per_sec: target.current_bandwidth_bytes_per_sec,
sent_count += i64_to_u64_floor_zero(target_stats.replicated_count); latency_ms: target.latency_ms,
})
targets.push(BucketReplicationTargetStats { .collect(),
target_arn, })
bandwidth_limit_bytes_per_sec: i64_to_u64_floor_zero(target_stats.bandwidth_limit_bytes_per_sec), .collect()
current_bandwidth_bytes_per_sec: target_stats.current_bandwidth_bytes_per_sec,
latency_ms: target_stats.latency.curr,
});
}
buckets.push(BucketReplicationStats {
bucket,
total_failed_bytes,
total_failed_count,
last_min_failed_bytes,
last_min_failed_count,
last_hour_failed_bytes,
last_hour_failed_count,
sent_bytes,
sent_count,
proxied_get_requests_total: i64_to_u64_floor_zero(proxy.get_total),
proxied_get_requests_failures: i64_to_u64_floor_zero(proxy.get_failed),
proxied_head_requests_total: i64_to_u64_floor_zero(proxy.head_total),
proxied_head_requests_failures: i64_to_u64_floor_zero(proxy.head_failed),
proxied_put_requests_total: i64_to_u64_floor_zero(proxy.put_total),
proxied_put_requests_failures: i64_to_u64_floor_zero(proxy.put_failed),
proxied_put_tagging_requests_total: i64_to_u64_floor_zero(proxy.put_tag_total),
proxied_put_tagging_requests_failures: i64_to_u64_floor_zero(proxy.put_tag_failed),
proxied_get_tagging_requests_total: i64_to_u64_floor_zero(proxy.get_tag_total),
proxied_get_tagging_requests_failures: i64_to_u64_floor_zero(proxy.get_tag_failed),
proxied_delete_tagging_requests_total: i64_to_u64_floor_zero(proxy.delete_tag_total),
proxied_delete_tagging_requests_failures: i64_to_u64_floor_zero(proxy.delete_tag_failed),
targets,
});
}
buckets
} }
async fn obs_site_replication_stats() -> ReplicationStats { async fn obs_site_replication_stats() -> ReplicationStats {
let Some(stats) = replication_stats_handle() else {
return ReplicationStats::default();
};
let site_metrics = stats.get_sr_metrics_for_node().await;
let current_data_transfer_rate = obs_bucket_replication_bandwidth_stats() let current_data_transfer_rate = obs_bucket_replication_bandwidth_stats()
.into_iter() .into_iter()
.flatten() .flatten()
.map(|stat| stat.current_bandwidth_bytes_per_sec) .map(|stat| stat.current_bandwidth_bytes_per_sec)
.sum::<f64>(); .sum::<f64>();
let stats = obs_replication_site_stats_snapshot(current_data_transfer_rate).await;
let all_bucket_stats = stats.get_all().await;
let average_data_transfer_rate = all_bucket_stats
.values()
.flat_map(|bucket| bucket.stats.values())
.map(|stat| stat.xfer_rate_lrg.avg + stat.xfer_rate_sml.avg)
.sum::<f64>();
let max_data_transfer_rate = all_bucket_stats
.values()
.flat_map(|bucket| bucket.stats.values())
.map(|stat| stat.xfer_rate_lrg.peak + stat.xfer_rate_sml.peak)
.sum::<f64>();
let recent_backlog_count = stats.mrf_stats.values().copied().filter(|value| *value > 0).sum::<i64>();
ReplicationStats { ReplicationStats {
average_active_workers: site_metrics.active_workers.avg, average_active_workers: stats.average_active_workers,
average_queued_bytes: site_metrics.queued.avg.bytes, average_queued_bytes: stats.average_queued_bytes,
average_queued_count: site_metrics.queued.avg.count, average_queued_count: stats.average_queued_count,
average_data_transfer_rate, average_data_transfer_rate: stats.average_data_transfer_rate,
active_workers: i32_to_u64_floor_zero(site_metrics.active_workers.curr), active_workers: stats.active_workers,
current_data_transfer_rate, current_data_transfer_rate: stats.current_data_transfer_rate,
last_minute_queued_bytes: i64_to_u64_floor_zero(site_metrics.queued.last_minute.bytes), last_minute_queued_bytes: stats.last_minute_queued_bytes,
last_minute_queued_count: i64_to_u64_floor_zero(site_metrics.queued.last_minute.count), last_minute_queued_count: stats.last_minute_queued_count,
max_active_workers: i32_to_u64_floor_zero(site_metrics.active_workers.max), max_active_workers: stats.max_active_workers,
max_queued_bytes: i64_to_u64_floor_zero(site_metrics.queued.max.bytes), max_queued_bytes: stats.max_queued_bytes,
max_queued_count: i64_to_u64_floor_zero(site_metrics.queued.max.count), max_queued_count: stats.max_queued_count,
max_data_transfer_rate, max_data_transfer_rate: stats.max_data_transfer_rate,
recent_backlog_count: i64_to_u64_floor_zero(recent_backlog_count), recent_backlog_count: stats.recent_backlog_count,
} }
} }
+188 -7
View File
@@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::time::Duration;
pub(crate) use rustfs_ecstore::api::bucket::bandwidth::monitor::Monitor as ObsBucketBandwidthMonitor; pub(crate) use rustfs_ecstore::api::bucket::bandwidth::monitor::Monitor as ObsBucketBandwidthMonitor;
pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::get_quota_config as obs_get_quota_config; pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::get_quota_config as obs_get_quota_config;
pub(crate) use rustfs_ecstore::api::bucket::replication::{ use rustfs_ecstore::api::bucket::replication::get_global_replication_stats;
ReplicationStats as ObsReplicationStats, get_global_replication_stats as obs_get_global_replication_stats,
};
pub(crate) use rustfs_ecstore::api::capacity::{ pub(crate) use rustfs_ecstore::api::capacity::{
get_total_usable_capacity as obs_get_total_usable_capacity, get_total_usable_capacity as obs_get_total_usable_capacity,
get_total_usable_capacity_free as obs_get_total_usable_capacity_free, get_total_usable_capacity_free as obs_get_total_usable_capacity_free,
@@ -32,13 +32,194 @@ pub(crate) use rustfs_ecstore::api::runtime::{
pub(crate) use rustfs_ecstore::api::storage::ECStore as ObsStore; pub(crate) use rustfs_ecstore::api::storage::ECStore as ObsStore;
use rustfs_storage_api as storage_contracts; use rustfs_storage_api as storage_contracts;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ObsBucketReplicationTargetStatsSnapshot {
pub(crate) target_arn: String,
pub(crate) bandwidth_limit_bytes_per_sec: u64,
pub(crate) current_bandwidth_bytes_per_sec: f64,
pub(crate) latency_ms: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ObsBucketReplicationStatsSnapshot {
pub(crate) bucket: String,
pub(crate) total_failed_bytes: u64,
pub(crate) total_failed_count: u64,
pub(crate) last_min_failed_bytes: u64,
pub(crate) last_min_failed_count: u64,
pub(crate) last_hour_failed_bytes: u64,
pub(crate) last_hour_failed_count: u64,
pub(crate) sent_bytes: u64,
pub(crate) sent_count: u64,
pub(crate) proxied_get_requests_total: u64,
pub(crate) proxied_get_requests_failures: u64,
pub(crate) proxied_head_requests_total: u64,
pub(crate) proxied_head_requests_failures: u64,
pub(crate) proxied_put_requests_total: u64,
pub(crate) proxied_put_requests_failures: u64,
pub(crate) proxied_put_tagging_requests_total: u64,
pub(crate) proxied_put_tagging_requests_failures: u64,
pub(crate) proxied_get_tagging_requests_total: u64,
pub(crate) proxied_get_tagging_requests_failures: u64,
pub(crate) proxied_delete_tagging_requests_total: u64,
pub(crate) proxied_delete_tagging_requests_failures: u64,
pub(crate) targets: Vec<ObsBucketReplicationTargetStatsSnapshot>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct ObsReplicationSiteStatsSnapshot {
pub(crate) average_active_workers: f64,
pub(crate) average_queued_bytes: i64,
pub(crate) average_queued_count: i64,
pub(crate) average_data_transfer_rate: f64,
pub(crate) active_workers: u64,
pub(crate) current_data_transfer_rate: f64,
pub(crate) last_minute_queued_bytes: u64,
pub(crate) last_minute_queued_count: u64,
pub(crate) max_active_workers: u64,
pub(crate) max_queued_bytes: u64,
pub(crate) max_queued_count: u64,
pub(crate) max_data_transfer_rate: f64,
pub(crate) recent_backlog_count: u64,
}
fn i64_to_u64_floor_zero(value: i64) -> u64 {
u64::try_from(value.max(0)).unwrap_or(0)
}
fn i32_to_u64_floor_zero(value: i32) -> u64 {
u64::try_from(value.max(0)).unwrap_or(0)
}
pub(crate) async fn obs_bucket_replication_stats_snapshot() -> Vec<ObsBucketReplicationStatsSnapshot> {
let Some(stats) = get_global_replication_stats() else {
return Vec::new();
};
let all_bucket_stats = stats.get_all().await;
let mut buckets = Vec::with_capacity(all_bucket_stats.len());
for (bucket, bucket_stats) in all_bucket_stats {
let proxy = stats.get_proxy_stats(&bucket).await;
let mut total_failed_bytes = 0u64;
let mut total_failed_count = 0u64;
let mut last_min_failed_bytes = 0u64;
let mut last_min_failed_count = 0u64;
let mut last_hour_failed_bytes = 0u64;
let mut last_hour_failed_count = 0u64;
let mut sent_bytes = 0u64;
let mut sent_count = 0u64;
let mut targets = Vec::with_capacity(bucket_stats.stats.len());
for (target_arn, target_stats) in bucket_stats.stats {
total_failed_bytes = total_failed_bytes.saturating_add(i64_to_u64_floor_zero(target_stats.fail_stats.size));
total_failed_count = total_failed_count.saturating_add(i64_to_u64_floor_zero(target_stats.fail_stats.count));
let last_min = target_stats.fail_stats.recent_since(Duration::from_secs(60));
last_min_failed_bytes = last_min_failed_bytes.saturating_add(i64_to_u64_floor_zero(last_min.size));
last_min_failed_count = last_min_failed_count.saturating_add(i64_to_u64_floor_zero(last_min.count));
let last_hour = target_stats.fail_stats.recent_since(Duration::from_secs(60 * 60));
last_hour_failed_bytes = last_hour_failed_bytes.saturating_add(i64_to_u64_floor_zero(last_hour.size));
last_hour_failed_count = last_hour_failed_count.saturating_add(i64_to_u64_floor_zero(last_hour.count));
sent_bytes = sent_bytes.saturating_add(i64_to_u64_floor_zero(target_stats.replicated_size));
sent_count = sent_count.saturating_add(i64_to_u64_floor_zero(target_stats.replicated_count));
targets.push(ObsBucketReplicationTargetStatsSnapshot {
target_arn,
bandwidth_limit_bytes_per_sec: i64_to_u64_floor_zero(target_stats.bandwidth_limit_bytes_per_sec),
current_bandwidth_bytes_per_sec: target_stats.current_bandwidth_bytes_per_sec,
latency_ms: target_stats.latency.curr,
});
}
buckets.push(ObsBucketReplicationStatsSnapshot {
bucket,
total_failed_bytes,
total_failed_count,
last_min_failed_bytes,
last_min_failed_count,
last_hour_failed_bytes,
last_hour_failed_count,
sent_bytes,
sent_count,
proxied_get_requests_total: i64_to_u64_floor_zero(proxy.get_total),
proxied_get_requests_failures: i64_to_u64_floor_zero(proxy.get_failed),
proxied_head_requests_total: i64_to_u64_floor_zero(proxy.head_total),
proxied_head_requests_failures: i64_to_u64_floor_zero(proxy.head_failed),
proxied_put_requests_total: i64_to_u64_floor_zero(proxy.put_total),
proxied_put_requests_failures: i64_to_u64_floor_zero(proxy.put_failed),
proxied_put_tagging_requests_total: i64_to_u64_floor_zero(proxy.put_tag_total),
proxied_put_tagging_requests_failures: i64_to_u64_floor_zero(proxy.put_tag_failed),
proxied_get_tagging_requests_total: i64_to_u64_floor_zero(proxy.get_tag_total),
proxied_get_tagging_requests_failures: i64_to_u64_floor_zero(proxy.get_tag_failed),
proxied_delete_tagging_requests_total: i64_to_u64_floor_zero(proxy.delete_tag_total),
proxied_delete_tagging_requests_failures: i64_to_u64_floor_zero(proxy.delete_tag_failed),
targets,
});
}
buckets
}
pub(crate) async fn obs_replication_site_stats_snapshot(current_data_transfer_rate: f64) -> ObsReplicationSiteStatsSnapshot {
let Some(stats) = get_global_replication_stats() else {
return ObsReplicationSiteStatsSnapshot::default();
};
let site_metrics = stats.get_sr_metrics_for_node().await;
let all_bucket_stats = stats.get_all().await;
// These fields keep the existing metric semantics: cluster-wide sums across bucket targets.
let average_data_transfer_rate = all_bucket_stats
.values()
.flat_map(|bucket| bucket.stats.values())
.map(|stat| stat.xfer_rate_lrg.avg + stat.xfer_rate_sml.avg)
.sum::<f64>();
let max_data_transfer_rate = all_bucket_stats
.values()
.flat_map(|bucket| bucket.stats.values())
.map(|stat| stat.xfer_rate_lrg.peak + stat.xfer_rate_sml.peak)
.sum::<f64>();
let recent_backlog_count = stats.mrf_stats.values().copied().filter(|value| *value > 0).sum::<i64>();
ObsReplicationSiteStatsSnapshot {
average_active_workers: site_metrics.active_workers.avg,
average_queued_bytes: site_metrics.queued.avg.bytes,
average_queued_count: site_metrics.queued.avg.count,
average_data_transfer_rate,
active_workers: i32_to_u64_floor_zero(site_metrics.active_workers.curr),
current_data_transfer_rate,
last_minute_queued_bytes: i64_to_u64_floor_zero(site_metrics.queued.last_minute.bytes),
last_minute_queued_count: i64_to_u64_floor_zero(site_metrics.queued.last_minute.count),
max_active_workers: i32_to_u64_floor_zero(site_metrics.active_workers.max),
max_queued_bytes: i64_to_u64_floor_zero(site_metrics.queued.max.bytes),
max_queued_count: i64_to_u64_floor_zero(site_metrics.queued.max.count),
max_data_transfer_rate,
recent_backlog_count: i64_to_u64_floor_zero(recent_backlog_count),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn obs_replication_numeric_conversions_floor_negative_values() {
assert_eq!(i64_to_u64_floor_zero(-1), 0);
assert_eq!(i64_to_u64_floor_zero(42), 42);
assert_eq!(i32_to_u64_floor_zero(-1), 0);
assert_eq!(i32_to_u64_floor_zero(42), 42);
}
}
pub(crate) mod metrics { pub(crate) mod metrics {
pub(crate) use super::storage_contracts::{BucketOperations, BucketOptions, StorageAdminApi}; pub(crate) use super::storage_contracts::{BucketOperations, BucketOptions, StorageAdminApi};
pub(crate) use super::{ pub(crate) use super::{
ObsBucketBandwidthMonitor, ObsEcstoreResult, ObsReplicationStats, ObsStore, obs_expiry_state_handle, ObsBucketBandwidthMonitor, ObsEcstoreResult, ObsStore, obs_bucket_replication_stats_snapshot, obs_expiry_state_handle,
obs_get_global_bucket_monitor, obs_get_global_replication_stats, obs_get_quota_config, obs_get_total_usable_capacity, obs_get_global_bucket_monitor, obs_get_quota_config, obs_get_total_usable_capacity, obs_get_total_usable_capacity_free,
obs_get_total_usable_capacity_free, obs_is_disk_compression_enabled, obs_load_compression_total_from_memory, obs_is_disk_compression_enabled, obs_load_compression_total_from_memory, obs_load_data_usage_from_backend,
obs_load_data_usage_from_backend, obs_resolve_object_store_handle, obs_transition_state_handle, obs_replication_site_stats_snapshot, obs_resolve_object_store_handle, obs_transition_state_handle,
}; };
} }
@@ -189,6 +189,10 @@ Required contracts before crate movement:
- `ReplicationObjectBridge`: app and SetDisks object write/delete replication - `ReplicationObjectBridge`: app and SetDisks object write/delete replication
decisions and scheduling are exposed through the contract type in decisions and scheduling are exposed through the contract type in
`crates/ecstore/src/bucket/replication/replication_object_bridge.rs`. `crates/ecstore/src/bucket/replication/replication_object_bridge.rs`.
- `ObsReplicationStatsSnapshot`: observability reads replication bucket/site
metrics through obs-local snapshot DTOs in
`crates/obs/src/metrics/storage_api.rs` instead of carrying the ECStore
replication stats handle through collectors.
- `ReplicationScannerBridge`: scanner-originated replication heal scheduling is - `ReplicationScannerBridge`: scanner-originated replication heal scheduling is
exposed through the contract type in exposed through the contract type in
`crates/ecstore/src/bucket/replication/replication_scanner_bridge.rs`. `crates/ecstore/src/bucket/replication/replication_scanner_bridge.rs`.
@@ -21,7 +21,7 @@ until the contracts below are extracted.
| `rustfs_ecstore::api::bucket::metadata_sys::get_quota_config` | `stats_collector.rs` | Behavior dependency | Reads per-bucket quota limits used in bucket usage metrics. | | `rustfs_ecstore::api::bucket::metadata_sys::get_quota_config` | `stats_collector.rs` | Behavior dependency | Reads per-bucket quota limits used in bucket usage metrics. |
| `rustfs_ecstore::api::bucket::bandwidth::monitor::Monitor` | `runtime_sources.rs`, `stats_collector.rs` | Type and runtime dependency | Reads replication bandwidth reports from the global bucket monitor handle. | | `rustfs_ecstore::api::bucket::bandwidth::monitor::Monitor` | `runtime_sources.rs`, `stats_collector.rs` | Type and runtime dependency | Reads replication bandwidth reports from the global bucket monitor handle. |
| `rustfs_ecstore::api::runtime::bucket_monitor` | `runtime_sources.rs` | Runtime dependency | Resolves the global bucket bandwidth monitor for metric collection. | | `rustfs_ecstore::api::runtime::bucket_monitor` | `runtime_sources.rs` | Runtime dependency | Resolves the global bucket bandwidth monitor for metric collection. |
| `rustfs_ecstore::api::bucket::replication::{GLOBAL_REPLICATION_STATS, ReplicationStats}` | `runtime_sources.rs`, `stats_collector.rs` | Type and runtime dependency | Reads replication status, transfer, failure, and site-replication stats. | | `rustfs_ecstore::api::bucket::replication::get_global_replication_stats` | `storage_api.rs` snapshot helpers | Runtime dependency | Reads replication status, transfer, failure, and site-replication stats, then projects them into obs-local snapshot DTOs. |
| `rustfs_ecstore::api::bucket::lifecycle::bucket_lifecycle_ops::{GLOBAL_ExpiryState, GLOBAL_TransitionState}` | `runtime_sources.rs`, `stats_collector.rs` | Runtime dependency | Reads lifecycle expiry and transition queue counters. | | `rustfs_ecstore::api::bucket::lifecycle::bucket_lifecycle_ops::{GLOBAL_ExpiryState, GLOBAL_TransitionState}` | `runtime_sources.rs`, `stats_collector.rs` | Runtime dependency | Reads lifecycle expiry and transition queue counters. |
| `rustfs_ecstore::api::error::Result` as `ObsEcstoreResult` | `stats_collector.rs` | Type dependency | Preserves ECStore error propagation while data-usage behavior remains ECStore-owned. | | `rustfs_ecstore::api::error::Result` as `ObsEcstoreResult` | `stats_collector.rs` | Type dependency | Preserves ECStore error propagation while data-usage behavior remains ECStore-owned. |
@@ -29,11 +29,10 @@ until the contracts below are extracted.
The remaining coupling is not just a dependency declaration problem: The remaining coupling is not just a dependency declaration problem:
- type coupling: `ObsStore`, `ObsEcstoreResult`, `ObsReplicationStats`, - type coupling: `ObsStore`, `ObsEcstoreResult`, `ObsBucketBandwidthMonitor`,
`ObsBucketBandwidthMonitor`, `StorageAdminApi`, `BucketOperations`, and `StorageAdminApi`, `BucketOperations`, and `BucketOptions`;
`BucketOptions`;
- runtime handle coupling: object-store handle, bucket monitor, replication - runtime handle coupling: object-store handle, bucket monitor, replication
stats, expiry state, and transition state; stats inside snapshot helpers, expiry state, and transition state;
- behavior coupling: data-usage loading, quota lookup, and capacity math. - behavior coupling: data-usage loading, quota lookup, and capacity math.
Removing `rustfs-ecstore` from `crates/obs/Cargo.toml` is unsafe until those Removing `rustfs-ecstore` from `crates/obs/Cargo.toml` is unsafe until those
@@ -43,8 +42,8 @@ three categories have replacement contracts and compile coverage.
1. Keep all direct ECStore and storage-api imports centralized in 1. Keep all direct ECStore and storage-api imports centralized in
`crates/obs/src/metrics/storage_api.rs`. `crates/obs/src/metrics/storage_api.rs`.
2. Keep projecting ECStore data-usage output into obs-local DTOs before 2. Keep projecting ECStore data-usage and replication stats output into
collectors consume it. obs-local DTOs before collectors consume it.
3. Introduce obs-owned provider traits for storage info, bucket info, quota, 3. Introduce obs-owned provider traits for storage info, bucket info, quota,
data usage, replication, bandwidth, and lifecycle queue snapshots. data usage, replication, bandwidth, and lifecycle queue snapshots.
4. Implement those traits in ECStore or an ECStore-owned adapter crate after the 4. Implement those traits in ECStore or an ECStore-owned adapter crate after the
@@ -58,6 +57,8 @@ The architecture guard enforces this inventory boundary:
- `crates/obs/src/metrics/storage_api.rs` is the only `rustfs-obs` source file - `crates/obs/src/metrics/storage_api.rs` is the only `rustfs-obs` source file
allowed to reference `rustfs_ecstore` or `rustfs_storage_api`; allowed to reference `rustfs_ecstore` or `rustfs_storage_api`;
- raw replication stats handles and ECStore replication stat methods must stay
behind the snapshot helpers in `crates/obs/src/metrics/storage_api.rs`;
- `rustfs-obs` must not add `storage_compat.rs` or `ecstore_compat.rs` - `rustfs-obs` must not add `storage_compat.rs` or `ecstore_compat.rs`
passthrough bridges; passthrough bridges;
- future extraction PRs must update this inventory and the guard in the same - future extraction PRs must update this inventory and the guard in the same
@@ -201,6 +201,7 @@ REPLICATION_FACADE_BYPASS_HITS_FILE="${TMP_DIR}/replication_facade_bypass_hits.t
REPLICATION_FACADE_WILDCARD_EXPORT_HITS_FILE="${TMP_DIR}/replication_facade_wildcard_export_hits.txt" REPLICATION_FACADE_WILDCARD_EXPORT_HITS_FILE="${TMP_DIR}/replication_facade_wildcard_export_hits.txt"
ADMIN_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/admin_replication_dto_boundary_bypass_hits.txt" ADMIN_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/admin_replication_dto_boundary_bypass_hits.txt"
APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/app_replication_dto_boundary_bypass_hits.txt" APP_REPLICATION_DTO_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/app_replication_dto_boundary_bypass_hits.txt"
OBS_REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/obs_replication_stats_boundary_bypass_hits.txt"
REPLICATION_BANDWIDTH_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_bandwidth_boundary_bypass_hits.txt" REPLICATION_BANDWIDTH_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_bandwidth_boundary_bypass_hits.txt"
REPLICATION_CONFIG_STORE_BYPASS_HITS_FILE="${TMP_DIR}/replication_config_store_bypass_hits.txt" REPLICATION_CONFIG_STORE_BYPASS_HITS_FILE="${TMP_DIR}/replication_config_store_bypass_hits.txt"
REPLICATION_ERROR_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_error_boundary_bypass_hits.txt" REPLICATION_ERROR_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_error_boundary_bypass_hits.txt"
@@ -2509,6 +2510,24 @@ if [[ -s "$REPLICATION_FACADE_BYPASS_HITS_FILE" ]]; then
report_failure "replication facade imports must stay in local storage_api boundaries: $(paste -sd '; ' "$REPLICATION_FACADE_BYPASS_HITS_FILE")" report_failure "replication facade imports must stay in local storage_api boundaries: $(paste -sd '; ' "$REPLICATION_FACADE_BYPASS_HITS_FILE")"
fi fi
(
cd "$ROOT_DIR"
{
rg -n --with-filename '\b(ObsReplicationStats|obs_get_global_replication_stats|replication_stats_handle|get_sr_metrics_for_node|get_proxy_stats|mrf_stats)\b' \
crates/obs/src/metrics \
--glob '*.rs' \
--glob '!crates/obs/src/metrics/storage_api.rs' || true
rg -n --with-filename 'rustfs_ecstore::api::bucket::replication' \
crates/obs/src/metrics \
--glob '*.rs' \
--glob '!crates/obs/src/metrics/storage_api.rs' || true
}
) >"$OBS_REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE"
if [[ -s "$OBS_REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE" ]]; then
report_failure "obs replication stats access must stay behind crates/obs/src/metrics/storage_api.rs snapshot helpers: $(paste -sd '; ' "$OBS_REPLICATION_STATS_BOUNDARY_BYPASS_HITS_FILE")"
fi
( (
cd "$ROOT_DIR" cd "$ROOT_DIR"
rg -n --with-filename 'pub(?:\(crate\))?\s+use\s+(?:config|datatypes|replication_pool|replication_resyncer|replication_state|rule)::\*;' \ rg -n --with-filename 'pub(?:\(crate\))?\s+use\s+(?:config|datatypes|replication_pool|replication_resyncer|replication_state|rule)::\*;' \