use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::RwLock;
use tonic::service::interceptor::InterceptedService;
use tonic::transport::Channel;
use tracing::{debug, warn};
use crate::auth::{ChannelAuthenticator, ChannelIdInterceptor, SaslStreamGuard};
use crate::client::master_inquire::MasterInquireClient;
use crate::config::GoosefsConfig;
use crate::error::{Error, Result};
use crate::proto::grpc::metric::{
metrics_master_client_service_client::MetricsMasterClientServiceClient, ClearMetricsPRequest,
ClientMetrics, GetMetricsPOptions, MetricValue, MetricsHeartbeatPOptions,
MetricsHeartbeatPRequest,
};
type AuthenticatedMetricsClient =
MetricsMasterClientServiceClient<InterceptedService<Channel, ChannelIdInterceptor>>;
const MAX_RPC_RETRIES: u32 = 2;
#[async_trait]
pub trait MetricsClient: Send + Sync {
async fn heartbeat(&self, client_metrics: Vec<ClientMetrics>) -> crate::error::Result<()>;
}
pub(crate) struct MetricsMasterClient {
inner: Arc<RwLock<AuthenticatedMetricsClient>>,
config: GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
_sasl_guard: Arc<RwLock<Option<SaslStreamGuard>>>,
}
impl MetricsMasterClient {
pub async fn connect_with_inquire(
config: &GoosefsConfig,
inquire_client: Arc<dyn MasterInquireClient>,
) -> Result<Self> {
let primary_addr = inquire_client.get_primary_rpc_address().await?;
let (client, sasl_guard) = Self::build_authenticated_client(config, &primary_addr).await?;
debug!(
addr = %primary_addr,
auth_type = %config.auth_type,
"connected to Goosefs MetricsMaster"
);
Ok(Self {
inner: Arc::new(RwLock::new(client)),
config: config.clone(),
inquire_client,
_sasl_guard: Arc::new(RwLock::new(sasl_guard)),
})
}
async fn build_authenticated_client(
config: &GoosefsConfig,
addr: &str,
) -> Result<(AuthenticatedMetricsClient, Option<SaslStreamGuard>)> {
let channel = Self::build_raw_channel(config, addr).await?;
let authenticator = ChannelAuthenticator::new(
config.auth_type,
config.auth_username.clone(),
None, )
.with_auth_timeout(config.auth_timeout);
let mut auth_channel = authenticator.authenticate(channel).await?;
let sasl_guard = auth_channel.take_sasl_guard();
Ok((
MetricsMasterClientServiceClient::new(auth_channel.channel),
sasl_guard,
))
}
async fn build_raw_channel(config: &GoosefsConfig, addr: &str) -> Result<Channel> {
let endpoint_uri = format!("http://{}", addr);
let endpoint = Channel::from_shared(endpoint_uri)
.map_err(|e| Error::ConfigError {
message: format!("invalid metrics master endpoint: {}", e),
})?
.connect_timeout(config.connect_timeout)
.timeout(config.request_timeout);
let channel = endpoint.connect().await?;
Ok(channel)
}
async fn reconnect(&self) -> Result<()> {
self.inquire_client.reset_cached_primary().await;
let primary_addr = self.inquire_client.get_primary_rpc_address().await?;
let (client, sasl_guard) =
Self::build_authenticated_client(&self.config, &primary_addr).await?;
*self.inner.write().await = client;
*self._sasl_guard.write().await = sasl_guard;
debug!(
addr = %primary_addr,
"reconnected to Goosefs MetricsMaster after failover"
);
Ok(())
}
async fn with_retry<F, Fut, T>(&self, op_name: &str, f: F) -> Result<T>
where
F: Fn(AuthenticatedMetricsClient) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut last_err: Option<Error> = None;
for attempt in 0..=MAX_RPC_RETRIES {
if attempt > 0 {
if let Err(reconnect_err) = self.reconnect().await {
warn!(
op = op_name,
attempt = attempt + 1,
error = %reconnect_err,
"metrics master reconnect failed; will retry reconnect on next attempt"
);
last_err = Some(Error::Internal {
message: format!("metrics master reconnect failed: {}", reconnect_err),
source: None,
});
continue;
}
}
let client: AuthenticatedMetricsClient = self.inner.read().await.clone();
match f(client).await {
Ok(result) => return Ok(result),
Err(err) => {
if err.is_retriable() && attempt < MAX_RPC_RETRIES {
warn!(
op = op_name,
attempt = attempt + 1,
max = MAX_RPC_RETRIES,
error = %err,
"retriable error on metrics RPC; will reconnect and retry"
);
last_err = Some(err);
} else {
return Err(err);
}
}
}
}
Err(last_err.unwrap_or_else(|| Error::Internal {
message: format!("{}: exhausted all retries", op_name),
source: None,
}))
}
pub async fn heartbeat(&self, client_metrics: Vec<ClientMetrics>) -> Result<()> {
let req = MetricsHeartbeatPRequest {
options: Some(MetricsHeartbeatPOptions { client_metrics }),
};
self.with_retry("metrics_heartbeat", |mut c| {
let req = req.clone();
async move {
c.metrics_heartbeat(req)
.await
.map(|_| ())
.map_err(Into::into)
}
})
.await
}
#[allow(dead_code)] pub async fn clear_metrics(&self) -> Result<()> {
self.with_retry("clear_metrics", |mut c| async move {
c.clear_metrics(ClearMetricsPRequest {})
.await
.map(|_| ())
.map_err(Into::into)
})
.await
}
#[allow(dead_code)] pub async fn get_metrics(&self) -> Result<HashMap<String, MetricValue>> {
self.with_retry("get_metrics", |mut c| async move {
let resp = c.get_metrics(GetMetricsPOptions {}).await?;
Ok(resp.into_inner().metrics)
})
.await
}
}
#[async_trait]
impl MetricsClient for MetricsMasterClient {
async fn heartbeat(&self, client_metrics: Vec<ClientMetrics>) -> crate::error::Result<()> {
MetricsMasterClient::heartbeat(self, client_metrics).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn assert_send_sync()
where
MetricsMasterClient: Send + Sync,
{
}
#[test]
fn heartbeat_request_is_clone() {
let req = MetricsHeartbeatPRequest {
options: Some(MetricsHeartbeatPOptions {
client_metrics: vec![ClientMetrics {
source: Some("test-app".into()),
metrics: vec![],
}],
}),
};
let _req2 = req.clone();
}
#[test]
fn unit_requests_are_copy() {
let _opts = GetMetricsPOptions {};
let _clear = ClearMetricsPRequest {};
}
}