Files
rustfs/crates/ecstore/src/rpc/peer_rest_client.rs
T

1218 lines
42 KiB
Rust

// 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::error::{Error, Result};
use crate::rpc::client::{TonicInterceptor, gen_tonic_signature_interceptor, node_service_time_out_client};
use crate::{
disk::disk_store::{get_drive_active_check_interval, get_drive_active_check_timeout},
endpoints::EndpointServerPools,
global::is_dist_erasure,
metrics_realtime::{CollectMetricsOpts, MetricType},
};
use rmp_serde::{Deserializer, Serializer};
use rustfs_madmin::{
ServerProperties,
health::{Cpus, MemInfo, OsInfo, Partitions, ProcInfo, SysConfig, SysErrors, SysServices},
metrics::RealtimeMetrics,
net::NetInfo,
};
use rustfs_protos::evict_failed_connection;
use rustfs_protos::proto_gen::node_service::{
CancelDecommissionRequest, ClearDecommissionRequest, DeleteBucketMetadataRequest, DeletePolicyRequest,
DeleteServiceAccountRequest, DeleteUserRequest, GetCpusRequest, GetLiveEventsRequest, GetMemInfoRequest, GetMetricsRequest,
GetNetInfoRequest, GetOsInfoRequest, GetPartitionsRequest, GetProcInfoRequest, GetSeLinuxInfoRequest, GetSysConfigRequest,
GetSysErrorsRequest, LoadBucketMetadataRequest, LoadGroupRequest, LoadPolicyMappingRequest, LoadPolicyRequest,
LoadRebalanceMetaRequest, LoadServiceAccountRequest, LoadTransitionTierConfigRequest, LoadUserRequest,
LocalStorageInfoRequest, Mss, ReloadPoolMetaRequest, ReloadSiteReplicationConfigRequest, ServerInfoRequest,
SignalServiceRequest, StartDecommissionRequest, StartProfilingRequest, StopRebalanceRequest,
node_service_client::NodeServiceClient,
};
use rustfs_utils::XHost;
use serde::{Deserialize, Serialize as _};
use std::{
collections::HashMap,
io::Cursor,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::SystemTime,
};
use tokio::{net::TcpStream, time::Duration};
use tonic::Request;
use tonic::service::interceptor::InterceptedService;
use tonic::transport::Channel;
use tracing::{Instrument, debug, warn};
pub const PEER_RESTSIGNAL: &str = "signal";
pub const PEER_RESTSUB_SYS: &str = "sub-sys";
pub const PEER_RESTDRY_RUN: &str = "dry-run";
pub const SERVICE_SIGNAL_REFRESH_CONFIG: u64 = 1;
pub const SERVICE_SIGNAL_RELOAD_DYNAMIC: u64 = 2;
const PEER_REST_RECOVERY_MAX_ATTEMPTS: u32 = 60;
const PEER_REST_RECOVERY_MAX_BACKOFF: Duration = Duration::from_secs(30);
#[derive(Clone, Debug)]
pub struct PeerLiveEventsBatch {
pub events: Vec<u8>,
pub next_sequence: u64,
pub truncated: bool,
}
#[derive(Clone, Debug)]
pub struct PeerRestClient {
pub host: XHost,
pub grid_host: String,
offline: Arc<AtomicBool>,
recovery_running: Arc<AtomicBool>,
}
impl PeerRestClient {
fn recovery_monitor_span(grid_host: &str) -> tracing::Span {
tracing::info_span!(
"recovery-monitor",
component = "ecstore",
subsystem = "peer_rest_client",
kind = "peer_rest",
grid_host = %grid_host
)
}
pub fn new(host: XHost, grid_host: String) -> Self {
Self {
host,
grid_host,
offline: Arc::new(AtomicBool::new(false)),
recovery_running: Arc::new(AtomicBool::new(false)),
}
}
pub async fn new_clients(eps: EndpointServerPools) -> (Vec<Option<Self>>, Vec<Option<Self>>) {
if !is_dist_erasure().await {
return (Vec::new(), Vec::new());
}
let eps = eps.clone();
let hosts = eps.hosts_sorted();
let mut remote = Vec::with_capacity(hosts.len());
let mut all = vec![None; hosts.len()];
for (i, hs_host) in hosts.iter().enumerate() {
if let Some(host) = hs_host
&& let Some(grid_host) = eps.find_grid_hosts_from_peer(host)
{
let client = PeerRestClient::new(host.clone(), grid_host);
all[i] = Some(client.clone());
remote.push(Some(client));
}
}
if all.len() != remote.len() + 1 {
warn!("Expected number of all hosts ({}) to be remote +1 ({})", all.len(), remote.len());
}
(remote, all)
}
pub async fn get_client(&self) -> Result<NodeServiceClient<InterceptedService<Channel, TonicInterceptor>>> {
if self.offline.load(Ordering::Acquire) {
self.mark_offline_and_spawn_recovery();
return Err(Error::other(format!("peer {} is temporarily offline", self.grid_host)));
}
node_service_time_out_client(&self.grid_host, TonicInterceptor::Signature(gen_tonic_signature_interceptor()))
.await
.map_err(|err| {
let storage_err = Error::other(format!("can not get client, err: {err}"));
if Self::is_network_like_error(&storage_err) {
self.mark_offline_and_spawn_recovery();
}
storage_err
})
}
/// Evict the connection to this peer from the global cache.
/// This should be called when communication with this peer fails.
pub async fn evict_connection(&self) {
evict_failed_connection(&self.grid_host).await;
}
fn is_network_like_error(err: &Error) -> bool {
let message = err.to_string().to_ascii_lowercase();
[
"temporarily offline",
"transport error",
"unavailable",
"error trying to connect",
"connection refused",
"connection reset",
"broken pipe",
"not connected",
"unexpected eof",
"timed out",
"deadline has elapsed",
"connection closed",
"connection aborted",
"tcp connect error",
]
.iter()
.any(|needle| message.contains(needle))
}
fn mark_offline_and_spawn_recovery(&self) {
self.offline.store(true, Ordering::Release);
if self
.recovery_running
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
.is_err()
{
return;
}
let grid_host = self.grid_host.clone();
let offline = Arc::clone(&self.offline);
let recovery_running = Arc::clone(&self.recovery_running);
let span = Self::recovery_monitor_span(&grid_host);
tokio::spawn(
async move {
let mut delay = get_drive_active_check_interval();
let connect_timeout = get_drive_active_check_timeout();
for _ in 0..PEER_REST_RECOVERY_MAX_ATTEMPTS {
tokio::time::sleep(delay).await;
if Self::perform_connectivity_check(&grid_host, connect_timeout).await.is_ok() {
offline.store(false, Ordering::Release);
recovery_running.store(false, Ordering::Release);
return;
}
delay = std::cmp::min(delay.saturating_mul(2), PEER_REST_RECOVERY_MAX_BACKOFF);
}
warn!(
grid_host = %grid_host,
attempts = PEER_REST_RECOVERY_MAX_ATTEMPTS,
"peer recovery monitor reached max attempts; will retry on next request"
);
recovery_running.store(false, Ordering::Release);
}
.instrument(span),
);
}
#[cfg(test)]
fn spawn_recovery_monitor_log_probe_for_test(&self) -> tokio::sync::oneshot::Receiver<()> {
let (tx, rx) = tokio::sync::oneshot::channel();
let grid_host = self.grid_host.clone();
let span = Self::recovery_monitor_span(&grid_host);
tokio::spawn(
async move {
warn!(grid_host = %grid_host, "peer recovery monitor log probe");
let _ = tx.send(());
}
.instrument(span),
);
rx
}
async fn perform_connectivity_check(addr: &str, timeout_duration: Duration) -> Result<()> {
let url = url::Url::parse(addr).map_err(|e| Error::other(format!("Invalid URL: {e}")))?;
let Some(host) = url.host_str() else {
return Err(Error::other("No host in URL".to_string()));
};
let port = url.port_or_known_default().unwrap_or(80);
match tokio::time::timeout(timeout_duration, TcpStream::connect((host, port))).await {
Ok(Ok(stream)) => {
drop(stream);
Ok(())
}
_ => Err(Error::other(format!("Cannot connect to {host}:{port}"))),
}
}
async fn finalize_result<T>(&self, result: Result<T>) -> Result<T> {
if let Err(err) = &result
&& Self::is_network_like_error(err)
{
self.mark_offline_and_spawn_recovery();
self.evict_connection().await;
}
result
}
}
impl PeerRestClient {
pub async fn local_storage_info(&self) -> Result<rustfs_madmin::StorageInfo> {
self.finalize_result(self.local_storage_info_inner().await).await
}
async fn local_storage_info_inner(&self) -> Result<rustfs_madmin::StorageInfo> {
let mut client = self.get_client().await?;
let request = Request::new(LocalStorageInfoRequest { metrics: true });
let response = client.local_storage_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.storage_info;
let mut buf = Deserializer::new(Cursor::new(data));
let storage_info: rustfs_madmin::StorageInfo = Deserialize::deserialize(&mut buf)?;
Ok(storage_info)
}
pub async fn server_info(&self) -> Result<ServerProperties> {
self.finalize_result(self.server_info_inner().await).await
}
async fn server_info_inner(&self) -> Result<ServerProperties> {
let mut client = self.get_client().await?;
let request = Request::new(ServerInfoRequest { metrics: true });
let response = client.server_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.server_properties;
let mut buf = Deserializer::new(Cursor::new(data));
let storage_properties: ServerProperties = Deserialize::deserialize(&mut buf)?;
Ok(storage_properties)
}
pub async fn get_cpus(&self) -> Result<Cpus> {
self.finalize_result(self.get_cpus_inner().await).await
}
async fn get_cpus_inner(&self) -> Result<Cpus> {
let mut client = self.get_client().await?;
let request = Request::new(GetCpusRequest {});
let response = client.get_cpus(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.cpus;
let mut buf = Deserializer::new(Cursor::new(data));
let cpus: Cpus = Deserialize::deserialize(&mut buf)?;
Ok(cpus)
}
pub async fn get_net_info(&self) -> Result<NetInfo> {
self.finalize_result(self.get_net_info_inner().await).await
}
async fn get_net_info_inner(&self) -> Result<NetInfo> {
let mut client = self.get_client().await?;
let request = Request::new(GetNetInfoRequest {});
let response = client.get_net_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.net_info;
let mut buf = Deserializer::new(Cursor::new(data));
let net_info: NetInfo = Deserialize::deserialize(&mut buf)?;
Ok(net_info)
}
pub async fn get_partitions(&self) -> Result<Partitions> {
self.finalize_result(self.get_partitions_inner().await).await
}
async fn get_partitions_inner(&self) -> Result<Partitions> {
let mut client = self.get_client().await?;
let request = Request::new(GetPartitionsRequest {});
let response = client.get_partitions(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.partitions;
let mut buf = Deserializer::new(Cursor::new(data));
let partitions: Partitions = Deserialize::deserialize(&mut buf)?;
Ok(partitions)
}
pub async fn get_os_info(&self) -> Result<OsInfo> {
self.finalize_result(self.get_os_info_inner().await).await
}
async fn get_os_info_inner(&self) -> Result<OsInfo> {
let mut client = self.get_client().await?;
let request = Request::new(GetOsInfoRequest {});
let response = client.get_os_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.os_info;
let mut buf = Deserializer::new(Cursor::new(data));
let os_info: OsInfo = Deserialize::deserialize(&mut buf)?;
Ok(os_info)
}
pub async fn get_se_linux_info(&self) -> Result<SysServices> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetSeLinuxInfoRequest {});
let response = client.get_se_linux_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.sys_services;
let mut buf = Deserializer::new(Cursor::new(data));
let sys_services: SysServices = Deserialize::deserialize(&mut buf)?;
Ok(sys_services)
}
.await,
)
.await
}
pub async fn get_sys_config(&self) -> Result<SysConfig> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetSysConfigRequest {});
let response = client.get_sys_config(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.sys_config;
let mut buf = Deserializer::new(Cursor::new(data));
let sys_config: SysConfig = Deserialize::deserialize(&mut buf)?;
Ok(sys_config)
}
.await,
)
.await
}
pub async fn get_sys_errors(&self) -> Result<SysErrors> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetSysErrorsRequest {});
let response = client.get_sys_errors(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.sys_errors;
let mut buf = Deserializer::new(Cursor::new(data));
let sys_errors: SysErrors = Deserialize::deserialize(&mut buf)?;
Ok(sys_errors)
}
.await,
)
.await
}
pub async fn get_mem_info(&self) -> Result<MemInfo> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetMemInfoRequest {});
let response = client.get_mem_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.mem_info;
let mut buf = Deserializer::new(Cursor::new(data));
let mem_info: MemInfo = Deserialize::deserialize(&mut buf)?;
Ok(mem_info)
}
.await,
)
.await
}
pub async fn get_metrics(&self, t: MetricType, opts: &CollectMetricsOpts) -> Result<RealtimeMetrics> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let mut buf_t = Vec::new();
t.serialize(&mut Serializer::new(&mut buf_t))?;
let mut buf_o = Vec::new();
opts.serialize(&mut Serializer::new(&mut buf_o))?;
let request = Request::new(GetMetricsRequest {
metric_type: buf_t.into(),
opts: buf_o.into(),
});
let response = client.get_metrics(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.realtime_metrics;
let mut buf = Deserializer::new(Cursor::new(data));
let realtime_metrics: RealtimeMetrics = Deserialize::deserialize(&mut buf)?;
Ok(realtime_metrics)
}
.await,
)
.await
}
pub async fn get_live_events(&self, after_sequence: u64, limit: u32) -> Result<PeerLiveEventsBatch> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetLiveEventsRequest { after_sequence, limit });
let response = client.get_live_events(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(PeerLiveEventsBatch {
events: response.events.to_vec(),
next_sequence: response.next_sequence,
truncated: response.truncated,
})
}
.await,
)
.await
}
pub async fn get_proc_info(&self) -> Result<ProcInfo> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(GetProcInfoRequest {});
let response = client.get_proc_info(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
let data = response.proc_info;
let mut buf = Deserializer::new(Cursor::new(data));
let proc_info: ProcInfo = Deserialize::deserialize(&mut buf)?;
Ok(proc_info)
}
.await,
)
.await
}
pub async fn start_profiling(&self, profiler: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(StartProfilingRequest {
profiler: profiler.to_string(),
});
let response = client.start_profiling(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn download_profile_data(&self) -> Result<()> {
warn!("download_profile_data is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn get_bucket_stats(&self) -> Result<()> {
warn!("get_bucket_stats is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn get_sr_metrics(&self) -> Result<()> {
warn!("get_sr_metrics is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn get_all_bucket_stats(&self) -> Result<()> {
warn!("get_all_bucket_stats is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn load_bucket_metadata(&self, bucket: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadBucketMetadataRequest {
bucket: bucket.to_string(),
});
let response = client.load_bucket_metadata(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn delete_bucket_metadata(&self, bucket: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(DeleteBucketMetadataRequest {
bucket: bucket.to_string(),
});
let response = client.delete_bucket_metadata(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn delete_policy(&self, policy: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(DeletePolicyRequest {
policy_name: policy.to_string(),
});
let response = client.delete_policy(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_policy(&self, policy: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadPolicyRequest {
policy_name: policy.to_string(),
});
let response = client.load_policy(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_policy_mapping(&self, user_or_group: &str, user_type: u64, is_group: bool) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadPolicyMappingRequest {
user_or_group: user_or_group.to_string(),
user_type,
is_group,
});
let response = client.load_policy_mapping(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn delete_user(&self, access_key: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(DeleteUserRequest {
access_key: access_key.to_string(),
});
let response = client.delete_user(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn delete_service_account(&self, access_key: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(DeleteServiceAccountRequest {
access_key: access_key.to_string(),
});
let response = client.delete_service_account(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_user(&self, access_key: &str, temp: bool) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadUserRequest {
access_key: access_key.to_string(),
temp,
});
let response = client.load_user(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_service_account(&self, access_key: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadServiceAccountRequest {
access_key: access_key.to_string(),
});
let response = client.load_service_account(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_group(&self, group: &str) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadGroupRequest {
group: group.to_string(),
});
let response = client.load_group(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn reload_site_replication_config(&self) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(ReloadSiteReplicationConfigRequest {});
let response = client.reload_site_replication_config(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn signal_service(&self, sig: u64, sub_sys: &str, dry_run: bool, _exec_at: SystemTime) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let mut vars = HashMap::new();
vars.insert(PEER_RESTSIGNAL.to_string(), sig.to_string());
vars.insert(PEER_RESTSUB_SYS.to_string(), sub_sys.to_string());
vars.insert(PEER_RESTDRY_RUN.to_string(), dry_run.to_string());
let request = Request::new(SignalServiceRequest {
vars: Some(Mss { value: vars }),
});
let response = client.signal_service(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn get_metacache_listing(&self) -> Result<()> {
warn!("get_metacache_listing is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn update_metacache_listing(&self) -> Result<()> {
warn!("update_metacache_listing is not implemented in PeerRestClient");
Err(Error::NotImplemented)
}
pub async fn reload_pool_meta(&self) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(ReloadPoolMetaRequest {});
let response = client.reload_pool_meta(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn stop_rebalance(&self, expected_rebalance_id: Option<&str>) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(StopRebalanceRequest {
expected_rebalance_id: expected_rebalance_id.unwrap_or_default().to_string(),
});
let response = client.stop_rebalance(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_rebalance_meta(&self, start_rebalance: bool) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadRebalanceMetaRequest { start_rebalance });
let response = client.load_rebalance_meta(request).await?.into_inner();
debug!(
event = "peer_rebalance_meta",
component = "ecstore",
subsystem = "peer_rest_client",
action = "load_rebalance_meta",
result = "response_received",
peer = %self.grid_host,
success = response.success,
start_rebalance = start_rebalance,
"peer rebalance metadata response"
);
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn start_decommission(&self, pool_indices: Vec<usize>) -> Result<()> {
self.finalize_result(
async {
let pool_indices = pool_indices
.into_iter()
.map(|idx| {
u32::try_from(idx).map_err(|_| Error::other(format!("decommission pool index {idx} exceeds RPC range")))
})
.collect::<Result<Vec<_>>>()?;
let mut client = self.get_client().await?;
let request = Request::new(StartDecommissionRequest { pool_indices });
let response = client.start_decommission(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn decommission_cancel(&self, pool_index: usize) -> Result<()> {
self.finalize_result(
async {
let pool_index = u32::try_from(pool_index)
.map_err(|_| Error::other(format!("decommission pool index {pool_index} exceeds RPC range")))?;
let mut client = self.get_client().await?;
let request = Request::new(CancelDecommissionRequest { pool_index });
let response = client.cancel_decommission(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn clear_decommission(&self, pool_index: usize) -> Result<()> {
self.finalize_result(
async {
let pool_index = u32::try_from(pool_index)
.map_err(|_| Error::other(format!("decommission pool index {pool_index} exceeds RPC range")))?;
let mut client = self.get_client().await?;
let request = Request::new(ClearDecommissionRequest { pool_index });
let response = client.clear_decommission(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
pub async fn load_transition_tier_config(&self) -> Result<()> {
self.finalize_result(
async {
let mut client = self.get_client().await?;
let request = Request::new(LoadTransitionTierConfigRequest {});
let response = client.load_transition_tier_config(request).await?.into_inner();
if !response.success {
if let Some(msg) = response.error_info {
return Err(Error::other(msg));
}
return Err(Error::other(""));
}
Ok(())
}
.await,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use tracing_subscriber::{Registry, fmt::MakeWriter, layer::SubscriberExt};
#[derive(Clone, Default)]
struct CapturedLogs {
buffer: Arc<Mutex<Vec<u8>>>,
}
struct CapturedLogWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl CapturedLogs {
fn lines(&self) -> Vec<Value> {
let buffer = self
.buffer
.lock()
.expect("captured logs mutex should not be poisoned")
.clone();
String::from_utf8(buffer)
.expect("captured logs should be valid UTF-8")
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("captured log line should be valid JSON"))
.collect()
}
}
impl Write for CapturedLogWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer
.lock()
.expect("captured logs mutex should not be poisoned")
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for CapturedLogs {
type Writer = CapturedLogWriter;
fn make_writer(&'a self) -> Self::Writer {
CapturedLogWriter {
buffer: Arc::clone(&self.buffer),
}
}
}
fn test_peer_client() -> PeerRestClient {
PeerRestClient::new(
XHost {
name: "127.0.0.1".to_string(),
port: 9000,
is_port_set: true,
},
"http://127.0.0.1:9000".to_string(),
)
}
#[test]
fn peer_rest_client_marks_network_like_errors() {
assert!(PeerRestClient::is_network_like_error(&Error::other("transport error")));
assert!(PeerRestClient::is_network_like_error(&Error::other("connection refused")));
assert!(!PeerRestClient::is_network_like_error(&Error::NotImplemented));
}
#[tokio::test]
async fn peer_rest_client_fast_fails_when_marked_offline() {
let client = test_peer_client();
client.offline.store(true, Ordering::Release);
let err = client
.get_client()
.await
.expect_err("offline peer should fast-fail before dialing");
assert!(err.to_string().contains("temporarily offline"));
}
#[tokio::test]
async fn peer_rest_client_finalize_result_marks_offline_for_network_errors() {
let client = test_peer_client();
let err = client
.finalize_result::<()>(Err(Error::other("transport error")))
.await
.expect_err("network error should still be returned");
assert!(err.to_string().contains("transport error"));
assert!(client.offline.load(Ordering::Acquire));
}
#[tokio::test]
async fn peer_rest_client_finalize_result_keeps_online_for_business_errors() {
let client = test_peer_client();
let err = client
.finalize_result::<()>(Err(Error::VolumeNotFound))
.await
.expect_err("business error should still be returned");
assert!(matches!(err, Error::VolumeNotFound));
assert!(!client.offline.load(Ordering::Acquire));
}
#[tokio::test(flavor = "current_thread")]
async fn peer_rest_recovery_probe_logs_keep_request_id_span_context() {
let logs = CapturedLogs::default();
let subscriber = Registry::default().with(
tracing_subscriber::fmt::layer()
.with_writer(logs.clone())
.with_ansi(false)
.without_time()
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(true),
);
let _guard = tracing::subscriber::set_default(subscriber);
let client = test_peer_client();
let span = tracing::info_span!("request-span", request_id = "req-peer-rest");
let _entered = span.enter();
let done = client.spawn_recovery_monitor_log_probe_for_test();
done.await.expect("recovery monitor probe should signal completion");
let log = logs
.lines()
.into_iter()
.find(|value| value.get("message").and_then(Value::as_str) == Some("peer recovery monitor log probe"))
.expect("expected peer recovery monitor probe log");
assert_eq!(log["span"]["name"], Value::String("recovery-monitor".to_string()));
assert_eq!(log["span"]["kind"], Value::String("peer_rest".to_string()));
let spans = log["spans"].as_array().expect("spans should be present");
assert!(spans.iter().any(|span| {
span.get("name").and_then(Value::as_str) == Some("request-span")
&& span.get("request_id").and_then(Value::as_str) == Some("req-peer-rest")
}));
}
}