use std::time::Duration;
use super::grpc::proto;
use super::types::{QueryRequest, QueryResponse};
#[derive(Debug)]
pub enum ClientError {
Connection(String),
Request(u16, String),
Parse(String),
Timeout,
}
impl std::fmt::Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientError::Connection(msg) => write!(f, "Connection error: {}", msg),
ClientError::Request(status, msg) => write!(f, "Request error ({}): {}", status, msg),
ClientError::Parse(msg) => write!(f, "Parse error: {}", msg),
ClientError::Timeout => write!(f, "Request timed out"),
}
}
}
impl std::error::Error for ClientError {}
fn tonic_status_to_http(code: tonic::Code) -> u16 {
match code {
tonic::Code::Ok => 200,
tonic::Code::InvalidArgument => 400,
tonic::Code::Unauthenticated => 401,
tonic::Code::PermissionDenied => 403,
tonic::Code::NotFound => 404,
tonic::Code::ResourceExhausted => 429,
tonic::Code::Internal => 500,
tonic::Code::Unavailable => 503,
tonic::Code::DeadlineExceeded => 504,
_ => 500,
}
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub base_url: String,
pub http_url: Option<String>,
pub timeout: Duration,
pub api_key: Option<String>,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
base_url: "http://127.0.0.1:9999".to_string(),
http_url: None,
timeout: Duration::from_secs(30),
api_key: None,
}
}
}
impl ClientConfig {
pub fn new(base_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
..Default::default()
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_api_key(mut self, api_key: &str) -> Self {
self.api_key = Some(api_key.to_string());
self
}
pub fn with_http_url(mut self, http_url: &str) -> Self {
self.http_url = Some(http_url.to_string());
self
}
fn http_base_url(&self) -> String {
if let Some(ref url) = self.http_url {
return url.clone();
}
if let Some(colon_pos) = self.base_url.rfind(':') {
let port_str = self
.base_url
.get(colon_pos.saturating_add(1)..)
.unwrap_or("");
if let Ok(port) = port_str.parse::<u16>() {
return format!("http://127.0.0.1:{}", port.saturating_add(1));
}
}
self.base_url.to_string()
}
}
pub struct ProxyClient {
search_client: proto::search_service_client::SearchServiceClient<tonic::transport::Channel>,
admin_client: proto::admin_service_client::AdminServiceClient<tonic::transport::Channel>,
obs_client:
proto::observability_service_client::ObservabilityServiceClient<tonic::transport::Channel>,
http_client: reqwest::Client,
config: ClientConfig,
}
impl ProxyClient {
pub fn new(config: ClientConfig) -> Result<Self, ClientError> {
let endpoint = tonic::transport::Endpoint::from_shared(config.base_url.clone())
.map_err(|e| ClientError::Connection(format!("Invalid endpoint: {}", e)))?
.timeout(config.timeout);
let channel = endpoint.connect_lazy();
let mut search_client =
proto::search_service_client::SearchServiceClient::new(channel.clone());
let mut admin_client =
proto::admin_service_client::AdminServiceClient::new(channel.clone());
let mut obs_client =
proto::observability_service_client::ObservabilityServiceClient::new(channel);
let timeout = config.timeout;
search_client = search_client.max_decoding_message_size(16 * 1024 * 1024);
admin_client = admin_client.max_decoding_message_size(16 * 1024 * 1024);
obs_client = obs_client.max_decoding_message_size(16 * 1024 * 1024);
let http_client = super::socket_tuning::create_tuned_client_default(timeout)
.build()
.map_err(|e| ClientError::Connection(e.to_string()))?;
Ok(Self {
search_client,
admin_client,
obs_client,
http_client,
config,
})
}
pub fn local() -> Result<Self, ClientError> {
Self::new(ClientConfig::default())
}
pub fn connect(base_url: &str) -> Result<Self, ClientError> {
Self::new(ClientConfig::new(base_url))
}
fn make_request<T>(&self, inner: T) -> tonic::Request<T> {
let mut request = tonic::Request::new(inner);
if let Some(ref api_key) = self.config.api_key {
if let Ok(value) = api_key.parse() {
request.metadata_mut().insert("x-api-key", value);
}
}
request
}
pub async fn query(&self, request: &QueryRequest) -> Result<QueryResponse, ClientError> {
let proto_req = proto::QueryRequest::from(request);
let grpc_request = self.make_request(proto_req);
let response = self
.search_client
.clone()
.query(grpc_request)
.await
.map_err(|status| {
let code = tonic_status_to_http(status.code());
ClientError::Request(code, status.message().to_string())
})?;
Ok(QueryResponse::from(response.into_inner()))
}
pub async fn search(&self, query: &str, limit: usize) -> Result<QueryResponse, ClientError> {
let request = QueryRequest {
query: query.to_string(),
top_k: Some(limit),
priority: None,
upstream_id: None,
upstream_type: None,
};
self.query(&request).await
}
pub async fn health(&self) -> Result<bool, ClientError> {
let url = format!(
"{}/health",
self.config.http_base_url().trim_end_matches('/')
);
let response = self
.http_client
.get(&url)
.send()
.await
.map_err(|e| ClientError::Connection(e.to_string()))?;
Ok(response.status().is_success())
}
pub async fn ready(&self) -> Result<bool, ClientError> {
let url = format!(
"{}/ready",
self.config.http_base_url().trim_end_matches('/')
);
let response = self
.http_client
.get(&url)
.send()
.await
.map_err(|e| ClientError::Connection(e.to_string()))?;
Ok(response.status().is_success())
}
pub async fn stats(&self) -> Result<serde_json::Value, ClientError> {
let grpc_request = self.make_request(proto::GetStatsRequest {});
let response = self
.obs_client
.clone()
.get_stats(grpc_request)
.await
.map_err(|status| {
let code = tonic_status_to_http(status.code());
ClientError::Request(code, status.message().to_string())
})?;
let stats = response.into_inner();
let json = serde_json::json!({
"uptime_secs": stats.uptime_secs,
"cache_entries": stats.cache_entries,
"total_hits": stats.total_hits,
"total_misses": stats.total_misses,
"hit_rate": stats.hit_rate,
"upstream_requests": stats.upstream_requests,
"upstream_failures": stats.upstream_failures,
"upstream_error_rate": stats.upstream_error_rate,
"degradation_level": stats.degradation_level,
"paused": stats.paused,
});
Ok(json)
}
pub async fn clear_cache(&self) -> Result<(), ClientError> {
let grpc_request = self.make_request(proto::CacheClearRequest {});
self.admin_client
.clone()
.cache_clear(grpc_request)
.await
.map_err(|status| {
let code = tonic_status_to_http(status.code());
ClientError::Request(code, status.message().to_string())
})?;
Ok(())
}
}
#[cfg(test)]
#[path = "tests/client_tests.rs"]
mod tests;