use std::{error::Error, fmt, future::Future, time::Duration};
use tokio_util::sync::CancellationToken;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HealthProbe {
Startup,
Readiness,
Liveness,
Other,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WaitUntilHealthyError {
Cancelled,
}
impl fmt::Display for WaitUntilHealthyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cancelled => f.write_str("health wait cancelled"),
}
}
}
impl Error for WaitUntilHealthyError {}
pub trait HealthCheck {
type HealthError: Error + Send + Sync + 'static;
fn is_healthy(&self, probe: HealthProbe) -> Result<(), Self::HealthError>;
fn wait_until_healthy(
&self,
cancel: CancellationToken,
interval: Duration,
) -> impl Future<Output = Result<(), WaitUntilHealthyError>> + Send + '_
where
Self: Sync,
{
async move {
loop {
if self.is_healthy(HealthProbe::Other).is_ok() {
return Ok(());
}
tokio::select! {
() = cancel.cancelled() => return Err(WaitUntilHealthyError::Cancelled),
() = tokio::time::sleep(interval) => {}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::{fmt, time::Duration};
use super::{HealthCheck, HealthProbe, WaitUntilHealthyError};
use tokio_util::sync::CancellationToken;
struct AlwaysHealthy;
impl HealthCheck for AlwaysHealthy {
type HealthError = Unhealthy;
fn is_healthy(&self, _probe: HealthProbe) -> Result<(), Self::HealthError> {
Ok(())
}
}
struct NeverHealthy;
impl HealthCheck for NeverHealthy {
type HealthError = Unhealthy;
fn is_healthy(&self, _probe: HealthProbe) -> Result<(), Self::HealthError> {
Err(Unhealthy)
}
}
#[derive(Debug)]
struct Unhealthy;
impl fmt::Display for Unhealthy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unhealthy")
}
}
impl std::error::Error for Unhealthy {}
#[test]
fn wait_until_healthy_returns_when_healthy() {
let runtime = runtime();
let health = AlwaysHealthy;
let cancel = CancellationToken::new();
let result = runtime.block_on(health.wait_until_healthy(cancel, Duration::from_secs(1)));
assert!(
result.is_ok(),
"healthy component should not wait for cancellation"
);
}
#[test]
fn wait_until_healthy_returns_when_cancelled() {
let runtime = runtime();
let health = NeverHealthy;
let cancel = CancellationToken::new();
cancel.cancel();
let result = runtime.block_on(health.wait_until_healthy(cancel, Duration::from_secs(1)));
assert_eq!(
result,
Err(WaitUntilHealthyError::Cancelled),
"cancelled wait should return the cancellation error"
);
}
fn runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("test runtime should build")
}
}