ricecoder_external_lsp/process/
health.rs1use crate::types::HealthStatus;
4use std::time::{Duration, Instant};
5use tracing::{debug, warn};
6
7pub struct HealthChecker {
9 last_check: Option<Instant>,
11 check_interval: Duration,
13 failure_count: u32,
15 max_failures: u32,
17}
18
19impl HealthChecker {
20 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 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 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 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 pub fn is_unhealthy(&self) -> bool {
66 self.failure_count >= self.max_failures
67 }
68
69 pub fn reset(&mut self) {
71 self.last_check = None;
72 self.failure_count = 0;
73 }
74
75 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 checker.record_failure("timeout".to_string());
121 assert_eq!(checker.failure_count(), 1);
122 assert!(!checker.is_unhealthy());
123
124 checker.record_failure("timeout".to_string());
126 assert_eq!(checker.failure_count(), 2);
127 assert!(!checker.is_unhealthy());
128
129 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}