magic_bird/store/pending.rs
1//! Runner liveness checking and recovery statistics.
2//!
3//! This module provides utilities for:
4//! - Checking if a process/runner is still alive
5//! - Tracking recovery operation statistics
6//!
7//! **V5 Schema Note**: In v5, pending detection is done via the invocations VIEW
8//! (attempts without matching outcomes). The old pending file mechanism from v4
9//! has been removed.
10
11/// Check if a runner is still alive based on its runner_id.
12///
13/// Supports various runner ID formats:
14/// - `pid:12345` - Local process ID
15/// - `gha:run:12345678` - GitHub Actions run
16/// - `k8s:pod:abc123` - Kubernetes pod
17pub fn is_runner_alive(runner_id: &str) -> bool {
18 if let Some(pid_str) = runner_id.strip_prefix("pid:") {
19 // Local process - check if PID exists
20 if let Ok(pid) = pid_str.parse::<i32>() {
21 return is_pid_alive(pid);
22 }
23 } else if runner_id.starts_with("gha:") {
24 // GitHub Actions - we can't reliably check, assume dead after max_age
25 // TODO: Could use GitHub API to check run status
26 return false;
27 } else if runner_id.starts_with("k8s:") {
28 // Kubernetes - we can't reliably check, assume dead after max_age
29 // TODO: Could use kubectl to check pod status
30 return false;
31 }
32
33 // Unknown format - assume dead
34 false
35}
36
37/// Check if a local PID is still alive.
38#[cfg(unix)]
39fn is_pid_alive(pid: i32) -> bool {
40 // Send signal 0 to check if process exists
41 // This works even for processes owned by other users
42 unsafe { libc::kill(pid, 0) == 0 }
43}
44
45#[cfg(not(unix))]
46fn is_pid_alive(_pid: i32) -> bool {
47 // On non-Unix, we can't easily check - assume alive
48 true
49}
50
51/// Statistics from recovery operations.
52#[derive(Debug, Default, Clone)]
53pub struct RecoveryStats {
54 /// Number of pending attempts checked.
55 pub pending_checked: usize,
56
57 /// Number still running (runner alive).
58 pub still_running: usize,
59
60 /// Number marked as orphaned.
61 pub orphaned: usize,
62
63 /// Number of errors encountered.
64 pub errors: usize,
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn test_is_runner_alive_current_process() {
73 let pid = std::process::id() as i32;
74 let runner_id = format!("pid:{}", pid);
75 assert!(is_runner_alive(&runner_id));
76 }
77
78 #[test]
79 fn test_is_runner_alive_dead_process() {
80 // PID 999999999 likely doesn't exist
81 let runner_id = "pid:999999999";
82 assert!(!is_runner_alive(runner_id));
83 }
84
85 #[test]
86 fn test_is_runner_alive_unknown_format() {
87 assert!(!is_runner_alive("unknown:123"));
88 assert!(!is_runner_alive("gha:run:12345"));
89 assert!(!is_runner_alive("k8s:pod:abc123"));
90 }
91}