use crate::error::Result;
use crate::handler::BoxFuture;
use crate::request::Request;
use crate::response::Response;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
type CheckFn = dyn Fn() -> BoxFuture<'static, std::result::Result<(), String>> + Send + Sync;
struct Check {
name: String,
critical: bool,
run: Arc<CheckFn>,
}
#[derive(Clone)]
struct Outcome {
ok: bool,
critical: bool,
millis: u64,
error: Option<String>,
}
struct Cached {
at: Instant,
outcomes: Arc<BTreeMap<String, Outcome>>,
}
struct Inner {
checks: Vec<Check>,
timeout: Duration,
cache_for: Duration,
expose_errors: bool,
cache: Mutex<Option<Cached>>,
}
#[derive(Clone)]
pub struct Health {
inner: Arc<Inner>,
}
impl Default for Health {
fn default() -> Self {
Self::new()
}
}
impl Health {
pub fn new() -> Self {
Self {
inner: Arc::new(Inner {
checks: Vec::new(),
timeout: Duration::from_secs(2),
cache_for: Duration::from_secs(1),
expose_errors: false,
cache: Mutex::new(None),
}),
}
}
fn edit(mut self, f: impl FnOnce(&mut Inner)) -> Self {
let inner = Arc::get_mut(&mut self.inner).expect("configure Health before sharing it");
f(inner);
self
}
fn add<F, Fut, E>(self, name: &str, critical: bool, check: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = std::result::Result<(), E>> + Send + 'static,
E: Display,
{
let name = name.to_string();
self.edit(|i| {
i.checks.push(Check {
name,
critical,
run: Arc::new(move || {
let fut = check();
Box::pin(async move { fut.await.map_err(|e| e.to_string()) })
}),
})
})
}
pub fn check<F, Fut, E>(self, name: &str, check: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = std::result::Result<(), E>> + Send + 'static,
E: Display,
{
self.add(name, true, check)
}
pub fn optional<F, Fut, E>(self, name: &str, check: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = std::result::Result<(), E>> + Send + 'static,
E: Display,
{
self.add(name, false, check)
}
pub fn timeout(self, timeout: Duration) -> Self {
self.edit(|i| i.timeout = timeout)
}
pub fn cache_for(self, ttl: Duration) -> Self {
self.edit(|i| i.cache_for = ttl)
}
pub fn expose_errors(self) -> Self {
self.edit(|i| i.expose_errors = true)
}
pub fn liveness(
&self,
) -> impl Fn(Request) -> BoxFuture<'static, Result<Response>> + Send + Sync + Clone + 'static
{
|_req| {
Box::pin(async {
Ok(Response::json(serde_json::json!({ "status": "up" }))
.with_header("cache-control", "no-store"))
})
}
}
pub fn readiness(
&self,
) -> impl Fn(Request) -> BoxFuture<'static, Result<Response>> + Send + Sync + Clone + 'static
{
let health = self.clone();
move |req: Request| {
let health = health.clone();
Box::pin(async move { Ok(health.report(&req).await) })
}
}
async fn outcomes(&self) -> Arc<BTreeMap<String, Outcome>> {
let inner = &self.inner;
let mut cache = inner.cache.lock().await;
if let Some(c) = cache.as_ref() {
if c.at.elapsed() < inner.cache_for {
return c.outcomes.clone();
}
}
let runs = inner.checks.iter().map(|check| async move {
let started = Instant::now();
let result = tokio::time::timeout(inner.timeout, (check.run)()).await;
let (ok, error) = match result {
Ok(Ok(())) => (true, None),
Ok(Err(e)) => (false, Some(e)),
Err(_) => (false, Some(format!("timed out after {:?}", inner.timeout))),
};
(
check.name.clone(),
Outcome {
ok,
critical: check.critical,
millis: started.elapsed().as_millis() as u64,
error,
},
)
});
let outcomes: BTreeMap<_, _> = join_all(runs).await.into_iter().collect();
let outcomes = Arc::new(outcomes);
*cache = Some(Cached {
at: Instant::now(),
outcomes: outcomes.clone(),
});
outcomes
}
async fn report(&self, req: &Request) -> Response {
let no_store = |r: Response| r.with_header("cache-control", "no-store");
if req.shutdown_signal().is_draining() {
return no_store(
Response::json(serde_json::json!({ "status": "draining" }))
.with_status(http::StatusCode::SERVICE_UNAVAILABLE),
);
}
let outcomes = self.outcomes().await;
let down = outcomes.values().any(|o| !o.ok && o.critical);
let degraded = outcomes.values().any(|o| !o.ok && !o.critical);
let status = if down {
"down"
} else if degraded {
"degraded"
} else {
"up"
};
let checks: serde_json::Map<_, _> = outcomes
.iter()
.map(|(name, o)| {
let mut entry = serde_json::json!({
"status": if o.ok { "up" } else { "down" },
"critical": o.critical,
"ms": o.millis,
});
if let (false, Some(err), true) = (o.ok, &o.error, self.inner.expose_errors) {
entry["error"] = err.clone().into();
}
(name.clone(), entry)
})
.collect();
let code = if down {
http::StatusCode::SERVICE_UNAVAILABLE
} else {
http::StatusCode::OK
};
no_store(
Response::json(serde_json::json!({ "status": status, "checks": checks }))
.with_status(code),
)
}
}
async fn join_all<F: Future>(futures: impl IntoIterator<Item = F>) -> Vec<F::Output> {
let mut futures: Vec<_> = futures.into_iter().map(Box::pin).collect();
let mut done: Vec<Option<F::Output>> = futures.iter().map(|_| None).collect();
std::future::poll_fn(|cx| {
let mut pending = false;
for (i, f) in futures.iter_mut().enumerate() {
if done[i].is_none() {
match f.as_mut().poll(cx) {
std::task::Poll::Ready(v) => done[i] = Some(v),
std::task::Poll::Pending => pending = true,
}
}
}
if pending {
std::task::Poll::Pending
} else {
std::task::Poll::Ready(())
}
})
.await;
done.into_iter().flatten().collect()
}