use serde::{Deserialize, Serialize};
use snafu::Snafu;
use std::fmt;
use crate::query::QueryHttpClient;
#[derive(Debug, Snafu)]
pub enum StatusError {
#[snafu(display("HTTP endpoint not configured. Use ClientBuilder::http_url() to set it."))]
HttpNotConfigured,
#[snafu(display("Failed to query {url} (HTTP {status_code}): {response_body}"))]
RequestFailed {
url: String,
status_code: u16,
response_body: String,
},
#[snafu(display("Failed to query {url}: {message}"))]
HttpError {
url: String,
message: String,
},
#[snafu(display("Failed to parse runtime status response: {message}"))]
ParseError {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentStatus {
Initializing,
Ready,
Disabled,
Error,
Refreshing,
ShuttingDown,
NotLoaded,
#[serde(untagged)]
Other(String),
}
impl ComponentStatus {
#[must_use]
pub fn is_ready(&self) -> bool {
matches!(self, ComponentStatus::Ready)
}
#[must_use]
pub fn is_error(&self) -> bool {
matches!(self, ComponentStatus::Error)
}
}
impl fmt::Display for ComponentStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ComponentStatus::Initializing => write!(f, "Initializing"),
ComponentStatus::Ready => write!(f, "Ready"),
ComponentStatus::Disabled => write!(f, "Disabled"),
ComponentStatus::Error => write!(f, "Error"),
ComponentStatus::Refreshing => write!(f, "Refreshing"),
ComponentStatus::ShuttingDown => write!(f, "ShuttingDown"),
ComponentStatus::NotLoaded => write!(f, "NotLoaded"),
ComponentStatus::Other(status) => write!(f, "{status}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnectionDetails {
pub name: String,
pub endpoint: String,
pub status: ComponentStatus,
}
impl ConnectionDetails {
#[must_use]
pub fn is_ready(&self) -> bool {
self.status.is_ready()
}
}
impl QueryHttpClient {
pub(crate) async fn runtime_status(&self) -> Result<Vec<ConnectionDetails>, StatusError> {
let url = format!("{}/v1/status", self.base_url());
let response = self
.authorized(self.client().get(&url))
.send()
.await
.map_err(|e| StatusError::HttpError {
url: url.clone(),
message: e.to_string(),
})?;
match response.status().as_u16() {
200 => response.json().await.map_err(|e| StatusError::ParseError {
message: e.to_string(),
}),
status_code => {
let response_body = response.text().await.unwrap_or_default();
Err(StatusError::RequestFailed {
url,
status_code,
response_body,
})
}
}
}
pub(crate) async fn is_ready(&self) -> Result<bool, StatusError> {
let url = format!("{}/v1/ready", self.base_url());
let response = self
.authorized(self.client().get(&url))
.send()
.await
.map_err(|e| StatusError::HttpError {
url: url.clone(),
message: e.to_string(),
})?;
match response.status().as_u16() {
200 => Ok(true),
503 => Ok(false),
status_code => {
let response_body = response.text().await.unwrap_or_default();
Err(StatusError::RequestFailed {
url,
status_code,
response_body,
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_statuses_round_trip() {
for (json, expected) in [
("\"Initializing\"", ComponentStatus::Initializing),
("\"Ready\"", ComponentStatus::Ready),
("\"Disabled\"", ComponentStatus::Disabled),
("\"Error\"", ComponentStatus::Error),
("\"Refreshing\"", ComponentStatus::Refreshing),
("\"ShuttingDown\"", ComponentStatus::ShuttingDown),
("\"NotLoaded\"", ComponentStatus::NotLoaded),
] {
let parsed: ComponentStatus =
serde_json::from_str(json).expect("deserialize component status");
assert_eq!(parsed, expected);
assert_eq!(
serde_json::to_string(&parsed).expect("serialize component status"),
json
);
}
}
#[test]
fn unknown_status_is_preserved() {
let parsed: ComponentStatus =
serde_json::from_str("\"SomethingNew\"").expect("deserialize unknown status");
assert_eq!(parsed, ComponentStatus::Other("SomethingNew".to_string()));
assert_eq!(parsed.to_string(), "SomethingNew");
assert!(!parsed.is_ready());
}
#[test]
fn status_predicates() {
assert!(ComponentStatus::Ready.is_ready());
assert!(!ComponentStatus::Initializing.is_ready());
assert!(ComponentStatus::Error.is_error());
assert!(!ComponentStatus::Ready.is_error());
}
#[test]
fn connection_details_deserialize() {
let body = r#"[
{"name":"http","endpoint":"127.0.0.1:8090","status":"Ready"},
{"name":"flight","endpoint":"127.0.0.1:50051","status":"Initializing"},
{"name":"metrics","endpoint":"N/A","status":"Disabled"}
]"#;
let details: Vec<ConnectionDetails> =
serde_json::from_str(body).expect("deserialize connection details");
assert_eq!(details.len(), 3);
assert_eq!(details[0].name, "http");
assert!(details[0].is_ready());
assert_eq!(details[1].status, ComponentStatus::Initializing);
assert!(!details[1].is_ready());
assert_eq!(details[2].endpoint, "N/A");
}
#[test]
fn request_errors_name_the_endpoint_that_failed() {
let request_failed = StatusError::RequestFailed {
url: "http://localhost:8090/v1/ready".to_string(),
status_code: 401,
response_body: "unauthorized".to_string(),
};
assert_eq!(
request_failed.to_string(),
"Failed to query http://localhost:8090/v1/ready (HTTP 401): unauthorized"
);
let http_error = StatusError::HttpError {
url: "http://localhost:8090/v1/status".to_string(),
message: "connection refused".to_string(),
};
assert_eq!(
http_error.to_string(),
"Failed to query http://localhost:8090/v1/status: connection refused"
);
}
}