#[cfg(test)]
mod tests;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Probe {
Liveness,
Readiness,
Startup,
}
impl Probe {
pub fn path(self) -> &'static str {
match self {
Probe::Liveness => "/livez",
Probe::Readiness => "/readyz",
Probe::Startup => "/startupz",
}
}
}
type Check = Arc<dyn Fn() -> Result<(), String> + Send + Sync>;
pub struct Registry {
checks: RwLock<Vec<(Probe, String, Check)>>,
timeout_ms: AtomicU64,
started: AtomicBool,
}
impl Default for Registry {
fn default() -> Self {
Registry { checks: RwLock::new(Vec::new()), timeout_ms: AtomicU64::new(2000), started: AtomicBool::new(false) }
}
}
pub fn global() -> &'static Registry {
static R: OnceLock<Registry> = OnceLock::new();
R.get_or_init(Registry::default)
}
impl Registry {
pub fn new() -> Self {
Registry::default()
}
pub fn register(&self, probe: Probe, name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
checks.retain(|(p, n, _)| !(*p == probe && n == name));
checks.push((probe, name.to_string(), Arc::new(check)));
}
pub fn unregister(&self, probe: Probe, name: &str) -> bool {
let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
let before = checks.len();
checks.retain(|(p, n, _)| !(*p == probe && n == name));
checks.len() != before
}
pub fn set_timeout(&self, timeout: Duration) {
self.timeout_ms.store(timeout.as_millis().max(1) as u64, Ordering::Relaxed);
}
pub fn has_checks(&self, probe: Probe) -> bool {
self.checks.read().unwrap_or_else(|e| e.into_inner()).iter().any(|(p, _, _)| *p == probe)
}
pub fn report(&self, probe: Probe) -> Report {
let checks: Vec<(String, Check)> = self
.checks
.read()
.unwrap_or_else(|e| e.into_inner())
.iter()
.filter(|(p, _, _)| *p == probe)
.map(|(_, n, c)| (n.clone(), Arc::clone(c)))
.collect();
if probe == Probe::Startup && self.started.load(Ordering::Relaxed) {
return Report { probe, ok: true, checks: checks.into_iter().map(|(name, _)| CheckResult { name, error: None }).collect(), note: None };
}
let timeout = Duration::from_millis(self.timeout_ms.load(Ordering::Relaxed));
let handles: Vec<(String, std::thread::JoinHandle<Result<(), String>>)> =
checks.into_iter().map(|(name, check)| (name, std::thread::spawn(move || run_one(check, timeout)))).collect();
let results: Vec<CheckResult> = handles
.into_iter()
.map(|(name, h)| CheckResult { name, error: h.join().unwrap_or_else(|_| Err("the check panicked".into())).err() })
.collect();
let mut note = None;
if probe == Probe::Readiness && !crate::metrics::SERVER_READY.load(Ordering::Relaxed) {
note = Some("server not ready (starting or shutting down)".to_string());
}
let ok = note.is_none() && results.iter().all(|c| c.error.is_none());
if probe == Probe::Startup && ok {
self.started.store(true, Ordering::Relaxed);
}
Report { probe, ok, checks: results, note }
}
}
pub fn register(probe: Probe, name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
global().register(probe, name, check)
}
pub fn liveness(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
register(Probe::Liveness, name, check)
}
pub fn readiness(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
register(Probe::Readiness, name, check)
}
pub fn startup(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
register(Probe::Startup, name, check)
}
#[cfg(feature = "http2")]
pub fn register_async<F, Fut>(probe: Probe, name: &str, check: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<(), String>> + Send + 'static,
{
register(probe, name, move || crate::async_bridge::block_on_shared(check()))
}
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
pub fn database(name: &str, pool: crate::model::DbPool) {
register_async(Probe::Readiness, name, move || {
let pool = pool.clone();
async move { pool.query_rows("SELECT 1", &[]).await.map(|_| ()).map_err(|e| e.to_string()) }
})
}
pub fn unregister(probe: Probe, name: &str) -> bool {
global().unregister(probe, name)
}
pub fn set_timeout(timeout: Duration) {
global().set_timeout(timeout)
}
pub fn has_checks(probe: Probe) -> bool {
global().has_checks(probe)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckResult {
pub name: String,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub probe: Probe,
pub ok: bool,
pub checks: Vec<CheckResult>,
pub note: Option<String>,
}
impl Report {
pub fn body(&self, verbose: bool) -> String {
let mut lines = Vec::new();
if let Some(note) = &self.note {
lines.push(format!("[-]{note}"));
}
for c in &self.checks {
match &c.error {
None if verbose => lines.push(format!("[+]{} ok", c.name)),
None => {}
Some(e) => lines.push(format!("[-]{} failed: {e}", c.name)),
}
}
if verbose {
let what = &self.probe.path()[1..];
lines.push(if self.ok { format!("{what} check passed") } else { format!("{what} check failed") });
lines.join("\n")
} else if self.ok {
"OK".to_string()
} else {
lines.join("\n")
}
}
}
fn run_one(check: Check, timeout: Duration) -> Result<(), String> {
let (tx, rx) = std::sync::mpsc::channel();
let spawned = std::thread::Builder::new().name("rws-health".into()).spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check())).unwrap_or_else(|_| Err("the check panicked".into()));
let _ = tx.send(result);
});
if spawned.is_err() {
return Err("could not start the check".into());
}
match rx.recv_timeout(timeout) {
Ok(result) => result,
Err(_) => Err(format!("timed out after {} ms", timeout.as_millis())),
}
}
pub fn report(probe: Probe) -> Report {
global().report(probe)
}
pub fn probe_for_path(path: &str) -> Option<Probe> {
match path {
"/livez" | "/healthz" => Some(Probe::Liveness),
"/readyz" => Some(Probe::Readiness),
"/startupz" => Some(Probe::Startup),
_ => None,
}
}
pub fn respond(probe: Probe, verbose: bool) -> crate::response::Response {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let report = report(probe);
let status = if report.ok { STATUS_CODE_REASON_PHRASE.n200_ok } else { STATUS_CODE_REASON_PHRASE.n503_service_unavailable };
let body = report.body(verbose);
let body = if !report.ok && body.is_empty() { "not ready".to_string() } else { body };
let mut response = Response::text(status, &body);
response.headers.push(crate::header::Header::new(crate::header::Header::_CACHE_CONTROL, "no-store"));
response
}