use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
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(1));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut checked_at = HashMap::<usize, Instant>::new();
loop {
interval.tick().await;
let load_balancers = load_balancers();
checked_at.retain(|id, _| {
load_balancers
.iter()
.any(|(_, load_balancer)| Arc::as_ptr(load_balancer) as usize == *id)
});
for (host, load_balancer) in load_balancers {
let Some(frequency) = load_balancer.health_check_frequency else {
continue;
};
let last = checked_at.entry(Arc::as_ptr(&load_balancer) as usize);
let last = last.or_insert_with(Instant::now);
if last.elapsed() < frequency {
continue;
}
*last = Instant::now();
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");
}
}