use std::{sync::Arc, time::Duration};
use async_trait::async_trait;
use pingora::{
lb::{selection::RoundRobin, LoadBalancer},
server::{ListenFds, ShutdownWatch},
services::Service,
};
use crate::stores::{self};
pub struct HealthService {}
impl HealthService {
pub fn new() -> Self {
Self {}
}
}
const RESOLVE_INTERVAL: Duration = Duration::from_secs(5);
fn load_balancers() -> Vec<(String, Arc<LoadBalancer<RoundRobin>>)> {
stores::get_routes()
.iter()
.flat_map(|(host, container)| {
container
.locations
.iter()
.map(|location| (host.clone(), location.load_balancer.clone()))
.collect::<Vec<_>>()
})
.chain(crate::tcp::load_balancers().iter().cloned())
.collect()
}
async fn run_health_check_loop() {
let mut interval = tokio::time::interval(Duration::from_secs(30));
interval.tick().await;
loop {
interval.tick().await;
for (host, load_balancer) in load_balancers() {
tracing::trace!("Running health check for host {}", host);
load_balancer.backends().run_health_check(false).await;
}
record_health();
}
}
async fn run_resolve_loop() {
let mut interval = tokio::time::interval(RESOLVE_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
interval.tick().await;
for (_, load_balancer) in load_balancers() {
load_balancer.update().await.ok();
}
}
}
fn record_health() {
for (_, host) in &stores::get_routes() {
for location in &host.locations {
let Some(metrics) = &location.metrics else {
continue;
};
for (index, healthy) in location.upstream_health().into_iter().enumerate() {
if let Some(upstream) = metrics.upstream(index) {
upstream.set_healthy(healthy);
}
}
}
}
}
#[async_trait]
impl Service for HealthService {
async fn start_service(
&mut self,
_fds: Option<ListenFds>,
_shutdown: ShutdownWatch,
_listeners_per_fd: usize,
) {
tracing::info!("Starting health check service");
tokio::join!(run_health_check_loop(), run_resolve_loop());
}
fn name(&self) -> &'static str {
"health_check_service"
}
fn threads(&self) -> Option<usize> {
Some(1)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
routing::{build::build_host_container, test_utils::host},
stores,
};
#[test]
fn test_upstream_health_metrics() {
crate::metrics::enable();
let container = build_host_container(&host("health-metrics.test", "10.0.0.9")).unwrap();
let location = container.locations[0].clone();
stores::insert_route("health-metrics.test".to_string(), Arc::new(container));
let healthy = || {
crate::metrics::gather().contains(
"proksi_upstream_healthy{host=\"health-metrics.test\",location=\"/*\",upstream=\"10.0.0.9:3000\"} 1",
)
};
assert!(healthy());
let backends = location.load_balancer.backends();
let backend = backends.get_backend().iter().next().unwrap().clone();
backends.set_enable(&backend, false);
super::record_health();
assert!(!healthy());
backends.set_enable(&backend, true);
super::record_health();
assert!(healthy());
stores::remove_route("health-metrics.test");
}
}