use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
Unknown,
}
impl HealthStatus {
#[must_use]
pub const fn is_healthy(&self) -> bool {
matches!(self, Self::Healthy)
}
#[must_use]
pub const fn is_operational(&self) -> bool {
matches!(self, Self::Healthy | Self::Degraded)
}
}
#[derive(Debug, Clone)]
pub struct HealthCheckResult {
pub status: HealthStatus,
pub message: Option<String>,
pub duration: Duration,
pub timestamp: Instant,
}
impl HealthCheckResult {
#[must_use]
pub fn healthy() -> Self {
Self {
status: HealthStatus::Healthy,
message: None,
duration: Duration::ZERO,
timestamp: Instant::now(),
}
}
#[must_use]
pub fn unhealthy(message: impl Into<String>) -> Self {
Self {
status: HealthStatus::Unhealthy,
message: Some(message.into()),
duration: Duration::ZERO,
timestamp: Instant::now(),
}
}
#[must_use]
pub fn degraded(message: impl Into<String>) -> Self {
Self {
status: HealthStatus::Degraded,
message: Some(message.into()),
duration: Duration::ZERO,
timestamp: Instant::now(),
}
}
#[must_use]
pub const fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
}
#[derive(Debug, Clone)]
pub struct HealthCheckConfig {
pub interval: Duration,
pub timeout: Duration,
pub failure_threshold: u32,
pub success_threshold: u32,
}
impl Default for HealthCheckConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(30),
timeout: Duration::from_secs(5),
failure_threshold: 3,
success_threshold: 1,
}
}
}
impl HealthCheckConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
#[must_use]
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub const fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold;
self
}
#[must_use]
pub const fn with_success_threshold(mut self, threshold: u32) -> Self {
self.success_threshold = threshold;
self
}
}
#[derive(Debug)]
pub struct HealthChecker {
config: HealthCheckConfig,
status: HealthStatus,
failures: u32,
successes: u32,
last_check: Option<Instant>,
last_result: Option<HealthCheckResult>,
}
impl HealthChecker {
#[must_use]
pub const fn new(config: HealthCheckConfig) -> Self {
Self {
config,
status: HealthStatus::Unknown,
failures: 0,
successes: 0,
last_check: None,
last_result: None,
}
}
#[must_use]
pub const fn status(&self) -> HealthStatus {
self.status
}
#[must_use]
pub const fn last_result(&self) -> Option<&HealthCheckResult> {
self.last_result.as_ref()
}
#[must_use]
pub fn is_check_due(&self) -> bool {
match self.last_check {
Some(last) => last.elapsed() >= self.config.interval,
None => true,
}
}
pub fn record_success(&mut self) {
self.failures = 0;
self.successes += 1;
self.last_check = Some(Instant::now());
if self.successes >= self.config.success_threshold {
self.status = HealthStatus::Healthy;
}
self.last_result = Some(HealthCheckResult::healthy());
}
pub fn record_failure(&mut self, message: impl Into<String>) {
self.successes = 0;
self.failures += 1;
self.last_check = Some(Instant::now());
if self.failures >= self.config.failure_threshold {
self.status = HealthStatus::Unhealthy;
} else if self.failures > 0 {
self.status = HealthStatus::Degraded;
}
self.last_result = Some(HealthCheckResult::unhealthy(message));
}
pub fn reset(&mut self) {
self.status = HealthStatus::Unknown;
self.failures = 0;
self.successes = 0;
self.last_check = None;
self.last_result = None;
}
}
#[must_use]
pub fn liveness_check() -> HealthCheckResult {
HealthCheckResult::healthy()
}
#[must_use]
#[cfg(unix)]
#[allow(unsafe_code)]
pub fn process_alive(pid: i32) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
#[must_use]
#[cfg(windows)]
#[allow(unsafe_code)]
pub fn process_alive(pid: i32) -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT};
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};
if pid <= 0 {
return false;
}
let handle = unsafe { OpenProcess(PROCESS_SYNCHRONIZE, 0, pid as u32) };
if handle.is_null() {
return false;
}
let result = unsafe { WaitForSingleObject(handle, 0) };
unsafe {
CloseHandle(handle);
}
result == WAIT_TIMEOUT
}
#[must_use]
#[cfg(not(any(unix, windows)))]
pub fn process_alive(_pid: i32) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn health_status() {
assert!(HealthStatus::Healthy.is_healthy());
assert!(HealthStatus::Healthy.is_operational());
assert!(!HealthStatus::Degraded.is_healthy());
assert!(HealthStatus::Degraded.is_operational());
assert!(!HealthStatus::Unhealthy.is_operational());
}
#[test]
fn health_checker_transitions() {
let config = HealthCheckConfig {
failure_threshold: 2,
success_threshold: 1,
..Default::default()
};
let mut checker = HealthChecker::new(config);
assert_eq!(checker.status(), HealthStatus::Unknown);
checker.record_success();
assert_eq!(checker.status(), HealthStatus::Healthy);
checker.record_failure("test");
assert_eq!(checker.status(), HealthStatus::Degraded);
checker.record_failure("test");
assert_eq!(checker.status(), HealthStatus::Unhealthy);
checker.record_success();
assert_eq!(checker.status(), HealthStatus::Healthy);
}
}