1use crate::{DbHealthChecker, HealthReport, HealthSnapshot, HealthStatus, HealthStatusProvider};
15use serde::{Deserialize, Serialize};
16use std::collections::{HashMap, HashSet};
17use std::sync::{mpsc, Arc, Mutex, RwLock};
18use std::time::{Duration, Instant};
19
20struct CachedReport {
26 report: HealthReport,
27 cached_at: Instant,
28}
29
30struct CacheStats {
32 hits: u64,
34 misses: u64,
36 evictions: u64,
38}
39
40pub struct HealthCheckCache {
50 inner: Arc<dyn DbHealthChecker>,
52 ttl: Duration,
54 cache: RwLock<HashMap<String, CachedReport>>,
56 stats: Mutex<CacheStats>,
58}
59
60impl HealthCheckCache {
61 pub fn new(inner: Arc<dyn DbHealthChecker>, ttl: Duration) -> Self {
67 Self {
68 inner,
69 ttl,
70 cache: RwLock::new(HashMap::new()),
71 stats: Mutex::new(CacheStats {
72 hits: 0,
73 misses: 0,
74 evictions: 0,
75 }),
76 }
77 }
78
79 pub fn check(&self, pool: &str) -> HealthReport {
84 if let Ok(cache) = self.cache.read() {
86 if let Some(cached) = cache.get(pool) {
87 if cached.cached_at.elapsed() < self.ttl {
88 if let Ok(mut stats) = self.stats.lock() {
90 stats.hits += 1;
91 }
92 return cached.report.clone();
93 }
94 }
95 }
96
97 let report = self.inner.check(pool);
99 let cached = CachedReport {
100 report: report.clone(),
101 cached_at: Instant::now(),
102 };
103
104 if let Ok(mut cache) = self.cache.write() {
106 cache.insert(pool.to_string(), cached);
107 }
108 if let Ok(mut stats) = self.stats.lock() {
109 stats.misses += 1;
110 }
111
112 report
113 }
114
115 pub fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
117 pools.iter().map(|p| self.check(p)).collect()
118 }
119
120 pub fn invalidate(&self, pool: &str) -> bool {
122 let removed = if let Ok(mut cache) = self.cache.write() {
123 cache.remove(pool).is_some()
124 } else {
125 false
126 };
127 if removed {
128 if let Ok(mut stats) = self.stats.lock() {
129 stats.evictions += 1;
130 }
131 }
132 removed
133 }
134
135 pub fn clear(&self) {
137 if let Ok(mut cache) = self.cache.write() {
138 cache.clear();
139 }
140 }
141
142 pub fn stats(&self) -> (u64, u64, u64) {
144 if let Ok(stats) = self.stats.lock() {
145 (stats.hits, stats.misses, stats.evictions)
146 } else {
147 (0, 0, 0)
148 }
149 }
150
151 pub fn hit_rate(&self) -> f64 {
153 if let Ok(stats) = self.stats.lock() {
154 let total = stats.hits + stats.misses;
155 if total == 0 {
156 0.0
157 } else {
158 stats.hits as f64 / total as f64
159 }
160 } else {
161 0.0
162 }
163 }
164
165 pub fn ttl(&self) -> Duration {
167 self.ttl
168 }
169}
170
171#[derive(Debug, Clone)]
177pub struct CascadingReport {
178 pub report: HealthReport,
180 pub dependencies: Vec<HealthReport>,
182 pub all_healthy: bool,
184}
185
186pub struct CascadingHealthChecker {
196 checker: Arc<dyn DbHealthChecker>,
198 dependencies: RwLock<HashMap<String, Vec<String>>>,
200}
201
202impl CascadingHealthChecker {
203 pub fn new(checker: Arc<dyn DbHealthChecker>) -> Self {
205 Self {
206 checker,
207 dependencies: RwLock::new(HashMap::new()),
208 }
209 }
210
211 pub fn add_dependency(&self, pool: &str, depends_on: impl Into<String>) {
213 if let Ok(mut deps) = self.dependencies.write() {
214 deps.entry(pool.to_string())
215 .or_default()
216 .push(depends_on.into());
217 }
218 }
219
220 pub fn add_dependencies(&self, pool: &str, deps: Vec<String>) {
222 if let Ok(mut map) = self.dependencies.write() {
223 map.entry(pool.to_string()).or_default().extend(deps);
224 }
225 }
226
227 pub fn remove_dependency(&self, pool: &str, depends_on: &str) -> bool {
229 if let Ok(mut map) = self.dependencies.write() {
230 if let Some(deps) = map.get_mut(pool) {
231 let before = deps.len();
232 deps.retain(|d| d != depends_on);
233 return deps.len() < before;
234 }
235 }
236 false
237 }
238
239 pub fn dependencies(&self, pool: &str) -> Vec<String> {
241 if let Ok(map) = self.dependencies.read() {
242 map.get(pool).cloned().unwrap_or_default()
243 } else {
244 Vec::new()
245 }
246 }
247
248 pub fn clear_dependencies(&self, pool: &str) {
250 if let Ok(mut map) = self.dependencies.write() {
251 map.remove(pool);
252 }
253 }
254
255 pub fn check_with_deps(&self, pool: &str) -> CascadingReport {
259 let mut visited = HashSet::new();
260 visited.insert(pool.to_string());
261 let mut dep_reports = Vec::new();
262 self.collect_dep_reports(pool, &mut visited, &mut dep_reports);
263
264 let report = self.checker.check(pool);
265 let all_healthy = report.status == HealthStatus::Healthy
266 && dep_reports
267 .iter()
268 .all(|r| r.status == HealthStatus::Healthy);
269
270 CascadingReport {
271 report,
272 dependencies: dep_reports,
273 all_healthy,
274 }
275 }
276
277 fn collect_dep_reports(
279 &self,
280 pool: &str,
281 visited: &mut HashSet<String>,
282 reports: &mut Vec<HealthReport>,
283 ) {
284 let deps = self.dependencies(pool);
285 for dep in deps {
286 if visited.contains(&dep) {
287 continue;
289 }
290 visited.insert(dep.clone());
291 reports.push(self.checker.check(&dep));
292 self.collect_dep_reports(&dep, visited, reports);
293 }
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307pub enum ProbeKind {
308 Liveness,
310 Readiness,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ProbeResult {
317 pub kind: ProbeKind,
319 pub status: HealthStatus,
321 pub message: String,
323 pub timestamp: String,
325}
326
327pub struct ProbeManager {
333 liveness: RwLock<HashMap<String, HealthSnapshot>>,
335 readiness: RwLock<HashMap<String, HealthSnapshot>>,
337}
338
339impl Default for ProbeManager {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl ProbeManager {
346 pub fn new() -> Self {
348 Self {
349 liveness: RwLock::new(HashMap::new()),
350 readiness: RwLock::new(HashMap::new()),
351 }
352 }
353
354 pub fn set_liveness(&self, name: &str, snapshot: HealthSnapshot) {
356 if let Ok(mut map) = self.liveness.write() {
357 map.insert(name.to_string(), snapshot);
358 }
359 }
360
361 pub fn set_readiness(&self, name: &str, snapshot: HealthSnapshot) {
363 if let Ok(mut map) = self.readiness.write() {
364 map.insert(name.to_string(), snapshot);
365 }
366 }
367
368 pub fn check_liveness(&self, name: &str) -> ProbeResult {
370 let snapshot = self.read_probe(ProbeKind::Liveness, name);
371 ProbeResult {
372 kind: ProbeKind::Liveness,
373 status: snapshot.status,
374 message: snapshot.message,
375 timestamp: chrono::Utc::now().to_rfc3339(),
376 }
377 }
378
379 pub fn check_readiness(&self, name: &str) -> ProbeResult {
381 let snapshot = self.read_probe(ProbeKind::Readiness, name);
382 ProbeResult {
383 kind: ProbeKind::Readiness,
384 status: snapshot.status,
385 message: snapshot.message,
386 timestamp: chrono::Utc::now().to_rfc3339(),
387 }
388 }
389
390 pub fn liveness_all(&self) -> Vec<ProbeResult> {
392 self.all_probes(ProbeKind::Liveness)
393 }
394
395 pub fn readiness_all(&self) -> Vec<ProbeResult> {
397 self.all_probes(ProbeKind::Readiness)
398 }
399
400 pub fn overall_liveness(&self) -> HealthStatus {
402 self.overall(ProbeKind::Liveness)
403 }
404
405 pub fn overall_readiness(&self) -> HealthStatus {
407 self.overall(ProbeKind::Readiness)
408 }
409
410 fn read_probe(&self, kind: ProbeKind, name: &str) -> HealthSnapshot {
412 let map = match kind {
413 ProbeKind::Liveness => self.liveness.read(),
414 ProbeKind::Readiness => self.readiness.read(),
415 };
416 match map {
417 Ok(guard) => guard.get(name).cloned().unwrap_or_else(|| HealthSnapshot {
418 status: HealthStatus::Unknown,
419 connection_count: 0,
420 slow_queries: 0,
421 message: format!("no {:?} probe registered for '{}'", kind, name),
422 }),
423 Err(_) => HealthSnapshot {
424 status: HealthStatus::Unknown,
425 connection_count: 0,
426 slow_queries: 0,
427 message: "lock poisoned".to_string(),
428 },
429 }
430 }
431
432 fn all_probes(&self, kind: ProbeKind) -> Vec<ProbeResult> {
434 let map = match kind {
435 ProbeKind::Liveness => self.liveness.read(),
436 ProbeKind::Readiness => self.readiness.read(),
437 };
438 let timestamp = chrono::Utc::now().to_rfc3339();
439 match map {
440 Ok(guard) => {
441 let mut results: Vec<ProbeResult> = guard
442 .values()
443 .map(|snap| ProbeResult {
444 kind,
445 status: snap.status.clone(),
446 message: snap.message.clone(),
447 timestamp: timestamp.clone(),
448 })
449 .collect();
450 results.sort_by(|a, b| a.message.cmp(&b.message));
452 results
453 }
454 Err(_) => Vec::new(),
455 }
456 }
457
458 fn overall(&self, kind: ProbeKind) -> HealthStatus {
460 let map = match kind {
461 ProbeKind::Liveness => self.liveness.read(),
462 ProbeKind::Readiness => self.readiness.read(),
463 };
464 match map {
465 Ok(guard) => {
466 if guard.is_empty() {
467 return HealthStatus::Unknown;
468 }
469 let mut any_unknown = false;
470 for snap in guard.values() {
471 match snap.status {
472 HealthStatus::Unhealthy => return HealthStatus::Unhealthy,
473 HealthStatus::Unknown => any_unknown = true,
474 HealthStatus::Healthy => {}
475 }
476 }
477 if any_unknown {
478 HealthStatus::Unknown
479 } else {
480 HealthStatus::Healthy
481 }
482 }
483 Err(_) => HealthStatus::Unknown,
484 }
485 }
486}
487
488struct TimeoutStats {
494 total_checks: u64,
496 timeouts: u64,
498 total_duration: Duration,
500}
501
502#[derive(Debug, Clone)]
504pub struct TimeoutStatsSnapshot {
505 pub total_checks: u64,
507 pub timeouts: u64,
509 pub total_duration: Duration,
511 pub avg_duration: Duration,
513 pub timeout_rate: f64,
515}
516
517pub struct TimeoutHealthChecker {
530 inner: Arc<dyn HealthStatusProvider>,
532 timeout: Duration,
534 stats: Mutex<TimeoutStats>,
536}
537
538impl TimeoutHealthChecker {
539 pub fn new(inner: Arc<dyn HealthStatusProvider>, timeout: Duration) -> Self {
545 Self {
546 inner,
547 timeout,
548 stats: Mutex::new(TimeoutStats {
549 total_checks: 0,
550 timeouts: 0,
551 total_duration: Duration::ZERO,
552 }),
553 }
554 }
555
556 pub fn timeout(&self) -> Duration {
558 self.timeout
559 }
560
561 pub fn stats(&self) -> TimeoutStatsSnapshot {
563 if let Ok(stats) = self.stats.lock() {
564 let avg = if stats.total_checks > 0 {
565 stats.total_duration / stats.total_checks as u32
566 } else {
567 Duration::ZERO
568 };
569 let rate = if stats.total_checks > 0 {
570 stats.timeouts as f64 / stats.total_checks as f64
571 } else {
572 0.0
573 };
574 TimeoutStatsSnapshot {
575 total_checks: stats.total_checks,
576 timeouts: stats.timeouts,
577 total_duration: stats.total_duration,
578 avg_duration: avg,
579 timeout_rate: rate,
580 }
581 } else {
582 TimeoutStatsSnapshot {
583 total_checks: 0,
584 timeouts: 0,
585 total_duration: Duration::ZERO,
586 avg_duration: Duration::ZERO,
587 timeout_rate: 0.0,
588 }
589 }
590 }
591}
592
593impl HealthStatusProvider for TimeoutHealthChecker {
594 fn snapshot(&self, pool: &str) -> HealthSnapshot {
595 let start = Instant::now();
596
597 let (tx, rx) = mpsc::channel();
599 let inner = Arc::clone(&self.inner);
600 let pool_owned = pool.to_string();
601
602 std::thread::spawn(move || {
604 let result = inner.snapshot(&pool_owned);
605 let _ = tx.send(result);
606 });
607
608 let result = match rx.recv_timeout(self.timeout) {
610 Ok(snapshot) => snapshot,
611 Err(_) => {
612 HealthSnapshot {
614 status: HealthStatus::Unhealthy,
615 connection_count: 0,
616 slow_queries: 0,
617 message: format!(
618 "health check timed out after {:?} for pool '{}'",
619 self.timeout, pool
620 ),
621 }
622 }
623 };
624
625 let elapsed = start.elapsed();
627 if let Ok(mut stats) = self.stats.lock() {
628 stats.total_checks += 1;
629 stats.total_duration += elapsed;
630 if result.status == HealthStatus::Unhealthy && result.message.contains("timed out") {
631 stats.timeouts += 1;
632 }
633 }
634
635 result
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642 use std::sync::atomic::{AtomicU32, Ordering};
643
644 struct CountingChecker {
648 call_count: AtomicU32,
649 status: HealthStatus,
650 }
651
652 impl CountingChecker {
653 fn new(status: HealthStatus) -> Self {
654 Self {
655 call_count: AtomicU32::new(0),
656 status,
657 }
658 }
659
660 fn calls(&self) -> u32 {
661 self.call_count.load(Ordering::SeqCst)
662 }
663 }
664
665 impl DbHealthChecker for CountingChecker {
666 fn check(&self, pool: &str) -> HealthReport {
667 self.call_count.fetch_add(1, Ordering::SeqCst);
668 HealthReport::new(pool).set_status(self.status.clone())
669 }
670
671 fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
672 pools.iter().map(|p| self.check(p)).collect()
673 }
674 }
675
676 struct SlowProvider {
678 delay: Duration,
679 }
680
681 impl HealthStatusProvider for SlowProvider {
682 fn snapshot(&self, _pool: &str) -> HealthSnapshot {
683 std::thread::sleep(self.delay);
684 HealthSnapshot::healthy()
685 }
686 }
687
688 struct FastProvider;
690
691 impl HealthStatusProvider for FastProvider {
692 fn snapshot(&self, _pool: &str) -> HealthSnapshot {
693 HealthSnapshot::healthy()
694 }
695 }
696
697 #[test]
700 fn test_cache_first_call_misses() {
701 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
702 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
703
704 let report = cache.check("pool-a");
705 assert_eq!(report.status, HealthStatus::Healthy);
706 assert_eq!(checker.calls(), 1);
707
708 let (hits, misses, _) = cache.stats();
709 assert_eq!(hits, 0);
710 assert_eq!(misses, 1);
711 }
712
713 #[test]
714 fn test_cache_second_call_within_ttl_hits() {
715 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
716 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
717
718 cache.check("pool-a");
719 cache.check("pool-a");
720
721 assert_eq!(checker.calls(), 1);
723 let (hits, misses, _) = cache.stats();
724 assert_eq!(hits, 1);
725 assert_eq!(misses, 1);
726 }
727
728 #[test]
729 fn test_cache_expired_triggers_recheck() {
730 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
731 let cache = HealthCheckCache::new(checker.clone(), Duration::from_millis(50));
732
733 cache.check("pool-a");
734 std::thread::sleep(Duration::from_millis(60));
735 cache.check("pool-a");
736
737 assert_eq!(checker.calls(), 2);
739 let (hits, misses, _) = cache.stats();
740 assert_eq!(hits, 0);
741 assert_eq!(misses, 2);
742 }
743
744 #[test]
745 fn test_cache_invalidate_clears_entry() {
746 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
747 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
748
749 cache.check("pool-a");
750 assert!(cache.invalidate("pool-a"));
751 cache.check("pool-a");
752
753 assert_eq!(checker.calls(), 2);
755 let (_, _, evictions) = cache.stats();
756 assert_eq!(evictions, 1);
757 }
758
759 #[test]
760 fn test_cache_invalidate_missing_returns_false() {
761 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
762 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
763 assert!(!cache.invalidate("never-cached"));
764 }
765
766 #[test]
767 fn test_cache_clear_empties_all() {
768 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
769 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
770
771 cache.check("a");
772 cache.check("b");
773 cache.clear();
774 cache.check("a");
775
776 assert_eq!(checker.calls(), 3);
778 }
779
780 #[test]
781 fn test_cache_hit_rate() {
782 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
783 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
784
785 cache.check("p"); cache.check("p"); cache.check("p"); let rate = cache.hit_rate();
790 assert!((rate - 2.0 / 3.0).abs() < 0.01);
791 }
792
793 #[test]
794 fn test_cache_hit_rate_no_requests() {
795 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
796 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
797 assert_eq!(cache.hit_rate(), 0.0);
798 }
799
800 #[test]
801 fn test_cache_ttl_accessor() {
802 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
803 let cache = HealthCheckCache::new(checker, Duration::from_secs(30));
804 assert_eq!(cache.ttl(), Duration::from_secs(30));
805 }
806
807 #[test]
808 fn test_cache_check_all_uses_cache() {
809 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
810 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
811
812 cache.check_all(&["a", "b"]);
813 cache.check_all(&["a", "b"]);
814
815 assert_eq!(checker.calls(), 2);
817 }
818
819 #[test]
820 fn test_cache_different_pools_independent() {
821 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
822 let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
823
824 cache.check("a");
825 cache.check("b");
826 cache.check("a"); cache.check("b"); assert_eq!(checker.calls(), 2);
830 let (hits, misses, _) = cache.stats();
831 assert_eq!(hits, 2);
832 assert_eq!(misses, 2);
833 }
834
835 #[test]
838 fn test_cascading_no_deps_returns_own_report() {
839 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
840 let cascading = CascadingHealthChecker::new(checker);
841
842 let result = cascading.check_with_deps("pool-a");
843 assert_eq!(result.report.status, HealthStatus::Healthy);
844 assert!(result.dependencies.is_empty());
845 assert!(result.all_healthy);
846 }
847
848 #[test]
849 fn test_cascading_with_healthy_deps() {
850 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
851 let cascading = CascadingHealthChecker::new(checker);
852
853 cascading.add_dependency("app", "database");
854 cascading.add_dependency("app", "cache");
855
856 let result = cascading.check_with_deps("app");
857 assert_eq!(result.report.status, HealthStatus::Healthy);
858 assert_eq!(result.dependencies.len(), 2);
859 assert!(result.all_healthy);
860 }
861
862 #[test]
863 fn test_cascading_unhealthy_dependency_propagates() {
864 struct MixedChecker;
865 impl DbHealthChecker for MixedChecker {
866 fn check(&self, pool: &str) -> HealthReport {
867 if pool == "db-down" {
868 HealthReport::new(pool).set_status(HealthStatus::Unhealthy)
869 } else {
870 HealthReport::new(pool).set_healthy()
871 }
872 }
873 fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
874 pools.iter().map(|p| self.check(p)).collect()
875 }
876 }
877
878 let checker = Arc::new(MixedChecker);
879 let cascading = CascadingHealthChecker::new(checker);
880
881 cascading.add_dependency("app", "db-down");
882 let result = cascading.check_with_deps("app");
883 assert!(!result.all_healthy);
884 assert_eq!(result.dependencies.len(), 1);
885 assert_eq!(result.dependencies[0].status, HealthStatus::Unhealthy);
886 }
887
888 #[test]
889 fn test_cascading_nested_deps() {
890 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
891 let cascading = CascadingHealthChecker::new(checker);
892
893 cascading.add_dependency("app", "middleware");
894 cascading.add_dependency("middleware", "database");
895
896 let result = cascading.check_with_deps("app");
897 assert_eq!(result.dependencies.len(), 2);
898 assert!(result.all_healthy);
899 }
900
901 #[test]
902 fn test_cascading_circular_dependency_no_infinite_loop() {
903 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
904 let cascading = CascadingHealthChecker::new(checker);
905
906 cascading.add_dependency("a", "b");
908 cascading.add_dependency("b", "a");
909
910 let result = cascading.check_with_deps("a");
912 assert_eq!(result.dependencies.len(), 1); }
914
915 #[test]
916 fn test_cascading_remove_dependency() {
917 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
918 let cascading = CascadingHealthChecker::new(checker);
919
920 cascading.add_dependency("app", "db");
921 cascading.add_dependency("app", "cache");
922 assert_eq!(cascading.dependencies("app").len(), 2);
923
924 assert!(cascading.remove_dependency("app", "db"));
925 assert_eq!(cascading.dependencies("app").len(), 1);
926 assert_eq!(cascading.dependencies("app")[0], "cache");
927 }
928
929 #[test]
930 fn test_cascading_remove_missing_dependency_returns_false() {
931 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
932 let cascading = CascadingHealthChecker::new(checker);
933 assert!(!cascading.remove_dependency("app", "never-added"));
934 }
935
936 #[test]
937 fn test_cascading_clear_dependencies() {
938 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
939 let cascading = CascadingHealthChecker::new(checker);
940
941 cascading.add_dependency("app", "db");
942 cascading.add_dependency("app", "cache");
943 cascading.clear_dependencies("app");
944
945 assert!(cascading.dependencies("app").is_empty());
946 }
947
948 #[test]
949 fn test_cascading_add_dependencies_batch() {
950 let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
951 let cascading = CascadingHealthChecker::new(checker);
952
953 cascading.add_dependencies(
954 "app",
955 vec!["db".to_string(), "cache".to_string(), "queue".to_string()],
956 );
957 assert_eq!(cascading.dependencies("app").len(), 3);
958 }
959
960 #[test]
961 fn test_cascading_unknown_dependency_pool_returns_unknown() {
962 struct UnknownChecker;
963 impl DbHealthChecker for UnknownChecker {
964 fn check(&self, pool: &str) -> HealthReport {
965 HealthReport::new(pool) }
967 fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
968 pools.iter().map(|p| self.check(p)).collect()
969 }
970 }
971
972 let checker = Arc::new(UnknownChecker);
973 let cascading = CascadingHealthChecker::new(checker);
974 cascading.add_dependency("app", "unknown-dep");
975
976 let result = cascading.check_with_deps("app");
977 assert!(!result.all_healthy); }
979
980 #[test]
983 fn test_probe_manager_default_is_unknown() {
984 let mgr = ProbeManager::new();
985 let result = mgr.check_liveness("svc");
986 assert_eq!(result.status, HealthStatus::Unknown);
987 assert_eq!(result.kind, ProbeKind::Liveness);
988 assert!(!result.message.is_empty());
989 }
990
991 #[test]
992 fn test_probe_manager_set_liveness_healthy() {
993 let mgr = ProbeManager::new();
994 mgr.set_liveness("svc", HealthSnapshot::healthy());
995
996 let result = mgr.check_liveness("svc");
997 assert_eq!(result.status, HealthStatus::Healthy);
998 assert_eq!(result.kind, ProbeKind::Liveness);
999 }
1000
1001 #[test]
1002 fn test_probe_manager_set_readiness_unhealthy() {
1003 let mgr = ProbeManager::new();
1004 mgr.set_readiness("svc", HealthSnapshot::unhealthy("dependency down"));
1005
1006 let result = mgr.check_readiness("svc");
1007 assert_eq!(result.status, HealthStatus::Unhealthy);
1008 assert_eq!(result.message, "dependency down");
1009 assert_eq!(result.kind, ProbeKind::Readiness);
1010 }
1011
1012 #[test]
1013 fn test_probe_manager_liveness_and_readiness_independent() {
1014 let mgr = ProbeManager::new();
1015
1016 mgr.set_liveness("svc", HealthSnapshot::healthy());
1018 mgr.set_readiness("svc", HealthSnapshot::unhealthy("warming up"));
1019
1020 assert_eq!(mgr.check_liveness("svc").status, HealthStatus::Healthy);
1021 assert_eq!(mgr.check_readiness("svc").status, HealthStatus::Unhealthy);
1022 }
1023
1024 #[test]
1025 fn test_probe_manager_overall_liveness_all_healthy() {
1026 let mgr = ProbeManager::new();
1027 mgr.set_liveness("a", HealthSnapshot::healthy());
1028 mgr.set_liveness("b", HealthSnapshot::healthy());
1029 assert_eq!(mgr.overall_liveness(), HealthStatus::Healthy);
1030 }
1031
1032 #[test]
1033 fn test_probe_manager_overall_readiness_one_unhealthy() {
1034 let mgr = ProbeManager::new();
1035 mgr.set_readiness("a", HealthSnapshot::healthy());
1036 mgr.set_readiness("b", HealthSnapshot::unhealthy("down"));
1037 assert_eq!(mgr.overall_readiness(), HealthStatus::Unhealthy);
1038 }
1039
1040 #[test]
1041 fn test_probe_manager_overall_empty_returns_unknown() {
1042 let mgr = ProbeManager::new();
1043 assert_eq!(mgr.overall_liveness(), HealthStatus::Unknown);
1044 assert_eq!(mgr.overall_readiness(), HealthStatus::Unknown);
1045 }
1046
1047 #[test]
1048 fn test_probe_manager_overall_one_unknown_no_unhealthy() {
1049 let mgr = ProbeManager::new();
1050 mgr.set_liveness("a", HealthSnapshot::healthy());
1051 mgr.set_liveness("b", HealthSnapshot::unknown());
1052 assert_eq!(mgr.overall_liveness(), HealthStatus::Unknown);
1053 }
1054
1055 #[test]
1056 fn test_probe_manager_liveness_all_returns_all_probes() {
1057 let mgr = ProbeManager::new();
1058 mgr.set_liveness("a", HealthSnapshot::healthy());
1059 mgr.set_liveness("b", HealthSnapshot::healthy());
1060
1061 let results = mgr.liveness_all();
1062 assert_eq!(results.len(), 2);
1063 assert!(results.iter().all(|r| r.kind == ProbeKind::Liveness));
1064 }
1065
1066 #[test]
1067 fn test_probe_manager_readiness_all_returns_all_probes() {
1068 let mgr = ProbeManager::new();
1069 mgr.set_readiness("x", HealthSnapshot::healthy());
1070
1071 let results = mgr.readiness_all();
1072 assert_eq!(results.len(), 1);
1073 assert_eq!(results[0].kind, ProbeKind::Readiness);
1074 }
1075
1076 #[test]
1077 fn test_probe_result_serialization_roundtrip() {
1078 let result = ProbeResult {
1079 kind: ProbeKind::Readiness,
1080 status: HealthStatus::Unhealthy,
1081 message: "db down".to_string(),
1082 timestamp: "2024-01-01T00:00:00Z".to_string(),
1083 };
1084 let json = serde_json::to_string(&result).expect("serialize");
1085 let back: ProbeResult = serde_json::from_str(&json).expect("deserialize");
1086 assert_eq!(back.kind, ProbeKind::Readiness);
1087 assert_eq!(back.status, HealthStatus::Unhealthy);
1088 assert_eq!(back.message, "db down");
1089 }
1090
1091 #[test]
1092 fn test_probe_kind_eq() {
1093 assert_eq!(ProbeKind::Liveness, ProbeKind::Liveness);
1094 assert_ne!(ProbeKind::Liveness, ProbeKind::Readiness);
1095 }
1096
1097 #[test]
1100 fn test_timeout_checker_fast_provider_succeeds() {
1101 let provider = Arc::new(FastProvider);
1102 let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
1103
1104 let snap = checker.snapshot("pool");
1105 assert_eq!(snap.status, HealthStatus::Healthy);
1106
1107 let stats = checker.stats();
1108 assert_eq!(stats.total_checks, 1);
1109 assert_eq!(stats.timeouts, 0);
1110 assert_eq!(stats.timeout_rate, 0.0);
1111 }
1112
1113 #[test]
1114 fn test_timeout_checker_slow_provider_times_out() {
1115 let provider = Arc::new(SlowProvider {
1116 delay: Duration::from_millis(200),
1117 });
1118 let checker = TimeoutHealthChecker::new(provider, Duration::from_millis(50));
1119
1120 let snap = checker.snapshot("pool");
1121 assert_eq!(snap.status, HealthStatus::Unhealthy);
1122 assert!(snap.message.contains("timed out"));
1123
1124 let stats = checker.stats();
1125 assert_eq!(stats.total_checks, 1);
1126 assert_eq!(stats.timeouts, 1);
1127 assert!((stats.timeout_rate - 1.0).abs() < 0.01);
1128 }
1129
1130 #[test]
1131 fn test_timeout_checker_timeout_accessor() {
1132 let provider = Arc::new(FastProvider);
1133 let checker = TimeoutHealthChecker::new(provider, Duration::from_millis(500));
1134 assert_eq!(checker.timeout(), Duration::from_millis(500));
1135 }
1136
1137 #[test]
1138 fn test_timeout_checker_stats_avg_duration() {
1139 let provider = Arc::new(FastProvider);
1140 let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
1141
1142 checker.snapshot("a");
1143 checker.snapshot("b");
1144
1145 let stats = checker.stats();
1146 assert_eq!(stats.total_checks, 2);
1147 assert!(stats.avg_duration < Duration::from_millis(100));
1148 }
1149
1150 #[test]
1151 fn test_timeout_checker_stats_empty() {
1152 let provider = Arc::new(FastProvider);
1153 let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
1154
1155 let stats = checker.stats();
1156 assert_eq!(stats.total_checks, 0);
1157 assert_eq!(stats.timeouts, 0);
1158 assert_eq!(stats.timeout_rate, 0.0);
1159 assert_eq!(stats.avg_duration, Duration::ZERO);
1160 }
1161
1162 #[test]
1163 fn test_timeout_checker_implements_send_sync() {
1164 fn assert_send_sync<T: Send + Sync>() {}
1165 assert_send_sync::<TimeoutHealthChecker>();
1166 assert_send_sync::<HealthCheckCache>();
1167 assert_send_sync::<CascadingHealthChecker>();
1168 assert_send_sync::<ProbeManager>();
1169 }
1170}