use std::{sync::Arc, time::Duration};
use anyhow::{anyhow, Result};
use tokio::{sync::watch, time::Instant};
use tokio_stream::StreamExt;
use tonic::{Code, Request};
use zelos_proto::trace::{
trace_publish_client::TracePublishClient as GrpcClient, PublishRequest, PublishStatus,
};
use zelos_trace::TraceRouter;
use crate::connection_status::ConnectionStatus;
const DEFAULT_BATCH_SIZE: usize = 1000;
const DEFAULT_BATCH_TIMEOUT_MS: u64 = 100;
const DEFAULT_URL: &str = "grpc://localhost:2300";
const DEFAULT_RECONNECT_DELAY_MS: u64 = 1000;
#[derive(Debug, Clone)]
pub struct TracePublishClientConfig {
pub url: String,
pub batch_size: usize,
pub batch_timeout: Duration,
pub reconnect_delay: Duration,
}
impl TracePublishClientConfig {
pub fn new_with_url(url: String) -> Self {
Self {
url,
..Default::default()
}
}
}
impl Default for TracePublishClientConfig {
fn default() -> Self {
Self {
url: DEFAULT_URL.to_string(),
batch_size: DEFAULT_BATCH_SIZE,
batch_timeout: Duration::from_millis(DEFAULT_BATCH_TIMEOUT_MS),
reconnect_delay: Duration::from_millis(DEFAULT_RECONNECT_DELAY_MS),
}
}
}
pub struct TracePublishClient {
pub config: TracePublishClientConfig,
connection_status: watch::Receiver<ConnectionStatus>,
publish_status: watch::Receiver<Option<PublishStatus>>,
}
impl TracePublishClient {
pub fn new(
router: Arc<TraceRouter>,
config: TracePublishClientConfig,
) -> (Self, impl Future<Output = Result<()>>) {
let (tx_connection_status, connection_status) =
watch::channel(ConnectionStatus::Disconnected);
let (tx_publish_status, publish_status) = watch::channel(None);
let client = Self {
config: config.clone(),
connection_status,
publish_status,
};
let task = Self::run(router, config, tx_publish_status, tx_connection_status);
(client, task)
}
pub fn new_with_default_config(
router: Arc<TraceRouter>,
) -> (Self, impl Future<Output = Result<()>>) {
Self::new(router, TracePublishClientConfig::default())
}
async fn connect(
router: Arc<TraceRouter>,
config: TracePublishClientConfig,
tx_publish_status: &watch::Sender<Option<PublishStatus>>,
tx_connection_status: &watch::Sender<ConnectionStatus>,
) -> Result<()> {
tracing::info!("Trace client connecting to {}", &config.url);
let channel = zelos_proto::channel::create_channel(config.url.clone())
.map_err(|e| anyhow!("Failed to connect to publish service: {}", e))?;
let mut client = GrpcClient::new(channel)
.max_decoding_message_size(zelos_proto::MAX_GRPC_MESSAGE_SIZE)
.max_encoding_message_size(zelos_proto::MAX_GRPC_MESSAGE_SIZE);
let stream = router
.subscribe_all_blocking_stream()
.await?
.chunks_timeout(config.batch_size, config.batch_timeout)
.map(|m| PublishRequest {
trace_messages: m.into_iter().map(|msg| msg.into()).collect(),
});
let response = client
.publish(Request::new(stream))
.await
.map_err(|e| anyhow!("Failed to establish publish stream: {e}"))?;
tracing::debug!("Successfully established new gRPC publish stream.");
tx_connection_status.send(ConnectionStatus::Connected)?;
let mut response_stream = response.into_inner();
loop {
match response_stream.message().await {
Ok(Some(resp)) => {
if let Some(status) = resp.status {
tx_publish_status.send(Some(status))?;
}
tracing::trace!("Publish status: {:?}", tx_publish_status);
}
Ok(None) => {
return Ok(());
}
Err(status) => {
return match status.code() {
Code::Ok => Ok(()),
_ => Err(anyhow!("Received error status: {}", status)),
};
}
}
}
}
async fn run(
router: Arc<TraceRouter>,
config: TracePublishClientConfig,
tx_publish_status: watch::Sender<Option<PublishStatus>>,
tx_connection_status: watch::Sender<ConnectionStatus>,
) -> Result<()> {
let mut last_connection_time: Instant;
loop {
last_connection_time = Instant::now();
tx_connection_status.send(ConnectionStatus::Connecting)?;
if let Err(e) = Self::connect(
router.clone(),
config.clone(),
&tx_publish_status,
&tx_connection_status,
)
.await
{
tracing::error!("Error forwarding trace events: {}", e);
tx_connection_status.send(ConnectionStatus::Error)?;
} else {
tx_connection_status.send(ConnectionStatus::Disconnected)?;
}
let elapsed = last_connection_time.elapsed();
if elapsed < config.reconnect_delay {
let remaining = config.reconnect_delay - elapsed;
tokio::time::sleep(remaining).await;
}
}
}
pub async fn connection_status(&self) -> watch::Receiver<ConnectionStatus> {
self.connection_status.clone()
}
pub async fn last_connection_status(&self) -> ConnectionStatus {
*self.connection_status.borrow()
}
pub async fn wait_until_connected(&self, timeout: Duration) -> Result<()> {
let mut connection_status = self.connection_status.clone();
tokio::time::timeout(timeout, async move {
loop {
let status = *connection_status.borrow_and_update();
if status == ConnectionStatus::Connected {
return Ok(()); }
connection_status.changed().await?;
}
})
.await .map_err(|_| {
anyhow!(
"Timed out waiting for connection to become connected within {:?}",
timeout
)
})?
}
pub async fn publish_status(&self) -> watch::Receiver<Option<PublishStatus>> {
self.publish_status.clone()
}
pub async fn last_publish_status(&self) -> Option<PublishStatus> {
self.publish_status.borrow().clone()
}
}