objects/store/
liveness.rs1use chrono::{DateTime, Duration, Utc};
5
6pub const AGENT_LEASE_DURATION: Duration = Duration::minutes(5);
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Liveness {
10 Alive,
12 Dead,
14}
15
16impl std::fmt::Display for Liveness {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::Alive => write!(f, "alive"),
20 Self::Dead => write!(f, "dead"),
21 }
22 }
23}
24
25#[cfg(target_os = "linux")]
33pub fn current_boot_id() -> Option<String> {
34 std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
35 .ok()
36 .map(|value| value.trim().to_string())
37 .filter(|value| !value.is_empty())
38}
39
40#[cfg(target_os = "macos")]
41pub fn current_boot_id() -> Option<String> {
42 std::process::Command::new("sysctl")
43 .arg("-n")
44 .arg("kern.boottime")
45 .output()
46 .ok()
47 .filter(|output| output.status.success())
48 .and_then(|output| String::from_utf8(output.stdout).ok())
49 .map(|value| {
50 let trimmed = value.trim();
51 let cutoff = trimmed
52 .find('}')
53 .map(|idx| idx + 1)
54 .unwrap_or(trimmed.len());
55 trimmed[..cutoff].to_string()
56 })
57 .filter(|value| !value.is_empty())
58}
59
60#[cfg(not(any(target_os = "linux", target_os = "macos")))]
61pub fn current_boot_id() -> Option<String> {
62 None
63}
64
65#[cfg(unix)]
71pub fn process_alive(pid: u32) -> bool {
72 let pid = pid as libc::pid_t;
73 if pid <= 0 {
74 return false;
75 }
76 let result = unsafe { libc::kill(pid, 0) };
77 if result == 0 {
78 return true;
79 }
80 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
81 errno != libc::ESRCH
82}
83
84#[cfg(not(unix))]
85pub fn process_alive(_pid: u32) -> bool {
86 true
90}
91
92pub fn reservation_liveness_at(
95 pid: Option<u32>,
96 recorded_boot_id: Option<&str>,
97 heartbeat_at: Option<DateTime<Utc>>,
98 now: DateTime<Utc>,
99) -> Liveness {
100 if pid.is_some_and(|pid| !process_alive(pid)) {
101 return Liveness::Dead;
102 }
103
104 if matches!(
105 (recorded_boot_id, current_boot_id()),
106 (Some(recorded), Some(current)) if recorded != current
107 ) {
108 return Liveness::Dead;
109 }
110
111 match heartbeat_at {
112 Some(heartbeat) if now <= heartbeat + AGENT_LEASE_DURATION => Liveness::Alive,
113 Some(_) => Liveness::Dead,
114 None => Liveness::Dead,
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn process_alive_returns_true_for_self() {
124 assert!(process_alive(std::process::id()));
125 }
126
127 #[test]
128 fn process_alive_returns_false_for_pid_zero() {
129 assert!(!process_alive(0));
130 }
131
132 #[test]
133 fn process_alive_returns_false_for_unlikely_pid() {
134 assert!(!process_alive(0x7fff_ffff));
139 }
140
141 #[test]
142 fn reservation_is_alive_from_fresh_heartbeat_without_pid() {
143 let now = Utc::now();
144 assert_eq!(
145 reservation_liveness_at(None, None, Some(now), now),
146 Liveness::Alive
147 );
148 }
149
150 #[test]
151 fn reservation_is_dead_when_boot_id_mismatches() {
152 let now = Utc::now();
153 let pid = std::process::id();
154 let liveness = reservation_liveness_at(
155 Some(pid),
156 Some("definitely-not-the-current-boot-id"),
157 Some(now),
158 now,
159 );
160 if current_boot_id().is_some() {
161 assert_eq!(liveness, Liveness::Dead);
162 } else {
163 assert_eq!(liveness, Liveness::Alive);
164 }
165 }
166
167 #[test]
168 fn reservation_is_alive_when_lease_and_process_are_current() {
169 let now = Utc::now();
170 let pid = std::process::id();
171 let boot = current_boot_id();
172 assert_eq!(
173 reservation_liveness_at(Some(pid), boot.as_deref(), Some(now), now),
174 Liveness::Alive
175 );
176 }
177
178 #[test]
179 fn reservation_is_dead_when_pid_is_dead() {
180 let now = Utc::now();
181 let liveness = reservation_liveness_at(
182 Some(0x7fff_ffff),
183 current_boot_id().as_deref(),
184 Some(now),
185 now,
186 );
187 assert_eq!(liveness, Liveness::Dead);
188 }
189
190 #[test]
191 fn reservation_is_dead_when_heartbeat_lease_expires() {
192 let now = Utc::now();
193 assert_eq!(
194 reservation_liveness_at(
195 None,
196 None,
197 Some(now - AGENT_LEASE_DURATION - Duration::seconds(1)),
198 now,
199 ),
200 Liveness::Dead
201 );
202 }
203
204 #[test]
205 fn reservation_is_dead_without_heartbeat() {
206 assert_eq!(
207 reservation_liveness_at(None, None, None, Utc::now()),
208 Liveness::Dead
209 );
210 }
211}