// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::StorageAPI; use crate::admin_server_info::get_commit_id; use crate::error::{Error, Result}; use crate::global::{GLOBAL_BOOT_TIME, get_global_endpoints}; use crate::metrics_realtime::{CollectMetricsOpts, MetricType}; use crate::rebalance::RebalSaveOpt; use crate::rpc::PeerRestClient; use crate::{endpoints::EndpointServerPools, new_object_layer_fn}; use futures::future::join_all; use lazy_static::lazy_static; use rustfs_madmin::health::{Cpus, MemInfo, OsInfo, Partitions, ProcInfo, SysConfig, SysErrors, SysServices}; use rustfs_madmin::metrics::RealtimeMetrics; use rustfs_madmin::net::NetInfo; use rustfs_madmin::{ItemState, ServerProperties}; use std::collections::hash_map::DefaultHasher; use std::future::Future; use std::hash::{Hash, Hasher}; use std::sync::OnceLock; use std::time::{Duration, SystemTime}; use tokio::time::timeout; use tracing::{error, warn}; lazy_static! { pub static ref GLOBAL_NotificationSys: OnceLock = OnceLock::new(); } pub async fn new_global_notification_sys(eps: EndpointServerPools) -> Result<()> { let _ = GLOBAL_NotificationSys .set(NotificationSys::new(eps).await) .map_err(|_| Error::other("init notification_sys fail")); Ok(()) } pub fn get_global_notification_sys() -> Option<&'static NotificationSys> { GLOBAL_NotificationSys.get() } pub struct NotificationSys { pub peer_clients: Vec>, #[allow(dead_code)] pub all_peer_clients: Vec>, } impl NotificationSys { pub async fn new(eps: EndpointServerPools) -> Self { let (peer_clients, all_peer_clients) = PeerRestClient::new_clients(eps).await; Self { peer_clients, all_peer_clients, } } } pub struct NotificationPeerErr { pub host: String, pub err: Option, } impl NotificationSys { pub fn rest_client_from_hash(&self, s: &str) -> Option { if self.all_peer_clients.is_empty() { return None; } let mut hasher = DefaultHasher::new(); s.hash(&mut hasher); let idx = (hasher.finish() as usize) % self.all_peer_clients.len(); self.all_peer_clients[idx].clone() } pub async fn delete_policy(&self, policy_name: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let policy = policy_name.to_string(); futures.push(async move { if let Some(client) = client { match client.delete_policy(&policy).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn load_policy(&self, policy_name: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let policy = policy_name.to_string(); futures.push(async move { if let Some(client) = client { match client.load_policy(&policy).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn load_policy_mapping(&self, user_or_group: &str, user_type: u64, is_group: bool) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let uog = user_or_group.to_string(); futures.push(async move { if let Some(client) = client { match client.load_policy_mapping(&uog, user_type, is_group).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn delete_user(&self, access_key: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let ak = access_key.to_string(); futures.push(async move { if let Some(client) = client { match client.delete_user(&ak).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn storage_info(&self, api: &S) -> rustfs_madmin::StorageInfo { let mut futures = Vec::with_capacity(self.peer_clients.len()); let endpoints = get_global_endpoints(); let peer_timeout = Duration::from_secs(2); // Same timeout as server_info for client in self.peer_clients.iter() { let endpoints = endpoints.clone(); futures.push(async move { if let Some(client) = client { let host = client.host.to_string(); // Wrap in timeout to ensure we don't hang on dead peers match timeout(peer_timeout, client.local_storage_info()).await { Ok(Ok(info)) => Some(info), Ok(Err(err)) => { warn!("peer {} storage_info failed: {}", host, err); Some(rustfs_madmin::StorageInfo { disks: get_offline_disks(&host, &endpoints), ..Default::default() }) } Err(_) => { warn!("peer {} storage_info timed out after {:?}", host, peer_timeout); client.evict_connection().await; Some(rustfs_madmin::StorageInfo { disks: get_offline_disks(&host, &endpoints), ..Default::default() }) } } } else { None } }); } let mut replies = join_all(futures).await; replies.push(Some(api.local_storage_info().await)); let mut disks = Vec::new(); for info in replies.into_iter().flatten() { disks.extend(info.disks); } let backend = api.backend_info().await; rustfs_madmin::StorageInfo { disks, backend } } pub async fn server_info(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); let endpoints = get_global_endpoints(); let peer_timeout = Duration::from_secs(2); for client in self.peer_clients.iter() { let endpoints = endpoints.clone(); futures.push(async move { if let Some(client) = client { let host = client.host.to_string(); match timeout(peer_timeout, client.server_info()).await { Ok(Ok(info)) => info, Ok(Err(err)) => { warn!("peer {} server_info failed: {}", host, err); // client.server_info handles eviction internally on error, but fallback needed offline_server_properties(&host, &endpoints) } Err(_) => { warn!("peer {} server_info timed out after {:?}", host, peer_timeout); client.evict_connection().await; offline_server_properties(&host, &endpoints) } } } else { ServerProperties::default() } }); } join_all(futures).await } pub async fn load_user(&self, access_key: &str, temp: bool) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let ak = access_key.to_string(); futures.push(async move { if let Some(client) = client { match client.load_user(&ak, temp).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn load_group(&self, group: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let gname = group.to_string(); futures.push(async move { if let Some(client) = client { match client.load_group(&gname).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn delete_service_account(&self, access_key: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let ak = access_key.to_string(); futures.push(async move { if let Some(client) = client { match client.delete_service_account(&ak).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn load_service_account(&self, access_key: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let ak = access_key.to_string(); futures.push(async move { if let Some(client) = client { match client.load_service_account(&ak).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn reload_pool_meta(&self) -> Result<()> { let mut failures = Vec::new(); let mut futures = Vec::with_capacity(self.peer_clients.len()); for (idx, client) in self.peer_clients.iter().enumerate() { if let Some(client) = client { let host = client.grid_host.clone(); futures.push(async move { client.reload_pool_meta().await.map_err(|err| (host, err)) }); } else { failures.push(format!("peer[{idx}] reload_pool_meta failed: peer is not reachable")); } } for result in join_all(futures).await { if let Err((host, err)) = result { let failure = format!("peer {host} reload_pool_meta failed: {err}"); error!("notification reload_pool_meta err {}", failure); failures.push(failure); } } aggregate_notification_failures("reload_pool_meta", failures) } #[tracing::instrument(skip(self))] pub async fn load_rebalance_meta(&self, start: bool) -> Result<()> { let operation = format!("load_rebalance_meta(start={start})"); let mut failures = Vec::new(); let mut futures = Vec::with_capacity(self.peer_clients.len()); for (idx, client) in self.peer_clients.iter().enumerate() { if let Some(client) = client { warn!( "notification load_rebalance_meta start: {}, index: {}, client: {:?}", start, idx, client.host ); let host = client.grid_host.clone(); futures.push(async move { client.load_rebalance_meta(start).await.map_err(|err| (host, err)) }); } else { failures.push(format!("peer[{idx}] {operation} failed: peer is not reachable")); } } for result in join_all(futures).await { if let Err((host, err)) = result { let failure = format!("peer {host} {operation} failed: {err}"); error!("notification load_rebalance_meta err {}", failure); failures.push(failure); } else { warn!("notification load_rebalance_meta success"); } } aggregate_notification_failures("load_rebalance_meta", failures) } pub async fn stop_rebalance(&self) -> Result<()> { warn!("notification stop_rebalance start"); let Some(store) = new_object_layer_fn() else { error!("stop_rebalance: not init"); return Err(Error::other("stop_rebalance: object layer not initialized")); }; // warn!("notification stop_rebalance load_rebalance_meta"); // self.load_rebalance_meta(false).await; // warn!("notification stop_rebalance load_rebalance_meta done"); let mut failures = Vec::new(); let mut futures = Vec::with_capacity(self.peer_clients.len()); for (idx, client) in self.peer_clients.iter().enumerate() { if let Some(client) = client { let host = client.grid_host.clone(); futures.push(async move { client.stop_rebalance().await.map_err(|err| (host, err)) }); } else { failures.push(format!("peer[{idx}] stop_rebalance failed: peer is not reachable")); } } for result in join_all(futures).await { if let Err((host, err)) = result { let failure = format!("peer {host} stop_rebalance failed: {err}"); error!("notification stop_rebalance err {}", failure); failures.push(failure); } } warn!("notification stop_rebalance stop_rebalance start"); match store.stop_rebalance().await { Ok(_) => { if let Err(err) = store.save_rebalance_stats(usize::MAX, RebalSaveOpt::StoppedAt).await { error!("notification stop_rebalance local save err {:?}", err); return Err(Error::other(format!( "local stop_rebalance save_rebalance_stats(stopped_at) failed: {err}" ))); } } Err(err) => { error!("notification stop_rebalance local stop err {:?}", err); return Err(Error::other(format!("local stop_rebalance stop failed: {err}"))); } } if let Err(err) = aggregate_notification_failures("stop_rebalance", failures) { warn!("{err}"); } warn!("notification stop_rebalance stop_rebalance done"); Ok(()) } pub async fn load_bucket_metadata(&self, bucket: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let b = bucket.to_string(); futures.push(async move { if let Some(client) = client { match client.load_bucket_metadata(&b).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn delete_bucket_metadata(&self, bucket: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let b = bucket.to_string(); futures.push(async move { if let Some(client) = client { match client.delete_bucket_metadata(&b).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn start_profiling(&self, profiler: &str) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { let pf = profiler.to_string(); futures.push(async move { if let Some(client) = client { match client.start_profiling(&pf).await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn get_cpus(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_cpus().await.unwrap_or_default() } else { Cpus::default() } }); } join_all(futures).await } pub async fn get_net_info(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_net_info().await.unwrap_or_default() } else { NetInfo::default() } }); } join_all(futures).await } pub async fn get_partitions(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_partitions().await.unwrap_or_default() } else { Partitions::default() } }); } join_all(futures).await } pub async fn get_os_info(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_os_info().await.unwrap_or_default() } else { OsInfo::default() } }); } join_all(futures).await } pub async fn get_sys_services(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_se_linux_info().await.unwrap_or_default() } else { SysServices::default() } }); } join_all(futures).await } pub async fn get_sys_config(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_sys_config().await.unwrap_or_default() } else { SysConfig::default() } }); } join_all(futures).await } pub async fn get_sys_errors(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_sys_errors().await.unwrap_or_default() } else { SysErrors::default() } }); } join_all(futures).await } pub async fn get_mem_info(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_mem_info().await.unwrap_or_default() } else { MemInfo::default() } }); } join_all(futures).await } pub async fn get_proc_info(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { futures.push(async move { if let Some(client) = client { client.get_proc_info().await.unwrap_or_default() } else { ProcInfo::default() } }); } join_all(futures).await } pub async fn get_metrics(&self, t: MetricType, opts: &CollectMetricsOpts) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter().cloned() { let t_clone = t; let opts_clone = opts; futures.push(async move { if let Some(client) = client { client.get_metrics(t_clone, opts_clone).await.unwrap_or_default() } else { RealtimeMetrics::default() } }); } join_all(futures).await } pub async fn reload_site_replication_config(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { futures.push(async move { if let Some(client) = client { match client.reload_site_replication_config().await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } pub async fn load_transition_tier_config(&self) -> Vec { let mut futures = Vec::with_capacity(self.peer_clients.len()); for client in self.peer_clients.iter() { futures.push(async move { if let Some(client) = client { match client.load_transition_tier_config().await { Ok(_) => NotificationPeerErr { host: client.host.to_string(), err: None, }, Err(e) => NotificationPeerErr { host: client.host.to_string(), err: Some(e), }, } } else { NotificationPeerErr { host: "".to_string(), err: Some(Error::other("peer is not reachable")), } } }); } join_all(futures).await } } async fn call_peer_with_timeout( timeout_dur: Duration, host_label: &str, op: F, fallback: impl FnOnce() -> ServerProperties, ) -> ServerProperties where F: FnOnce() -> Fut, Fut: Future> + Send, { match timeout(timeout_dur, op()).await { Ok(Ok(info)) => info, Ok(Err(err)) => { warn!("peer {host_label} server_info failed: {err}"); fallback() } Err(_) => { warn!("peer {host_label} server_info timed out after {:?}", timeout_dur); fallback() } } } fn offline_server_properties(host: &str, endpoints: &EndpointServerPools) -> ServerProperties { ServerProperties { uptime: GLOBAL_BOOT_TIME .get() .and_then(|boot_time| SystemTime::now().duration_since(*boot_time).ok()) .unwrap_or_default() .as_secs(), version: get_commit_id(), endpoint: host.to_string(), state: ItemState::Offline.to_string().to_owned(), disks: get_offline_disks(host, endpoints), ..Default::default() } } fn get_offline_disks(offline_host: &str, endpoints: &EndpointServerPools) -> Vec { let mut offline_disks = Vec::new(); for pool in endpoints.as_ref() { for ep in pool.endpoints.as_ref() { if (offline_host.is_empty() && ep.is_local) || offline_host == ep.host_port() { offline_disks.push(rustfs_madmin::Disk { endpoint: ep.to_string(), state: ItemState::Offline.to_string().to_owned(), pool_index: ep.pool_idx, set_index: ep.set_idx, disk_index: ep.disk_idx, ..Default::default() }); } } } offline_disks } fn aggregate_notification_failures(operation: &str, failures: Vec) -> Result<()> { if failures.is_empty() { return Ok(()); } Err(Error::other(format!( "{operation} encountered {} failure(s): {}", failures.len(), failures.join(" | ") ))) } #[cfg(test)] mod tests { use super::*; fn build_props(endpoint: &str) -> ServerProperties { ServerProperties { endpoint: endpoint.to_string(), ..Default::default() } } #[tokio::test] async fn call_peer_with_timeout_returns_value_when_fast() { let result = call_peer_with_timeout( Duration::from_millis(50), "peer-1", || async { Ok::<_, Error>(build_props("fast")) }, || build_props("fallback"), ) .await; assert_eq!(result.endpoint, "fast"); } #[tokio::test] async fn call_peer_with_timeout_uses_fallback_on_error() { let result = call_peer_with_timeout( Duration::from_millis(50), "peer-2", || async { Err::(Error::other("boom")) }, || build_props("fallback"), ) .await; assert_eq!(result.endpoint, "fallback"); } #[tokio::test] async fn call_peer_with_timeout_uses_fallback_on_timeout() { let result = call_peer_with_timeout( Duration::from_millis(5), "peer-3", || async { tokio::time::sleep(Duration::from_millis(25)).await; Ok::<_, Error>(build_props("slow")) }, || build_props("fallback"), ) .await; assert_eq!(result.endpoint, "fallback"); } #[test] fn aggregate_notification_failures_returns_ok_when_empty() { assert!(aggregate_notification_failures("stop_rebalance", Vec::new()).is_ok()); } #[test] fn aggregate_notification_failures_returns_joined_error_when_non_empty() { let err = aggregate_notification_failures( "load_rebalance_meta", vec!["peer-1 failed".to_string(), "local save failed".to_string()], ) .expect_err("non-empty failures should return error"); let msg = err.to_string(); assert!(msg.contains("load_rebalance_meta")); assert!(msg.contains("2 failure(s)")); assert!(msg.contains("peer-1 failed")); assert!(msg.contains("local save failed")); } }