1#[cfg(test)]
36mod tests;
37
38use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
39use std::sync::{Arc, OnceLock, RwLock};
40use std::time::Duration;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Probe {
45 Liveness,
46 Readiness,
47 Startup,
48}
49
50impl Probe {
51 pub fn path(self) -> &'static str {
53 match self {
54 Probe::Liveness => "/livez",
55 Probe::Readiness => "/readyz",
56 Probe::Startup => "/startupz",
57 }
58 }
59}
60
61type Check = Arc<dyn Fn() -> Result<(), String> + Send + Sync>;
62
63pub struct Registry {
66 checks: RwLock<Vec<(Probe, String, Check)>>,
67 timeout_ms: AtomicU64,
68 started: AtomicBool,
69}
70
71impl Default for Registry {
72 fn default() -> Self {
73 Registry { checks: RwLock::new(Vec::new()), timeout_ms: AtomicU64::new(2000), started: AtomicBool::new(false) }
74 }
75}
76
77pub fn global() -> &'static Registry {
79 static R: OnceLock<Registry> = OnceLock::new();
80 R.get_or_init(Registry::default)
81}
82
83impl Registry {
84 pub fn new() -> Self {
85 Registry::default()
86 }
87
88 pub fn register(&self, probe: Probe, name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
90 let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
91 checks.retain(|(p, n, _)| !(*p == probe && n == name));
92 checks.push((probe, name.to_string(), Arc::new(check)));
93 }
94
95 pub fn unregister(&self, probe: Probe, name: &str) -> bool {
97 let mut checks = self.checks.write().unwrap_or_else(|e| e.into_inner());
98 let before = checks.len();
99 checks.retain(|(p, n, _)| !(*p == probe && n == name));
100 checks.len() != before
101 }
102
103 pub fn set_timeout(&self, timeout: Duration) {
105 self.timeout_ms.store(timeout.as_millis().max(1) as u64, Ordering::Relaxed);
106 }
107
108 pub fn has_checks(&self, probe: Probe) -> bool {
110 self.checks.read().unwrap_or_else(|e| e.into_inner()).iter().any(|(p, _, _)| *p == probe)
111 }
112
113 pub fn report(&self, probe: Probe) -> Report {
117 let checks: Vec<(String, Check)> = self
118 .checks
119 .read()
120 .unwrap_or_else(|e| e.into_inner())
121 .iter()
122 .filter(|(p, _, _)| *p == probe)
123 .map(|(_, n, c)| (n.clone(), Arc::clone(c)))
124 .collect();
125 if probe == Probe::Startup && self.started.load(Ordering::Relaxed) {
126 return Report { probe, ok: true, checks: checks.into_iter().map(|(name, _)| CheckResult { name, error: None }).collect(), note: None };
127 }
128 let timeout = Duration::from_millis(self.timeout_ms.load(Ordering::Relaxed));
129 let handles: Vec<(String, std::thread::JoinHandle<Result<(), String>>)> =
130 checks.into_iter().map(|(name, check)| (name, std::thread::spawn(move || run_one(check, timeout)))).collect();
131 let results: Vec<CheckResult> = handles
132 .into_iter()
133 .map(|(name, h)| CheckResult { name, error: h.join().unwrap_or_else(|_| Err("the check panicked".into())).err() })
134 .collect();
135 let mut note = None;
136 if probe == Probe::Readiness && !crate::metrics::SERVER_READY.load(Ordering::Relaxed) {
137 note = Some("server not ready (starting or shutting down)".to_string());
138 }
139 let ok = note.is_none() && results.iter().all(|c| c.error.is_none());
140 if probe == Probe::Startup && ok {
141 self.started.store(true, Ordering::Relaxed);
142 }
143 Report { probe, ok, checks: results, note }
144 }
145}
146
147pub fn register(probe: Probe, name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
149 global().register(probe, name, check)
150}
151
152pub fn liveness(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
154 register(Probe::Liveness, name, check)
155}
156
157pub fn readiness(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
159 register(Probe::Readiness, name, check)
160}
161
162pub fn startup(name: &str, check: impl Fn() -> Result<(), String> + Send + Sync + 'static) {
164 register(Probe::Startup, name, check)
165}
166
167#[cfg(feature = "http2")]
169pub fn register_async<F, Fut>(probe: Probe, name: &str, check: F)
170where
171 F: Fn() -> Fut + Send + Sync + 'static,
172 Fut: std::future::Future<Output = Result<(), String>> + Send + 'static,
173{
174 register(probe, name, move || crate::async_bridge::block_on_shared(check()))
175}
176
177#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
179pub fn database(name: &str, pool: crate::model::DbPool) {
180 register_async(Probe::Readiness, name, move || {
181 let pool = pool.clone();
182 async move { pool.query_rows("SELECT 1", &[]).await.map(|_| ()).map_err(|e| e.to_string()) }
183 })
184}
185
186pub fn unregister(probe: Probe, name: &str) -> bool {
188 global().unregister(probe, name)
189}
190
191pub fn set_timeout(timeout: Duration) {
193 global().set_timeout(timeout)
194}
195
196pub fn has_checks(probe: Probe) -> bool {
198 global().has_checks(probe)
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct CheckResult {
204 pub name: String,
205 pub error: Option<String>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct Report {
212 pub probe: Probe,
213 pub ok: bool,
214 pub checks: Vec<CheckResult>,
215 pub note: Option<String>,
217}
218
219impl Report {
220 pub fn body(&self, verbose: bool) -> String {
223 let mut lines = Vec::new();
224 if let Some(note) = &self.note {
225 lines.push(format!("[-]{note}"));
226 }
227 for c in &self.checks {
228 match &c.error {
229 None if verbose => lines.push(format!("[+]{} ok", c.name)),
230 None => {}
231 Some(e) => lines.push(format!("[-]{} failed: {e}", c.name)),
232 }
233 }
234 if verbose {
235 let what = &self.probe.path()[1..];
236 lines.push(if self.ok { format!("{what} check passed") } else { format!("{what} check failed") });
237 lines.join("\n")
238 } else if self.ok {
239 "OK".to_string()
240 } else {
241 lines.join("\n")
242 }
243 }
244}
245
246fn run_one(check: Check, timeout: Duration) -> Result<(), String> {
247 let (tx, rx) = std::sync::mpsc::channel();
248 let spawned = std::thread::Builder::new().name("rws-health".into()).spawn(move || {
249 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check())).unwrap_or_else(|_| Err("the check panicked".into()));
250 let _ = tx.send(result);
251 });
252 if spawned.is_err() {
253 return Err("could not start the check".into());
254 }
255 match rx.recv_timeout(timeout) {
256 Ok(result) => result,
257 Err(_) => Err(format!("timed out after {} ms", timeout.as_millis())),
258 }
259}
260
261pub fn report(probe: Probe) -> Report {
263 global().report(probe)
264}
265
266pub fn probe_for_path(path: &str) -> Option<Probe> {
268 match path {
269 "/livez" | "/healthz" => Some(Probe::Liveness),
270 "/readyz" => Some(Probe::Readiness),
271 "/startupz" => Some(Probe::Startup),
272 _ => None,
273 }
274}
275
276pub fn respond(probe: Probe, verbose: bool) -> crate::response::Response {
278 use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
279 let report = report(probe);
280 let status = if report.ok { STATUS_CODE_REASON_PHRASE.n200_ok } else { STATUS_CODE_REASON_PHRASE.n503_service_unavailable };
281 let body = report.body(verbose);
282 let body = if !report.ok && body.is_empty() { "not ready".to_string() } else { body };
283 let mut response = Response::text(status, &body);
284 response.headers.push(crate::header::Header::new(crate::header::Header::_CACHE_CONTROL, "no-store"));
285 response
286}
287