use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthState {
Healthy,
Unhealthy,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct HealthStatus {
pub status: HealthState,
pub message: Option<String>,
}
impl HealthStatus {
#[must_use]
pub const fn healthy() -> Self {
Self {
status: HealthState::Healthy,
message: None,
}
}
#[must_use]
pub fn unhealthy(message: impl Into<String>) -> Self {
Self {
status: HealthState::Unhealthy,
message: Some(message.into()),
}
}
}
#[must_use]
pub const fn health_check() -> HealthStatus {
HealthStatus::healthy()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ClusterReadiness {
#[default]
NotConfigured,
Configured {
membership_established: bool,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ReadinessState {
pub config_loaded: bool,
pub listener_bound: bool,
pub cluster: ClusterReadiness,
}
impl ReadinessState {
#[must_use]
pub const fn new(config_loaded: bool, listener_bound: bool, cluster: ClusterReadiness) -> Self {
Self {
config_loaded,
listener_bound,
cluster,
}
}
#[must_use]
pub const fn ready_without_cluster() -> Self {
Self::new(true, true, ClusterReadiness::NotConfigured)
}
#[must_use]
pub const fn ready_with_cluster() -> Self {
Self::new(
true,
true,
ClusterReadiness::Configured {
membership_established: true,
},
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReadinessCondition {
ConfigLoaded,
ListenerBound,
ClusterMembershipEstablished,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ReadinessStatus {
pub ready: bool,
pub unmet_conditions: Vec<ReadinessCondition>,
}
impl ReadinessStatus {
#[must_use]
pub fn from_unmet_conditions(unmet_conditions: Vec<ReadinessCondition>) -> Self {
Self {
ready: unmet_conditions.is_empty(),
unmet_conditions,
}
}
}
#[derive(Debug, Clone)]
pub struct SharedReadinessState {
inner: Arc<ReadinessFlags>,
}
impl SharedReadinessState {
#[must_use]
pub fn new(initial: ReadinessState) -> Self {
Self {
inner: Arc::new(ReadinessFlags::from_state(initial)),
}
}
#[must_use]
pub fn snapshot(&self) -> ReadinessState {
let cluster = if self.inner.cluster_configured.load(Ordering::SeqCst) {
ClusterReadiness::Configured {
membership_established: self
.inner
.cluster_membership_established
.load(Ordering::SeqCst),
}
} else {
ClusterReadiness::NotConfigured
};
ReadinessState::new(
self.inner.config_loaded.load(Ordering::SeqCst),
self.inner.listener_bound.load(Ordering::SeqCst),
cluster,
)
}
pub fn set_config_loaded(&self, loaded: bool) {
self.inner.config_loaded.store(loaded, Ordering::SeqCst);
}
pub fn set_listener_bound(&self, bound: bool) {
self.inner.listener_bound.store(bound, Ordering::SeqCst);
}
pub fn set_cluster_configured(&self, configured: bool) {
self.inner
.cluster_configured
.store(configured, Ordering::SeqCst);
if !configured {
self.set_cluster_membership_established(false);
}
}
pub fn set_cluster_membership_established(&self, established: bool) {
self.inner
.cluster_membership_established
.store(established, Ordering::SeqCst);
}
}
impl Default for SharedReadinessState {
fn default() -> Self {
Self::new(ReadinessState::default())
}
}
#[derive(Debug)]
struct ReadinessFlags {
config_loaded: AtomicBool,
listener_bound: AtomicBool,
cluster_configured: AtomicBool,
cluster_membership_established: AtomicBool,
}
impl ReadinessFlags {
const fn from_state(state: ReadinessState) -> Self {
let (cluster_configured, cluster_membership_established) = match state.cluster {
ClusterReadiness::NotConfigured => (false, false),
ClusterReadiness::Configured {
membership_established,
} => (true, membership_established),
};
Self {
config_loaded: AtomicBool::new(state.config_loaded),
listener_bound: AtomicBool::new(state.listener_bound),
cluster_configured: AtomicBool::new(cluster_configured),
cluster_membership_established: AtomicBool::new(cluster_membership_established),
}
}
}
#[must_use]
pub fn readiness_check(state: &ReadinessState) -> ReadinessStatus {
let mut unmet_conditions = Vec::new();
if !state.config_loaded {
unmet_conditions.push(ReadinessCondition::ConfigLoaded);
}
if !state.listener_bound {
unmet_conditions.push(ReadinessCondition::ListenerBound);
}
if state.cluster
== (ClusterReadiness::Configured {
membership_established: false,
})
{
unmet_conditions.push(ReadinessCondition::ClusterMembershipEstablished);
}
ReadinessStatus::from_unmet_conditions(unmet_conditions)
}
#[cfg(test)]
mod tests {
use super::{
ClusterReadiness, HealthState, ReadinessCondition, ReadinessState, SharedReadinessState,
health_check, readiness_check,
};
#[test]
fn health_check_is_always_healthy_liveness() {
let status = health_check();
assert_eq!(status.status, HealthState::Healthy);
assert!(status.message.is_none());
}
#[test]
fn readiness_reports_missing_config() {
let state = ReadinessState::new(false, true, ClusterReadiness::NotConfigured);
let status = readiness_check(&state);
assert!(!status.ready);
assert_eq!(
status.unmet_conditions,
vec![ReadinessCondition::ConfigLoaded]
);
}
#[test]
fn readiness_reports_missing_listener() {
let state = ReadinessState::new(true, false, ClusterReadiness::NotConfigured);
let status = readiness_check(&state);
assert!(!status.ready);
assert_eq!(
status.unmet_conditions,
vec![ReadinessCondition::ListenerBound]
);
}
#[test]
fn readiness_requires_cluster_membership_when_configured() {
let state = ReadinessState::new(
true,
true,
ClusterReadiness::Configured {
membership_established: false,
},
);
let status = readiness_check(&state);
assert!(!status.ready);
assert_eq!(
status.unmet_conditions,
vec![ReadinessCondition::ClusterMembershipEstablished]
);
}
#[test]
fn readiness_ignores_cluster_membership_when_not_configured() {
let state = ReadinessState::ready_without_cluster();
let status = readiness_check(&state);
assert!(status.ready);
assert!(status.unmet_conditions.is_empty());
}
#[test]
fn readiness_is_ready_only_when_all_applicable_conditions_are_met() {
let state = ReadinessState::ready_with_cluster();
let status = readiness_check(&state);
assert!(status.ready);
assert!(status.unmet_conditions.is_empty());
}
#[test]
fn shared_readiness_state_snapshots_updates() {
let shared = SharedReadinessState::default();
shared.set_config_loaded(true);
shared.set_listener_bound(true);
shared.set_cluster_configured(true);
shared.set_cluster_membership_established(true);
assert_eq!(shared.snapshot(), ReadinessState::ready_with_cluster());
}
}