ricecoder_external_lsp/process/
health.rs

1//! Health checking for LSP servers
2
3use crate::types::HealthStatus;
4use std::time::{Duration, Instant};
5use tracing::{debug, warn};
6
7/// Performs health checks on LSP servers
8pub struct HealthChecker {
9    /// Last successful health check time
10    last_check: Option<Instant>,
11    /// Health check interval
12    check_interval: Duration,
13    /// Number of consecutive failures
14    failure_count: u32,
15    /// Maximum consecutive failures before marking unhealthy
16    max_failures: u32,
17}
18
19impl HealthChecker {
20    /// Create a new health checker
21    pub fn new(check_interval: Duration) -> Self {
22        Self {
23            last_check: None,
24            check_interval,
25            failure_count: 0,
26            max_failures: 3,
27        }
28    }
29
30    /// Check if a health check is due
31    pub fn is_check_due(&self) -> bool {
32        match self.last_check {
33            None => true,
34            Some(last) => last.elapsed() >= self.check_interval,
35        }
36    }
37
38    /// Record a successful health check
39    pub fn record_success(&mut self, latency: Duration) -> HealthStatus {
40        self.last_check = Some(Instant::now());
41        self.failure_count = 0;
42        debug!(
43            latency_ms = latency.as_millis(),
44            "Health check passed"
45        );
46        HealthStatus::Healthy { latency }
47    }
48
49    /// Record a failed health check
50    pub fn record_failure(&mut self, reason: String) -> HealthStatus {
51        self.failure_count += 1;
52        self.last_check = Some(Instant::now());
53
54        warn!(
55            failure_count = self.failure_count,
56            max_failures = self.max_failures,
57            reason = %reason,
58            "Health check failed"
59        );
60
61        HealthStatus::Unhealthy { reason }
62    }
63
64    /// Check if the server should be marked as unhealthy
65    pub fn is_unhealthy(&self) -> bool {
66        self.failure_count >= self.max_failures
67    }
68
69    /// Reset health check state
70    pub fn reset(&mut self) {
71        self.last_check = None;
72        self.failure_count = 0;
73    }
74
75    /// Get the number of consecutive failures
76    pub fn failure_count(&self) -> u32 {
77        self.failure_count
78    }
79}
80
81impl Default for HealthChecker {
82    fn default() -> Self {
83        Self::new(Duration::from_secs(30))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_health_checker_creation() {
93        let checker = HealthChecker::new(Duration::from_secs(30));
94        assert!(checker.is_check_due());
95        assert_eq!(checker.failure_count(), 0);
96        assert!(!checker.is_unhealthy());
97    }
98
99    #[test]
100    fn test_health_check_success() {
101        let mut checker = HealthChecker::new(Duration::from_secs(30));
102        let status = checker.record_success(Duration::from_millis(50));
103
104        match status {
105            HealthStatus::Healthy { latency } => {
106                assert_eq!(latency, Duration::from_millis(50));
107            }
108            _ => panic!("Expected Healthy status"),
109        }
110
111        assert_eq!(checker.failure_count(), 0);
112        assert!(!checker.is_unhealthy());
113    }
114
115    #[test]
116    fn test_health_check_failures() {
117        let mut checker = HealthChecker::new(Duration::from_secs(30));
118
119        // First failure
120        checker.record_failure("timeout".to_string());
121        assert_eq!(checker.failure_count(), 1);
122        assert!(!checker.is_unhealthy());
123
124        // Second failure
125        checker.record_failure("timeout".to_string());
126        assert_eq!(checker.failure_count(), 2);
127        assert!(!checker.is_unhealthy());
128
129        // Third failure - should mark as unhealthy
130        checker.record_failure("timeout".to_string());
131        assert_eq!(checker.failure_count(), 3);
132        assert!(checker.is_unhealthy());
133    }
134
135    #[test]
136    fn test_health_check_reset() {
137        let mut checker = HealthChecker::new(Duration::from_secs(30));
138
139        checker.record_failure("timeout".to_string());
140        assert_eq!(checker.failure_count(), 1);
141
142        checker.reset();
143        assert_eq!(checker.failure_count(), 0);
144        assert!(checker.is_check_due());
145    }
146}