use crate::RuntimeError;
use crate::resource::{MIN_HEALTH_INTERVAL, Resource};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
const HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
async fn probe_url(client: &reqwest::Client, url: &str) -> bool {
client
.get(url)
.timeout(HEALTH_CHECK_TIMEOUT)
.send()
.await
.is_ok_and(|r| r.status().is_success())
}
impl std::fmt::Debug for ProxyHealthResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProxyHealthResource")
.field("name", &self.name)
.field("url", &self.url)
.finish()
}
}
pub struct ProxyHealthResource {
name: Box<str>,
url: Box<str>,
routing_flag: Arc<AtomicBool>,
}
impl ProxyHealthResource {
pub fn new(backend: &str, path: &str) -> Self {
Self {
name: Box::from(backend),
url: format!("{backend}{path}").into_boxed_str(),
routing_flag: Arc::new(AtomicBool::new(true)),
}
}
pub fn routing_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.routing_flag)
}
}
impl Resource for ProxyHealthResource {
fn name(&self) -> &str {
&self.name
}
fn health_check(&self) -> Result<(), RuntimeError> {
let client = super::async_proxy::proxy_client()?;
let handle = tokio::runtime::Handle::current();
let url: &str = &self.url;
let ok = handle.block_on(probe_url(client, url));
self.routing_flag.store(ok, Ordering::Release);
match ok {
true => Ok(()),
false => Err(RuntimeError::Http("backend health check failed".into())),
}
}
fn shutdown(&self) -> Result<(), RuntimeError> {
Ok(())
}
}
pub async fn spawn_health_checker(
backend: &str,
path: &str,
interval: Duration,
) -> Result<Arc<AtomicBool>, RuntimeError> {
if interval < MIN_HEALTH_INTERVAL {
return Err(RuntimeError::InvalidArgument(
"health check interval must be at least 1 second".into(),
));
}
let url = format!("{backend}{path}");
let client = super::async_proxy::proxy_client()?.clone();
let initial_ok = probe_url(&client, &url).await;
let healthy = Arc::new(AtomicBool::new(initial_ok));
let flag = Arc::clone(&healthy);
tokio::spawn(async move {
loop {
tokio::time::sleep(interval).await;
let ok = probe_url(&client, &url).await;
flag.store(ok, Ordering::Release);
}
});
Ok(healthy)
}