use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use hyper::Request;
use plecto_control::{Control, HealthConfig, UpstreamInstance};
use crate::upstream_client::{HyperUpstreamClient, UpstreamClient, UpstreamClients};
pub(crate) async fn serve_health_checks(
control: Arc<Control>,
mut drain: tokio::sync::watch::Receiver<bool>,
) {
let clients = UpstreamClients::new();
let mut last: HashMap<(String, String), Instant> = HashMap::new();
loop {
let groups = control.upstream_groups();
let now = Instant::now();
let mut live: HashSet<(String, String)> = HashSet::new();
let mut period = Duration::from_secs(5);
for g in &groups {
let interval = Duration::from_millis(g.health.interval_ms.max(1));
period = period.min(interval);
for inst in &g.endpoints().instances {
let key = (g.name.clone(), inst.address().to_string());
let due = last
.get(&key)
.is_none_or(|t| now.duration_since(*t) >= interval);
if due {
last.insert(key.clone(), now);
let client = clients.for_group(g);
let scheme = g.scheme();
let inst = inst.clone();
let health = g.health.clone();
tokio::spawn(async move { probe_once(&client, scheme, &health, &inst).await });
}
live.insert(key);
}
}
last.retain(|k, _| live.contains(k));
tokio::select! {
_ = crate::listener::drained(&mut drain) => return,
_ = tokio::time::sleep(period.max(Duration::from_millis(20))) => {}
}
}
}
async fn probe_once(
client: &HyperUpstreamClient,
scheme: &str,
health: &HealthConfig,
inst: &UpstreamInstance,
) {
let uri = format!(
"{}://{}{}",
scheme,
probe_address(inst.address(), health.port),
health.path
);
let req = match Request::builder()
.method("GET")
.uri(&uri)
.body(crate::body::empty_req())
{
Ok(req) => req,
Err(_) => {
inst.record_probe_failure();
return;
}
};
let timeout = Duration::from_millis(health.timeout_ms.max(1));
match tokio::time::timeout(timeout, client.request(req)).await {
Ok(Ok(resp)) if resp.status().is_success() => inst.record_probe_success(),
_ => inst.record_probe_failure(),
}
}
fn probe_address(address: &str, port: Option<u16>) -> Cow<'_, str> {
match (port, address.rsplit_once(':')) {
(Some(port), Some((host, _))) => Cow::Owned(format!("{host}:{port}")),
_ => Cow::Borrowed(address),
}
}
#[cfg(test)]
mod tests {
use super::probe_address;
#[test]
fn no_override_keeps_the_traffic_address() {
assert_eq!(probe_address("127.0.0.1:9000", None), "127.0.0.1:9000");
}
#[test]
fn override_swaps_only_the_port() {
assert_eq!(
probe_address("127.0.0.1:9000", Some(9100)),
"127.0.0.1:9100"
);
assert_eq!(
probe_address("upstream.example:9000", Some(9100)),
"upstream.example:9100"
);
}
#[test]
fn malformed_address_falls_back_unchanged_rather_than_panicking() {
assert_eq!(
probe_address("not-a-host-port", Some(9100)),
"not-a-host-port"
);
}
}