use chrono::{DateTime, Duration, Utc};
pub const AGENT_LEASE_DURATION: Duration = Duration::minutes(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Liveness {
Alive,
Dead,
}
impl std::fmt::Display for Liveness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Alive => write!(f, "alive"),
Self::Dead => write!(f, "dead"),
}
}
}
#[cfg(target_os = "linux")]
pub fn current_boot_id() -> Option<String> {
std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
#[cfg(target_os = "macos")]
pub fn current_boot_id() -> Option<String> {
std::process::Command::new("sysctl")
.arg("-n")
.arg("kern.boottime")
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|value| {
let trimmed = value.trim();
let cutoff = trimmed
.find('}')
.map(|idx| idx + 1)
.unwrap_or(trimmed.len());
trimmed[..cutoff].to_string()
})
.filter(|value| !value.is_empty())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn current_boot_id() -> Option<String> {
None
}
#[cfg(unix)]
pub fn process_alive(pid: u32) -> bool {
let pid = pid as libc::pid_t;
if pid <= 0 {
return false;
}
let result = unsafe { libc::kill(pid, 0) };
if result == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
errno != libc::ESRCH
}
#[cfg(not(unix))]
pub fn process_alive(_pid: u32) -> bool {
true
}
pub fn reservation_liveness_at(
pid: Option<u32>,
recorded_boot_id: Option<&str>,
heartbeat_at: Option<DateTime<Utc>>,
now: DateTime<Utc>,
) -> Liveness {
if pid.is_some_and(|pid| !process_alive(pid)) {
return Liveness::Dead;
}
if matches!(
(recorded_boot_id, current_boot_id()),
(Some(recorded), Some(current)) if recorded != current
) {
return Liveness::Dead;
}
match heartbeat_at {
Some(heartbeat) if now <= heartbeat + AGENT_LEASE_DURATION => Liveness::Alive,
Some(_) => Liveness::Dead,
None => Liveness::Dead,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_alive_returns_true_for_self() {
assert!(process_alive(std::process::id()));
}
#[test]
fn process_alive_returns_false_for_pid_zero() {
assert!(!process_alive(0));
}
#[test]
fn process_alive_returns_false_for_unlikely_pid() {
assert!(!process_alive(0x7fff_ffff));
}
#[test]
fn reservation_is_alive_from_fresh_heartbeat_without_pid() {
let now = Utc::now();
assert_eq!(
reservation_liveness_at(None, None, Some(now), now),
Liveness::Alive
);
}
#[test]
fn reservation_is_dead_when_boot_id_mismatches() {
let now = Utc::now();
let pid = std::process::id();
let liveness = reservation_liveness_at(
Some(pid),
Some("definitely-not-the-current-boot-id"),
Some(now),
now,
);
if current_boot_id().is_some() {
assert_eq!(liveness, Liveness::Dead);
} else {
assert_eq!(liveness, Liveness::Alive);
}
}
#[test]
fn reservation_is_alive_when_lease_and_process_are_current() {
let now = Utc::now();
let pid = std::process::id();
let boot = current_boot_id();
assert_eq!(
reservation_liveness_at(Some(pid), boot.as_deref(), Some(now), now),
Liveness::Alive
);
}
#[test]
fn reservation_is_dead_when_pid_is_dead() {
let now = Utc::now();
let liveness = reservation_liveness_at(
Some(0x7fff_ffff),
current_boot_id().as_deref(),
Some(now),
now,
);
assert_eq!(liveness, Liveness::Dead);
}
#[test]
fn reservation_is_dead_when_heartbeat_lease_expires() {
let now = Utc::now();
assert_eq!(
reservation_liveness_at(
None,
None,
Some(now - AGENT_LEASE_DURATION - Duration::seconds(1)),
now,
),
Liveness::Dead
);
}
#[test]
fn reservation_is_dead_without_heartbeat() {
assert_eq!(
reservation_liveness_at(None, None, None, Utc::now()),
Liveness::Dead
);
}
}