use std::{
collections::HashSet,
time::{SystemTime, UNIX_EPOCH},
};
use bytes::Bytes;
use chrono::{DateTime, Utc};
use http::{Response, StatusCode};
use serde_json::{Map, Value, json};
use super::answer::json;
use crate::{Phase, ProjectionRun, ProjectionStatus};
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
pub struct Health {
runs: Vec<(String, ProjectionRun)>,
verdict: Verdict,
}
#[derive(Clone, Copy, Debug)]
enum Verdict {
Ready,
Alive,
}
impl Health {
pub fn readiness(runs: impl IntoIterator<Item = ProjectionRun>) -> Self {
Self::new(runs, Verdict::Ready)
}
pub fn liveness(runs: impl IntoIterator<Item = ProjectionRun>) -> Self {
Self::new(runs, Verdict::Alive)
}
fn new(runs: impl IntoIterator<Item = ProjectionRun>, verdict: Verdict) -> Self {
let mut names = HashSet::new();
let runs = runs
.into_iter()
.enumerate()
.map(|(position, run)| {
let Some(name) = run.name().map(ToOwned::to_owned) else {
panic!(
"architecturekit: projection {position} has no name, give it one with ProjectionOptions"
);
};
assert!(
names.insert(name.clone()),
"architecturekit: two projections are named {name:?}"
);
(name, run)
})
.collect();
Self { runs, verdict }
}
#[must_use]
pub fn answer(&self) -> Response<Bytes> {
let mut is_healthy = true;
let mut projections = Map::new();
for (name, run) in &self.runs {
let status = run.status();
is_healthy = is_healthy && self.judge(&status);
let _ = projections.insert(name.clone(), describe(&status));
}
let key = match self.verdict {
Verdict::Ready => "isReady",
Verdict::Alive => "isAlive",
};
let status = if is_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
json(
status,
&json!({ key: is_healthy, "projections": projections }),
)
}
fn judge(&self, status: &ProjectionStatus) -> bool {
let has_stopped = status.phase == Phase::Stopped;
match self.verdict {
Verdict::Ready => status.has_caught_up && !has_stopped,
Verdict::Alive => !has_stopped,
}
}
}
fn describe(status: &ProjectionStatus) -> Value {
let phase = match status.phase {
Phase::CatchingUp => "catching-up",
Phase::Live => "live",
Phase::Reconnecting => "reconnecting",
Phase::Stopped => "stopped",
};
json!({
"phase": phase,
"since": time_of(status.since),
"hasCaughtUp": status.has_caught_up,
"attempts": status.attempts,
"revision": status.revision.as_deref().unwrap_or_default(),
})
}
fn time_of(time: SystemTime) -> DateTime<Utc> {
let since_epoch = time.duration_since(UNIX_EPOCH).unwrap_or_default();
i64::try_from(since_epoch.as_secs())
.ok()
.and_then(|seconds| DateTime::from_timestamp(seconds, since_epoch.subsec_nanos()))
.unwrap_or(DateTime::UNIX_EPOCH)
}
#[cfg(feature = "axum")]
impl<S: Send + Sync + 'static> axum::handler::Handler<(), S> for Health {
type Future = std::future::Ready<axum::response::Response>;
fn call(self, _: axum::extract::Request, _: S) -> Self::Future {
std::future::ready(self.answer().map(axum::body::Body::from))
}
}