use std::{
net::SocketAddr,
sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use axum::{Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
use prometheus::{Encoder, IntGaugeVec, TextEncoder, register_int_gauge_vec};
use tokio_util::sync::CancellationToken;
static BUILD_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
let gauge = register_int_gauge_vec!(
"polychrome_build_info",
"Always 1 for a live process; the `version` label carries the build's release version.",
&["version"]
)
.expect("register polychrome_build_info");
gauge.with_label_values(&[env!("CARGO_PKG_VERSION")]).set(1);
gauge
});
const DEFAULT_DRAIN_PROPAGATION_DELAY: Duration = Duration::from_secs(5);
type DrainHook = Arc<dyn Fn() + Send + Sync>;
#[derive(Clone)]
pub struct Health {
ready: Arc<AtomicBool>,
draining: Arc<AtomicBool>,
on_drain: Option<DrainHook>,
drain_propagation_delay: Duration,
}
impl Default for Health {
fn default() -> Self {
Self {
ready: Arc::default(),
draining: Arc::default(),
on_drain: None,
drain_propagation_delay: DEFAULT_DRAIN_PROPAGATION_DELAY,
}
}
}
impl Health {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_ready(&self, ready: bool) {
self.ready.store(ready, Ordering::Relaxed);
}
#[must_use]
pub fn with_on_drain(mut self, hook: DrainHook) -> Self {
self.on_drain = Some(hook);
self
}
#[must_use]
pub const fn with_drain_propagation_delay(mut self, delay: Duration) -> Self {
self.drain_propagation_delay = delay;
self
}
#[must_use]
pub fn is_draining(&self) -> bool {
self.draining.load(Ordering::Relaxed)
}
}
pub async fn serve(
addr: SocketAddr,
health: Health,
shutdown: CancellationToken,
) -> anyhow::Result<()> {
let listener = tokio::net::TcpListener::bind(addr).await?;
serve_on(listener, health, shutdown).await
}
pub async fn serve_on(
listener: tokio::net::TcpListener,
health: Health,
shutdown: CancellationToken,
) -> anyhow::Result<()> {
let app = Router::new()
.route("/healthz", get(|| async { StatusCode::OK }))
.route("/livez", get(|| async { StatusCode::OK }))
.route("/readyz", get(readyz))
.route("/metrics", get(metrics))
.route("/drain", get(drain).post(drain))
.with_state(health);
let addr = listener.local_addr()?;
tracing::info!(%addr, "side-server listening");
axum::serve(listener, app)
.with_graceful_shutdown(async move { shutdown.cancelled().await })
.await?;
Ok(())
}
async fn readyz(State(health): State<Health>) -> impl IntoResponse {
if health.draining.load(Ordering::Relaxed) {
return StatusCode::SERVICE_UNAVAILABLE;
}
if health.ready.load(Ordering::Relaxed) {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
}
}
async fn drain(State(health): State<Health>) -> impl IntoResponse {
let already_draining = health.draining.swap(true, Ordering::SeqCst);
if !already_draining && let Some(hook) = &health.on_drain {
hook();
}
if !health.drain_propagation_delay.is_zero() {
tokio::time::sleep(health.drain_propagation_delay).await;
}
(StatusCode::OK, "draining\n")
}
async fn metrics() -> impl IntoResponse {
LazyLock::force(&BUILD_INFO);
let families = prometheus::gather();
let mut buf = Vec::new();
let encoder = TextEncoder::new();
if encoder.encode(&families, &mut buf).is_err() {
return (StatusCode::INTERNAL_SERVER_ERROR, Vec::new()).into_response();
}
(
[(axum::http::header::CONTENT_TYPE, encoder.format_type())],
buf,
)
.into_response()
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_util::sync::CancellationToken;
use super::{Duration, Health, serve_on};
async fn status_line(addr: std::net::SocketAddr, path: &str) -> String {
method_status_line(addr, "GET", path).await
}
async fn method_status_line(addr: std::net::SocketAddr, method: &str, path: &str) -> String {
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
stream
.write_all(
format!(
"{method} {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
)
.as_bytes(),
)
.await
.unwrap();
let mut buf = String::new();
stream.read_to_string(&mut buf).await.unwrap();
buf.lines().next().unwrap_or_default().to_owned()
}
#[tokio::test]
async fn serve_on_takes_a_prebound_listener_and_readyz_tracks_the_flag() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let health = Health::new();
let shutdown = CancellationToken::new();
let srv = tokio::spawn(serve_on(listener, health.clone(), shutdown.clone()));
assert!(status_line(addr, "/readyz").await.contains("503"));
assert!(status_line(addr, "/livez").await.contains("200"));
health.set_ready(true);
assert!(status_line(addr, "/readyz").await.contains("200"));
health.set_ready(false);
assert!(status_line(addr, "/readyz").await.contains("503"));
shutdown.cancel();
srv.await.unwrap().unwrap();
}
#[tokio::test]
async fn drain_flips_readiness_and_fires_hook_once() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let hook_calls = std::sync::Arc::new(AtomicUsize::new(0));
let health = Health::new()
.with_drain_propagation_delay(Duration::ZERO)
.with_on_drain({
let hook_calls = hook_calls.clone();
std::sync::Arc::new(move || {
hook_calls.fetch_add(1, Ordering::SeqCst);
})
});
health.set_ready(true);
let shutdown = CancellationToken::new();
let srv = tokio::spawn(serve_on(listener, health.clone(), shutdown.clone()));
assert!(status_line(addr, "/readyz").await.contains("200"));
assert!(!health.is_draining());
assert!(
method_status_line(addr, "GET", "/drain")
.await
.contains("200")
);
assert_eq!(hook_calls.load(Ordering::SeqCst), 1);
assert!(health.is_draining());
assert!(
status_line(addr, "/readyz").await.contains("503"),
"readyz must report 503 while draining even though set_ready(true) was never undone"
);
assert!(
method_status_line(addr, "POST", "/drain")
.await
.contains("200")
);
assert_eq!(
hook_calls.load(Ordering::SeqCst),
1,
"the drain hook must fire only once, on the first /drain hit"
);
assert!(status_line(addr, "/readyz").await.contains("503"));
shutdown.cancel();
srv.await.unwrap().unwrap();
}
#[tokio::test]
async fn metrics_reports_build_info_with_version_label() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let shutdown = CancellationToken::new();
let srv = tokio::spawn(serve_on(listener, Health::new(), shutdown.clone()));
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
stream
.write_all(b"GET /metrics HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.await
.unwrap();
let mut buf = String::new();
stream.read_to_string(&mut buf).await.unwrap();
let expected = format!(
"polychrome_build_info{{version=\"{}\"}} 1",
env!("CARGO_PKG_VERSION")
);
assert!(
buf.contains(&expected),
"scrape missing labelled build_info:\n{buf}"
);
shutdown.cancel();
srv.await.unwrap().unwrap();
}
}