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(target_os = "linux")]
87pub fn process_birth(pid: u32) -> Option<String> {
88 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
89 let fields = stat.rsplit_once(") ")?.1;
90 let tick = fields.split_whitespace().nth(19)?;
91 Some(tick.to_owned())
92}
93
94#[cfg(target_os = "macos")]
95pub fn process_birth(pid: u32) -> Option<String> {
96 let pid = i32::try_from(pid).ok()?;
97 let size = i32::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).ok()?;
98 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit();
99 let read = unsafe {
100 libc::proc_pidinfo(
101 pid,
102 libc::PROC_PIDTBSDINFO,
103 0,
104 info.as_mut_ptr().cast(),
105 size,
106 )
107 };
108 if read != size {
109 return None;
110 }
111 let info = unsafe { info.assume_init() };
112 Some(format!(
113 "{}:{}",
114 info.pbi_start_tvsec, info.pbi_start_tvusec
115 ))
116}
117
118#[cfg(not(any(target_os = "linux", target_os = "macos")))]
119pub fn process_birth(_pid: u32) -> Option<String> {
120 None
121}
122
123#[cfg(not(unix))]
124pub fn process_alive(_pid: u32) -> bool {
125 true
129}
130
131pub fn reservation_liveness_at(
134 pid: Option<u32>,
135 recorded_boot_id: Option<&str>,
136 heartbeat_at: Option<DateTime<Utc>>,
137 now: DateTime<Utc>,
138) -> Liveness {
139 if pid.is_some_and(|pid| !process_alive(pid)) {
140 return Liveness::Dead;
141 }
142
143 if matches!(
144 (recorded_boot_id, current_boot_id()),
145 (Some(recorded), Some(current)) if recorded != current
146 ) {
147 return Liveness::Dead;
148 }
149
150 match heartbeat_at {
151 Some(heartbeat) if now <= heartbeat + AGENT_LEASE_DURATION => Liveness::Alive,
152 Some(_) => Liveness::Dead,
153 None => Liveness::Dead,
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn process_alive_returns_true_for_self() {
163 assert!(process_alive(std::process::id()));
164 }
165
166 #[test]
167 fn process_alive_returns_false_for_pid_zero() {
168 assert!(!process_alive(0));
169 }
170
171 #[test]
172 fn process_alive_returns_false_for_unlikely_pid() {
173 assert!(!process_alive(0x7fff_ffff));
178 }
179
180 #[test]
181 fn reservation_is_alive_from_fresh_heartbeat_without_pid() {
182 let now = Utc::now();
183 assert_eq!(
184 reservation_liveness_at(None, None, Some(now), now),
185 Liveness::Alive
186 );
187 }
188
189 #[test]
190 fn reservation_is_dead_when_boot_id_mismatches() {
191 let now = Utc::now();
192 let pid = std::process::id();
193 let liveness = reservation_liveness_at(
194 Some(pid),
195 Some("definitely-not-the-current-boot-id"),
196 Some(now),
197 now,
198 );
199 if current_boot_id().is_some() {
200 assert_eq!(liveness, Liveness::Dead);
201 } else {
202 assert_eq!(liveness, Liveness::Alive);
203 }
204 }
205
206 #[test]
207 fn reservation_is_alive_when_lease_and_process_are_current() {
208 let now = Utc::now();
209 let pid = std::process::id();
210 let boot = current_boot_id();
211 assert_eq!(
212 reservation_liveness_at(Some(pid), boot.as_deref(), Some(now), now),
213 Liveness::Alive
214 );
215 }
216
217 #[test]
218 fn reservation_is_dead_when_pid_is_dead() {
219 let now = Utc::now();
220 let liveness = reservation_liveness_at(
221 Some(0x7fff_ffff),
222 current_boot_id().as_deref(),
223 Some(now),
224 now,
225 );
226 assert_eq!(liveness, Liveness::Dead);
227 }
228
229 #[test]
230 fn reservation_is_dead_when_heartbeat_lease_expires() {
231 let now = Utc::now();
232 assert_eq!(
233 reservation_liveness_at(
234 None,
235 None,
236 Some(now - AGENT_LEASE_DURATION - Duration::seconds(1)),
237 now,
238 ),
239 Liveness::Dead
240 );
241 }
242
243 #[test]
244 fn reservation_is_dead_without_heartbeat() {
245 assert_eq!(
246 reservation_liveness_at(None, None, None, Utc::now()),
247 Liveness::Dead
248 );
249 }
250}