1pub mod advanced;
19
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
26pub enum HealthStatus {
27 Healthy,
28 Unhealthy,
29 #[default]
30 Unknown,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HealthReport {
36 pub pool_name: String,
37 pub status: HealthStatus,
38 pub connection_count: u32,
39 pub slow_queries: u32,
40 pub message: String,
41 #[serde(default)]
43 pub error_rate: Option<f64>,
44 #[serde(default)]
46 pub p50_ms: Option<f64>,
47 #[serde(default)]
49 pub p95_ms: Option<f64>,
50 #[serde(default)]
52 pub p99_ms: Option<f64>,
53 #[serde(default)]
55 pub saturation: Option<f64>,
56 #[serde(default)]
58 pub uptime_ratio: Option<f64>,
59}
60
61impl HealthReport {
62 pub fn new(name: &str) -> Self {
63 Self {
64 pool_name: name.to_string(),
65 status: HealthStatus::Unknown,
66 connection_count: 0,
67 slow_queries: 0,
68 message: String::new(),
69 error_rate: None,
70 p50_ms: None,
71 p95_ms: None,
72 p99_ms: None,
73 saturation: None,
74 uptime_ratio: None,
75 }
76 }
77
78 pub fn set_healthy(mut self) -> Self {
79 self.status = HealthStatus::Healthy;
80 self
81 }
82
83 pub fn set_status(mut self, status: HealthStatus) -> Self {
84 self.status = status;
85 self
86 }
87
88 pub fn with_connection_count(mut self, count: u32) -> Self {
89 self.connection_count = count;
90 self
91 }
92
93 pub fn with_slow_queries(mut self, count: u32) -> Self {
94 self.slow_queries = count;
95 self
96 }
97
98 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
99 self.message = msg.into();
100 self
101 }
102
103 pub fn with_error_rate(mut self, rate: f64) -> Self {
104 self.error_rate = Some(rate);
105 self
106 }
107
108 pub fn with_latency_p50(mut self, ms: f64) -> Self {
109 self.p50_ms = Some(ms);
110 self
111 }
112
113 pub fn with_latency_p95(mut self, ms: f64) -> Self {
114 self.p95_ms = Some(ms);
115 self
116 }
117
118 pub fn with_latency_p99(mut self, ms: f64) -> Self {
119 self.p99_ms = Some(ms);
120 self
121 }
122
123 pub fn with_saturation(mut self, ratio: f64) -> Self {
124 self.saturation = Some(ratio);
125 self
126 }
127
128 pub fn with_uptime_ratio(mut self, ratio: f64) -> Self {
129 self.uptime_ratio = Some(ratio);
130 self
131 }
132}
133
134#[derive(Debug, Clone, Default)]
136pub struct HealthSnapshot {
137 pub status: HealthStatus,
138 pub connection_count: u32,
139 pub slow_queries: u32,
140 pub message: String,
141}
142
143impl HealthSnapshot {
144 pub fn healthy() -> Self {
145 Self {
146 status: HealthStatus::Healthy,
147 connection_count: 0,
148 slow_queries: 0,
149 message: String::new(),
150 }
151 }
152
153 pub fn unhealthy(message: impl Into<String>) -> Self {
154 Self {
155 status: HealthStatus::Unhealthy,
156 connection_count: 0,
157 slow_queries: 0,
158 message: message.into(),
159 }
160 }
161
162 pub fn unknown() -> Self {
163 Self {
164 status: HealthStatus::Unknown,
165 connection_count: 0,
166 slow_queries: 0,
167 message: String::new(),
168 }
169 }
170}
171
172pub trait HealthStatusProvider: Send + Sync {
176 fn snapshot(&self, pool: &str) -> HealthSnapshot;
177}
178
179pub trait DbHealthChecker: Send + Sync {
182 fn check(&self, pool: &str) -> HealthReport;
183 fn check_all(&self, pools: &[&str]) -> Vec<HealthReport>;
184}
185
186pub struct DefaultHealthChecker {
191 statuses: RwLock<HashMap<String, HealthSnapshot>>,
193 providers: RwLock<HashMap<String, Arc<dyn HealthStatusProvider>>>,
195}
196
197impl Default for DefaultHealthChecker {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203impl DefaultHealthChecker {
204 pub fn new() -> Self {
205 Self {
206 statuses: RwLock::new(HashMap::new()),
207 providers: RwLock::new(HashMap::new()),
208 }
209 }
210
211 pub fn set_status(&self, pool: &str, snapshot: HealthSnapshot) {
215 if let Ok(mut statuses) = self.statuses.write() {
217 statuses.insert(pool.to_string(), snapshot);
218 }
219 }
220
221 pub fn set_healthy(&self, pool: &str, connection_count: u32, slow_queries: u32) {
223 self.set_status(
224 pool,
225 HealthSnapshot {
226 status: HealthStatus::Healthy,
227 connection_count,
228 slow_queries,
229 message: String::new(),
230 },
231 );
232 }
233
234 pub fn set_unhealthy(&self, pool: &str, message: impl Into<String>) {
236 self.set_status(
237 pool,
238 HealthSnapshot {
239 status: HealthStatus::Unhealthy,
240 connection_count: 0,
241 slow_queries: 0,
242 message: message.into(),
243 },
244 );
245 }
246
247 pub fn register_provider(&self, pool: &str, provider: Arc<dyn HealthStatusProvider>) {
250 if let Ok(mut providers) = self.providers.write() {
252 providers.insert(pool.to_string(), provider);
253 }
254 }
255
256 pub fn unregister_provider(&self, pool: &str) -> bool {
258 match self.providers.write() {
260 Ok(mut providers) => providers.remove(pool).is_some(),
261 Err(_) => false,
262 }
263 }
264
265 fn read_snapshot(&self, pool: &str) -> HealthSnapshot {
268 if let Some(provider) = {
271 match self.providers.read() {
272 Ok(providers) => providers.get(pool).cloned(),
273 Err(_) => None,
274 }
275 } {
276 return provider.snapshot(pool);
277 }
278
279 let statuses = match self.statuses.read() {
280 Ok(s) => s,
281 Err(_) => {
282 return HealthSnapshot {
283 status: HealthStatus::Unknown,
284 connection_count: 0,
285 slow_queries: 0,
286 message: format!("lock poisoned for pool '{}'", pool),
287 }
288 }
289 };
290 if let Some(snap) = statuses.get(pool) {
291 return snap.clone();
292 }
293
294 HealthSnapshot {
295 status: HealthStatus::Unknown,
296 connection_count: 0,
297 slow_queries: 0,
298 message: format!("no status recorded for pool '{}'", pool),
299 }
300 }
301
302 pub fn overall_status(&self, pools: &[&str]) -> HealthStatus {
306 let mut any_unknown = false;
307 for pool in pools {
308 let snap = self.read_snapshot(pool);
309 match snap.status {
310 HealthStatus::Unhealthy => return HealthStatus::Unhealthy,
311 HealthStatus::Unknown => any_unknown = true,
312 HealthStatus::Healthy => {}
313 }
314 }
315 if any_unknown || pools.is_empty() {
316 HealthStatus::Unknown
317 } else {
318 HealthStatus::Healthy
319 }
320 }
321}
322
323impl DbHealthChecker for DefaultHealthChecker {
324 fn check(&self, pool: &str) -> HealthReport {
325 let snap = self.read_snapshot(pool);
326 HealthReport {
327 pool_name: pool.to_string(),
328 status: snap.status,
329 connection_count: snap.connection_count,
330 slow_queries: snap.slow_queries,
331 message: snap.message,
332 error_rate: None,
333 p50_ms: None,
334 p95_ms: None,
335 p99_ms: None,
336 saturation: None,
337 uptime_ratio: None,
338 }
339 }
340
341 fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
342 pools.iter().map(|p| self.check(p)).collect()
343 }
344}
345
346pub struct StaticStatusProvider {
349 snapshot: HealthSnapshot,
350}
351
352impl StaticStatusProvider {
353 pub fn new(snapshot: HealthSnapshot) -> Self {
354 Self { snapshot }
355 }
356}
357
358impl HealthStatusProvider for StaticStatusProvider {
359 fn snapshot(&self, _pool: &str) -> HealthSnapshot {
360 self.snapshot.clone()
361 }
362}
363
364pub struct ThresholdProvider {
367 connection_count: u32,
368 slow_queries: u32,
369 max_connections: u32,
370 max_slow_queries: u32,
371}
372
373impl ThresholdProvider {
374 pub fn new(
375 connection_count: u32,
376 slow_queries: u32,
377 max_connections: u32,
378 max_slow_queries: u32,
379 ) -> Self {
380 Self {
381 connection_count,
382 slow_queries,
383 max_connections,
384 max_slow_queries,
385 }
386 }
387}
388
389impl HealthStatusProvider for ThresholdProvider {
390 fn snapshot(&self, _pool: &str) -> HealthSnapshot {
391 if self.connection_count > self.max_connections {
392 return HealthSnapshot {
393 status: HealthStatus::Unhealthy,
394 connection_count: self.connection_count,
395 slow_queries: self.slow_queries,
396 message: format!(
397 "connection count {} exceeds max {}",
398 self.connection_count, self.max_connections
399 ),
400 };
401 }
402 if self.slow_queries > self.max_slow_queries {
403 return HealthSnapshot {
404 status: HealthStatus::Unhealthy,
405 connection_count: self.connection_count,
406 slow_queries: self.slow_queries,
407 message: format!(
408 "slow queries {} exceeds max {}",
409 self.slow_queries, self.max_slow_queries
410 ),
411 };
412 }
413 HealthSnapshot {
414 status: HealthStatus::Healthy,
415 connection_count: self.connection_count,
416 slow_queries: self.slow_queries,
417 message: String::new(),
418 }
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
430pub enum AlertLevel {
431 Info,
432 Warning,
433 Critical,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct HealthAlert {
440 pub level: AlertLevel,
441 pub pool_name: String,
442 pub message: String,
443 pub timestamp: chrono::DateTime<chrono::Utc>,
444 pub metrics: Option<HealthReport>,
445}
446
447impl HealthAlert {
448 pub fn new(
449 level: AlertLevel,
450 pool_name: impl Into<String>,
451 message: impl Into<String>,
452 ) -> Self {
453 Self {
454 level,
455 pool_name: pool_name.into(),
456 message: message.into(),
457 timestamp: chrono::Utc::now(),
458 metrics: None,
459 }
460 }
461
462 pub fn with_metrics(mut self, report: HealthReport) -> Self {
463 self.metrics = Some(report);
464 self
465 }
466}
467
468pub trait AlertChannel: Send + Sync {
472 fn send(&self, alert: &HealthAlert) -> Result<(), String>;
475 fn name(&self) -> &str;
477}
478
479pub struct LogAlertChannel {
481 name: String,
482}
483
484impl LogAlertChannel {
485 pub fn new() -> Self {
486 Self {
487 name: "log".to_string(),
488 }
489 }
490}
491
492impl Default for LogAlertChannel {
493 fn default() -> Self {
494 Self::new()
495 }
496}
497
498impl AlertChannel for LogAlertChannel {
499 fn send(&self, alert: &HealthAlert) -> Result<(), String> {
500 eprintln!(
501 "[{}] [{:?}] pool={} msg={}",
502 alert.timestamp.to_rfc3339(),
503 alert.level,
504 alert.pool_name,
505 alert.message
506 );
507 Ok(())
508 }
509
510 fn name(&self) -> &str {
511 &self.name
512 }
513}
514
515pub struct WebhookAlertChannel {
518 url: String,
519 name: String,
520 sent: RwLock<Vec<HealthAlert>>,
521}
522
523impl WebhookAlertChannel {
524 pub fn new(url: String) -> Self {
525 Self {
526 url,
527 name: "webhook".to_string(),
528 sent: RwLock::new(Vec::new()),
529 }
530 }
531
532 pub fn url(&self) -> &str {
533 &self.url
534 }
535
536 pub fn sent_alerts(&self) -> Vec<HealthAlert> {
537 self.sent.read().map(|g| g.clone()).unwrap_or_default()
539 }
540}
541
542impl AlertChannel for WebhookAlertChannel {
543 fn send(&self, alert: &HealthAlert) -> Result<(), String> {
544 let mut guard = self
547 .sent
548 .write()
549 .map_err(|e| format!("sent lock poisoned: {}", e))?;
550 guard.push(alert.clone());
551 Ok(())
552 }
553
554 fn name(&self) -> &str {
555 &self.name
556 }
557}
558
559pub struct ImAlertChannel {
562 webhook_url: String,
563 name: String,
564 sent: RwLock<Vec<HealthAlert>>,
565}
566
567impl ImAlertChannel {
568 pub fn new(webhook_url: String) -> Self {
569 Self {
570 webhook_url,
571 name: "im".to_string(),
572 sent: RwLock::new(Vec::new()),
573 }
574 }
575
576 pub fn webhook_url(&self) -> &str {
577 &self.webhook_url
578 }
579
580 pub fn sent_alerts(&self) -> Vec<HealthAlert> {
581 self.sent.read().map(|g| g.clone()).unwrap_or_default()
583 }
584}
585
586impl AlertChannel for ImAlertChannel {
587 fn send(&self, alert: &HealthAlert) -> Result<(), String> {
588 let mut guard = self
591 .sent
592 .write()
593 .map_err(|e| format!("sent lock poisoned: {}", e))?;
594 guard.push(alert.clone());
595 Ok(())
596 }
597
598 fn name(&self) -> &str {
599 &self.name
600 }
601}
602
603pub struct AlertManager {
608 channels: Vec<Arc<dyn AlertChannel>>,
609}
610
611impl Default for AlertManager {
612 fn default() -> Self {
613 Self::new()
614 }
615}
616
617impl AlertManager {
618 pub fn new() -> Self {
619 Self {
620 channels: Vec::new(),
621 }
622 }
623
624 pub fn register(&mut self, channel: Arc<dyn AlertChannel>) {
625 self.channels.push(channel);
626 }
627
628 pub fn notify(&self, alert: &HealthAlert) -> Vec<Result<(), String>> {
631 self.channels.iter().map(|c| c.send(alert)).collect()
632 }
633
634 pub fn channels(&self) -> Vec<String> {
635 self.channels.iter().map(|c| c.name().to_string()).collect()
636 }
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub enum FailoverAction {
642 StayOnPrimary,
643 FailoverToSecondary,
644 FailoverToTertiary,
645 CircuitOpen,
646}
647
648pub struct FailoverPolicy {
654 error_rate_threshold: f64,
655 latency_threshold_ms: f64,
656}
657
658impl Default for FailoverPolicy {
659 fn default() -> Self {
660 Self::new()
661 }
662}
663
664impl FailoverPolicy {
665 pub fn new() -> Self {
666 Self {
667 error_rate_threshold: 0.5,
668 latency_threshold_ms: 5000.0,
669 }
670 }
671
672 pub fn with_error_rate_threshold(mut self, threshold: f64) -> Self {
673 self.error_rate_threshold = threshold;
674 self
675 }
676
677 pub fn with_latency_threshold(mut self, threshold_ms: f64) -> Self {
678 self.latency_threshold_ms = threshold_ms;
679 self
680 }
681
682 pub fn evaluate(&self, report: &HealthReport) -> FailoverAction {
687 if report.status == HealthStatus::Unhealthy {
688 return FailoverAction::CircuitOpen;
689 }
690 let error_exceeded = report
691 .error_rate
692 .map(|r| r > self.error_rate_threshold)
693 .unwrap_or(false);
694 let latency_exceeded = report
695 .p99_ms
696 .map(|p| p > self.latency_threshold_ms)
697 .unwrap_or(false);
698 if error_exceeded || latency_exceeded {
699 FailoverAction::FailoverToSecondary
700 } else {
701 FailoverAction::StayOnPrimary
702 }
703 }
704}
705
706pub struct MultiRegionHealthView {
710 regions: HashMap<String, HealthStatus>,
711}
712
713impl Default for MultiRegionHealthView {
714 fn default() -> Self {
715 Self::new()
716 }
717}
718
719impl MultiRegionHealthView {
720 pub fn new() -> Self {
721 Self {
722 regions: HashMap::new(),
723 }
724 }
725
726 pub fn register(&mut self, region: &str, status: HealthStatus) {
727 self.regions.insert(region.to_string(), status);
728 }
729
730 pub fn aggregate(&self) -> HealthStatus {
731 if self.regions.is_empty() {
732 return HealthStatus::Unknown;
733 }
734 let mut any_unknown = false;
735 for status in self.regions.values() {
736 match status {
737 HealthStatus::Unhealthy => return HealthStatus::Unhealthy,
738 HealthStatus::Unknown => any_unknown = true,
739 HealthStatus::Healthy => {}
740 }
741 }
742 if any_unknown {
743 HealthStatus::Unknown
744 } else {
745 HealthStatus::Healthy
746 }
747 }
748
749 pub fn healthy_regions(&self) -> Vec<String> {
750 let mut out: Vec<String> = self
751 .regions
752 .iter()
753 .filter(|(_, s)| **s == HealthStatus::Healthy)
754 .map(|(k, _)| k.clone())
755 .collect();
756 out.sort();
757 out
758 }
759
760 pub fn unhealthy_regions(&self) -> Vec<String> {
761 let mut out: Vec<String> = self
762 .regions
763 .iter()
764 .filter(|(_, s)| **s == HealthStatus::Unhealthy)
765 .map(|(k, _)| k.clone())
766 .collect();
767 out.sort();
768 out
769 }
770}
771
772#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum CircuitState {
775 Closed,
777 Open,
779 HalfOpen,
781}
782
783pub struct CircuitBreaker {
787 failure_threshold: usize,
788 reset_timeout: std::time::Duration,
789 state: CircuitState,
790 consecutive_failures: usize,
791 last_failure_at: Option<std::time::Instant>,
792}
793
794impl CircuitBreaker {
795 pub fn new(failure_threshold: usize, reset_timeout: std::time::Duration) -> Self {
796 Self {
797 failure_threshold,
798 reset_timeout,
799 state: CircuitState::Closed,
800 consecutive_failures: 0,
801 last_failure_at: None,
802 }
803 }
804
805 pub fn state(&self) -> CircuitState {
806 self.state
807 }
808
809 pub fn record_success(&mut self) {
810 self.consecutive_failures = 0;
811 self.state = CircuitState::Closed;
812 self.last_failure_at = None;
813 }
814
815 pub fn record_failure(&mut self) {
816 self.consecutive_failures += 1;
817 self.last_failure_at = Some(std::time::Instant::now());
818 if self.consecutive_failures >= self.failure_threshold {
819 self.state = CircuitState::Open;
820 }
821 }
822
823 pub fn can_execute(&mut self) -> bool {
826 match self.state {
827 CircuitState::Closed => true,
828 CircuitState::HalfOpen => true,
829 CircuitState::Open => {
830 let elapsed = self
831 .last_failure_at
832 .map(|t| t.elapsed())
833 .unwrap_or_else(|| std::time::Duration::ZERO);
834 if elapsed >= self.reset_timeout {
835 self.state = CircuitState::HalfOpen;
836 true
837 } else {
838 false
839 }
840 }
841 }
842 }
843
844 pub fn reset(&mut self) -> bool {
854 let changed = self.state != CircuitState::Closed || self.consecutive_failures != 0;
855 self.state = CircuitState::Closed;
856 self.consecutive_failures = 0;
857 self.last_failure_at = None;
858 changed
859 }
860}
861
862pub struct BackupHealthProvider {
865 max_age: chrono::Duration,
866 last_backup: Option<chrono::DateTime<chrono::Utc>>,
867}
868
869impl BackupHealthProvider {
870 pub fn new(max_age: chrono::Duration) -> Self {
871 Self {
872 max_age,
873 last_backup: None,
874 }
875 }
876
877 pub fn set_last_backup(&mut self, timestamp: chrono::DateTime<chrono::Utc>) {
878 self.last_backup = Some(timestamp);
879 }
880
881 pub fn is_stale(&self) -> bool {
884 match self.last_backup {
885 None => true,
886 Some(ts) => {
887 let now = chrono::Utc::now();
888 let age = now.signed_duration_since(ts);
889 age > self.max_age
890 }
891 }
892 }
893
894 pub fn check(&self) -> HealthStatus {
895 if self.is_stale() {
896 HealthStatus::Unhealthy
897 } else {
898 HealthStatus::Healthy
899 }
900 }
901}
902
903pub async fn start_health_server(
914 checker: Arc<DefaultHealthChecker>,
915 pools: Vec<String>,
916 addr: std::net::SocketAddr,
917) -> Result<(), std::io::Error> {
918 use tokio::io::AsyncWriteExt;
919
920 let listener = tokio::net::TcpListener::bind(addr).await?;
921 loop {
922 let (mut stream, _) = listener.accept().await?;
923 let checker = checker.clone();
924 let pools = pools.clone();
925 tokio::spawn(async move {
926 let pool_refs: Vec<&str> = pools.iter().map(|s| s.as_str()).collect();
927 let reports = checker.check_all(&pool_refs);
928 let overall = checker.overall_status(&pool_refs);
929 let json = serde_json::to_string(&reports).unwrap_or_default();
930 let status = if overall == HealthStatus::Healthy {
931 "200 OK"
932 } else {
933 "503 Service Unavailable"
934 };
935 let response = format!(
936 "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
937 status,
938 json.len(),
939 json
940 );
941 let _ = stream.write_all(response.as_bytes()).await;
942 });
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use std::sync::atomic::{AtomicU32, Ordering};
950
951 #[test]
952 fn test_report_builder() {
953 let r = HealthReport::new("main")
954 .set_healthy()
955 .with_connection_count(5)
956 .with_slow_queries(1)
957 .with_message("ok");
958 assert_eq!(r.pool_name, "main");
959 assert_eq!(r.status, HealthStatus::Healthy);
960 assert_eq!(r.connection_count, 5);
961 assert_eq!(r.slow_queries, 1);
962 assert_eq!(r.message, "ok");
963 }
964
965 #[test]
966 fn test_check_unknown_pool_returns_unknown() {
967 let checker = DefaultHealthChecker::new();
968 let report = checker.check("never-set");
969 assert_eq!(report.status, HealthStatus::Unknown);
970 assert_eq!(report.connection_count, 0);
971 assert_eq!(report.slow_queries, 0);
972 assert!(!report.message.is_empty());
973 }
974
975 #[test]
976 fn test_set_status_healthy() {
977 let checker = DefaultHealthChecker::new();
978 checker.set_healthy("pool-a", 10, 2);
979 let report = checker.check("pool-a");
980 assert_eq!(report.status, HealthStatus::Healthy);
981 assert_eq!(report.connection_count, 10);
982 assert_eq!(report.slow_queries, 2);
983 assert_eq!(report.pool_name, "pool-a");
984 }
985
986 #[test]
987 fn test_set_status_unhealthy() {
988 let checker = DefaultHealthChecker::new();
989 checker.set_unhealthy("pool-b", "connection refused");
990 let report = checker.check("pool-b");
991 assert_eq!(report.status, HealthStatus::Unhealthy);
992 assert_eq!(report.message, "connection refused");
993 }
994
995 #[test]
996 fn test_set_status_with_snapshot() {
997 let checker = DefaultHealthChecker::new();
998 checker.set_status(
999 "pool-c",
1000 HealthSnapshot {
1001 status: HealthStatus::Healthy,
1002 connection_count: 42,
1003 slow_queries: 7,
1004 message: "all good".to_string(),
1005 },
1006 );
1007 let report = checker.check("pool-c");
1008 assert_eq!(report.status, HealthStatus::Healthy);
1009 assert_eq!(report.connection_count, 42);
1010 assert_eq!(report.slow_queries, 7);
1011 assert_eq!(report.message, "all good");
1012 }
1013
1014 #[test]
1015 fn test_overwrite_status() {
1016 let checker = DefaultHealthChecker::new();
1017 checker.set_healthy("pool-d", 1, 0);
1018 assert_eq!(checker.check("pool-d").status, HealthStatus::Healthy);
1019
1020 checker.set_unhealthy("pool-d", "down");
1021 let report = checker.check("pool-d");
1022 assert_eq!(report.status, HealthStatus::Unhealthy);
1023 assert_eq!(report.message, "down");
1024 }
1025
1026 #[test]
1027 fn test_check_all_aggregates() {
1028 let checker = DefaultHealthChecker::new();
1029 checker.set_healthy("p1", 1, 0);
1030 checker.set_unhealthy("p2", "timeout");
1031 checker.set_status("p3", HealthSnapshot::unknown());
1032
1033 let reports = checker.check_all(&["p1", "p2", "p3"]);
1034 assert_eq!(reports.len(), 3);
1035 assert_eq!(reports[0].pool_name, "p1");
1036 assert_eq!(reports[0].status, HealthStatus::Healthy);
1037 assert_eq!(reports[1].status, HealthStatus::Unhealthy);
1038 assert_eq!(reports[2].status, HealthStatus::Unknown);
1039 }
1040
1041 #[test]
1042 fn test_overall_status_all_healthy() {
1043 let checker = DefaultHealthChecker::new();
1044 checker.set_healthy("a", 1, 0);
1045 checker.set_healthy("b", 2, 0);
1046 assert_eq!(checker.overall_status(&["a", "b"]), HealthStatus::Healthy);
1047 }
1048
1049 #[test]
1050 fn test_overall_status_one_unhealthy() {
1051 let checker = DefaultHealthChecker::new();
1052 checker.set_healthy("a", 1, 0);
1053 checker.set_unhealthy("b", "down");
1054 checker.set_healthy("c", 3, 0);
1055 assert_eq!(
1056 checker.overall_status(&["a", "b", "c"]),
1057 HealthStatus::Unhealthy
1058 );
1059 }
1060
1061 #[test]
1062 fn test_overall_status_one_unknown() {
1063 let checker = DefaultHealthChecker::new();
1064 checker.set_healthy("a", 1, 0);
1065 assert_eq!(checker.overall_status(&["a", "b"]), HealthStatus::Unknown);
1067 }
1068
1069 #[test]
1070 fn test_overall_status_empty_pools() {
1071 let checker = DefaultHealthChecker::new();
1072 assert_eq!(checker.overall_status(&[]), HealthStatus::Unknown);
1073 }
1074
1075 #[test]
1076 fn test_static_provider_overrides_manual_status() {
1077 let checker = DefaultHealthChecker::new();
1078 checker.set_unhealthy("p", "manual");
1080
1081 checker.register_provider(
1083 "p",
1084 Arc::new(StaticStatusProvider::new(HealthSnapshot {
1085 status: HealthStatus::Healthy,
1086 connection_count: 9,
1087 slow_queries: 1,
1088 message: "from provider".to_string(),
1089 })),
1090 );
1091
1092 let report = checker.check("p");
1093 assert_eq!(report.status, HealthStatus::Healthy);
1094 assert_eq!(report.connection_count, 9);
1095 assert_eq!(report.slow_queries, 1);
1096 assert_eq!(report.message, "from provider");
1097 }
1098
1099 #[test]
1100 fn test_unregister_provider_falls_back_to_manual() {
1101 let checker = DefaultHealthChecker::new();
1102 checker.set_healthy("p", 1, 0);
1103 checker.register_provider(
1104 "p",
1105 Arc::new(StaticStatusProvider::new(HealthSnapshot::unhealthy(
1106 "from provider",
1107 ))),
1108 );
1109
1110 assert_eq!(checker.check("p").status, HealthStatus::Unhealthy);
1111
1112 assert!(checker.unregister_provider("p"));
1113 let report = checker.check("p");
1114 assert_eq!(report.status, HealthStatus::Healthy);
1115 assert_eq!(report.connection_count, 1);
1116 }
1117
1118 #[test]
1119 fn test_unregister_missing_provider_returns_false() {
1120 let checker = DefaultHealthChecker::new();
1121 assert!(!checker.unregister_provider("never-registered"));
1122 }
1123
1124 #[test]
1125 fn test_threshold_provider_healthy() {
1126 let provider = ThresholdProvider::new(5, 1, 10, 3);
1127 let snap = provider.snapshot("pool");
1128 assert_eq!(snap.status, HealthStatus::Healthy);
1129 assert_eq!(snap.connection_count, 5);
1130 assert_eq!(snap.slow_queries, 1);
1131 }
1132
1133 #[test]
1134 fn test_threshold_provider_connection_overflow() {
1135 let provider = ThresholdProvider::new(11, 0, 10, 3);
1136 let snap = provider.snapshot("pool");
1137 assert_eq!(snap.status, HealthStatus::Unhealthy);
1138 assert!(snap.message.contains("connection count 11 exceeds max 10"));
1139 }
1140
1141 #[test]
1142 fn test_threshold_provider_slow_queries_overflow() {
1143 let provider = ThresholdProvider::new(5, 5, 10, 3);
1144 let snap = provider.snapshot("pool");
1145 assert_eq!(snap.status, HealthStatus::Unhealthy);
1146 assert!(snap.message.contains("slow queries 5 exceeds max 3"));
1147 }
1148
1149 #[test]
1150 fn test_threshold_provider_boundary_equal_is_healthy() {
1151 let provider = ThresholdProvider::new(10, 3, 10, 3);
1153 let snap = provider.snapshot("pool");
1154 assert_eq!(snap.status, HealthStatus::Healthy);
1155 }
1156
1157 #[test]
1158 fn test_threshold_provider_via_checker() {
1159 let checker = DefaultHealthChecker::new();
1160 checker.register_provider("pool", Arc::new(ThresholdProvider::new(20, 0, 10, 3)));
1161 let report = checker.check("pool");
1162 assert_eq!(report.status, HealthStatus::Unhealthy);
1163 assert_eq!(report.connection_count, 20);
1164 assert_eq!(report.slow_queries, 0);
1165 }
1166
1167 #[test]
1168 fn test_check_all_with_providers_mixed() {
1169 let checker = DefaultHealthChecker::new();
1170 checker.register_provider(
1171 "healthy-pool",
1172 Arc::new(StaticStatusProvider::new(HealthSnapshot {
1173 status: HealthStatus::Healthy,
1174 connection_count: 3,
1175 slow_queries: 0,
1176 message: String::new(),
1177 })),
1178 );
1179 checker.register_provider(
1180 "unhealthy-pool",
1181 Arc::new(StaticStatusProvider::new(HealthSnapshot::unhealthy(
1182 "provider says down",
1183 ))),
1184 );
1185
1186 let reports = checker.check_all(&["healthy-pool", "unhealthy-pool", "unknown-pool"]);
1187 assert_eq!(reports.len(), 3);
1188 assert_eq!(reports[0].status, HealthStatus::Healthy);
1189 assert_eq!(reports[1].status, HealthStatus::Unhealthy);
1190 assert_eq!(reports[2].status, HealthStatus::Unknown);
1191 assert_eq!(
1192 checker.overall_status(&["healthy-pool", "unhealthy-pool", "unknown-pool"]),
1193 HealthStatus::Unhealthy
1194 );
1195 }
1196
1197 struct DynamicProvider {
1200 counter: AtomicU32,
1201 statuses: Vec<HealthSnapshot>,
1202 }
1203
1204 impl DynamicProvider {
1205 fn new(statuses: Vec<HealthSnapshot>) -> Self {
1206 Self {
1207 counter: AtomicU32::new(0),
1208 statuses,
1209 }
1210 }
1211 }
1212
1213 impl HealthStatusProvider for DynamicProvider {
1214 fn snapshot(&self, _pool: &str) -> HealthSnapshot {
1215 let idx = self.counter.fetch_add(1, Ordering::SeqCst) as usize;
1216 self.statuses
1217 .get(idx)
1218 .cloned()
1219 .unwrap_or_else(|| HealthSnapshot {
1220 status: HealthStatus::Unknown,
1221 connection_count: 0,
1222 slow_queries: 0,
1223 message: "no more snapshots".to_string(),
1224 })
1225 }
1226 }
1227
1228 #[test]
1229 fn test_dynamic_provider_changes_over_time() {
1230 let provider = Arc::new(DynamicProvider::new(vec![
1231 HealthSnapshot::healthy(),
1232 HealthSnapshot::unhealthy("flap"),
1233 HealthSnapshot::healthy(),
1234 ]));
1235 let checker = DefaultHealthChecker::new();
1236 checker.register_provider("flapping", provider);
1237
1238 let r1 = checker.check("flapping");
1239 let r2 = checker.check("flapping");
1240 let r3 = checker.check("flapping");
1241 let r4 = checker.check("flapping");
1242
1243 assert_eq!(r1.status, HealthStatus::Healthy);
1244 assert_eq!(r2.status, HealthStatus::Unhealthy);
1245 assert_eq!(r2.message, "flap");
1246 assert_eq!(r3.status, HealthStatus::Healthy);
1247 assert_eq!(r4.status, HealthStatus::Unknown);
1248 }
1249
1250 #[test]
1251 fn test_thread_safe_concurrent_checks() {
1252 use std::thread;
1253 let checker = Arc::new(DefaultHealthChecker::new());
1254 checker.set_healthy("shared", 5, 0);
1255
1256 let mut handles = vec![];
1257 for _ in 0..4 {
1258 let c = checker.clone();
1259 handles.push(thread::spawn(move || {
1260 let report = c.check("shared");
1261 assert_eq!(report.status, HealthStatus::Healthy);
1262 assert_eq!(report.connection_count, 5);
1263 }));
1264 }
1265 for h in handles {
1266 h.join().expect("thread panicked");
1267 }
1268 }
1269
1270 #[test]
1271 fn test_state_transitions_observed_via_check() {
1272 let checker = DefaultHealthChecker::new();
1274 let mut observed = Vec::<HealthStatus>::new();
1275
1276 checker.set_healthy("p", 1, 0);
1277 observed.push(checker.check("p").status);
1278 checker.set_unhealthy("p", "x");
1279 observed.push(checker.check("p").status);
1280 checker.set_status("p", HealthSnapshot::unknown());
1281 observed.push(checker.check("p").status);
1282
1283 assert_eq!(
1284 observed,
1285 vec![
1286 HealthStatus::Healthy,
1287 HealthStatus::Unhealthy,
1288 HealthStatus::Unknown,
1289 ]
1290 );
1291 }
1292
1293 #[test]
1294 fn test_serialization_roundtrip() {
1295 let report = HealthReport::new("pool")
1296 .set_healthy()
1297 .with_connection_count(7)
1298 .with_slow_queries(2)
1299 .with_message("ok");
1300 let json = serde_json::to_string(&report).expect("serialize");
1301 let back: HealthReport = serde_json::from_str(&json).expect("deserialize");
1302 assert_eq!(back.pool_name, "pool");
1303 assert_eq!(back.status, HealthStatus::Healthy);
1304 assert_eq!(back.connection_count, 7);
1305 assert_eq!(back.slow_queries, 2);
1306 assert_eq!(back.message, "ok");
1307 }
1308
1309 #[test]
1310 fn test_health_status_eq() {
1311 assert_eq!(HealthStatus::Healthy, HealthStatus::Healthy);
1312 assert_ne!(HealthStatus::Healthy, HealthStatus::Unhealthy);
1313 assert_ne!(HealthStatus::Unhealthy, HealthStatus::Unknown);
1314 }
1315
1316 #[test]
1317 fn test_db_health_checker_via_trait_object() {
1318 let checker: Box<dyn DbHealthChecker> = Box::new(DefaultHealthChecker::new());
1319 let report = checker.check("nothing");
1321 assert_eq!(report.status, HealthStatus::Unknown);
1322 let reports = checker.check_all(&["a", "b"]);
1323 assert_eq!(reports.len(), 2);
1324 assert_eq!(reports[0].status, HealthStatus::Unknown);
1325 }
1326
1327 #[test]
1328 fn test_snapshot_default_is_unknown() {
1329 let snap = HealthSnapshot::default();
1330 assert_eq!(snap.status, HealthStatus::Unknown);
1331 assert_eq!(snap.connection_count, 0);
1332 assert_eq!(snap.slow_queries, 0);
1333 assert!(snap.message.is_empty());
1334 }
1335
1336 #[test]
1337 fn test_snapshot_constructors() {
1338 let h = HealthSnapshot::healthy();
1339 assert_eq!(h.status, HealthStatus::Healthy);
1340
1341 let u = HealthSnapshot::unhealthy("err");
1342 assert_eq!(u.status, HealthStatus::Unhealthy);
1343 assert_eq!(u.message, "err");
1344
1345 let n = HealthSnapshot::unknown();
1346 assert_eq!(n.status, HealthStatus::Unknown);
1347 }
1348
1349 #[test]
1352 fn test_report_sla_fields_default_none() {
1353 let r = HealthReport::new("p");
1354 assert_eq!(r.error_rate, None);
1355 assert_eq!(r.p50_ms, None);
1356 assert_eq!(r.p95_ms, None);
1357 assert_eq!(r.p99_ms, None);
1358 assert_eq!(r.saturation, None);
1359 assert_eq!(r.uptime_ratio, None);
1360 }
1361
1362 #[test]
1363 fn test_report_sla_builders_set_values() {
1364 let r = HealthReport::new("p")
1365 .set_healthy()
1366 .with_error_rate(0.01)
1367 .with_latency_p50(10.0)
1368 .with_latency_p95(50.0)
1369 .with_latency_p99(100.0)
1370 .with_saturation(0.7)
1371 .with_uptime_ratio(0.999);
1372 assert_eq!(r.error_rate, Some(0.01));
1373 assert_eq!(r.p50_ms, Some(10.0));
1374 assert_eq!(r.p95_ms, Some(50.0));
1375 assert_eq!(r.p99_ms, Some(100.0));
1376 assert_eq!(r.saturation, Some(0.7));
1377 assert_eq!(r.uptime_ratio, Some(0.999));
1378 }
1379
1380 #[test]
1381 fn test_report_sla_fields_serialize_roundtrip() {
1382 let r = HealthReport::new("p")
1383 .set_healthy()
1384 .with_error_rate(0.05)
1385 .with_latency_p99(200.0);
1386 let json = serde_json::to_string(&r).expect("serialize");
1387 let back: HealthReport = serde_json::from_str(&json).expect("deserialize");
1388 assert_eq!(back.error_rate, Some(0.05));
1389 assert_eq!(back.p99_ms, Some(200.0));
1390 assert_eq!(back.p50_ms, None);
1391 }
1392
1393 #[test]
1394 fn test_report_backward_compat_old_json_deserializes() {
1395 let old_json = r#"{
1398 "pool_name": "legacy",
1399 "status": "Healthy",
1400 "connection_count": 3,
1401 "slow_queries": 0,
1402 "message": "ok"
1403 }"#;
1404 let back: HealthReport = serde_json::from_str(old_json).expect("deserialize legacy");
1405 assert_eq!(back.pool_name, "legacy");
1406 assert_eq!(back.status, HealthStatus::Healthy);
1407 assert_eq!(back.connection_count, 3);
1408 assert_eq!(back.error_rate, None);
1409 assert_eq!(back.p99_ms, None);
1410 assert_eq!(back.uptime_ratio, None);
1411 }
1412
1413 #[test]
1414 fn test_default_checker_check_sla_fields_none() {
1415 let checker = DefaultHealthChecker::new();
1416 checker.set_healthy("p", 1, 0);
1417 let r = checker.check("p");
1418 assert_eq!(r.error_rate, None);
1419 assert_eq!(r.p99_ms, None);
1420 }
1421
1422 #[test]
1425 fn test_alert_level_variants() {
1426 assert_ne!(AlertLevel::Info, AlertLevel::Warning);
1427 assert_ne!(AlertLevel::Warning, AlertLevel::Critical);
1428 assert_ne!(AlertLevel::Info, AlertLevel::Critical);
1429 }
1430
1431 #[test]
1432 fn test_health_alert_new_defaults() {
1433 let alert = HealthAlert::new(AlertLevel::Warning, "pool-a", "high latency");
1434 assert_eq!(alert.level, AlertLevel::Warning);
1435 assert_eq!(alert.pool_name, "pool-a");
1436 assert_eq!(alert.message, "high latency");
1437 assert!(alert.metrics.is_none());
1438 assert!(alert.timestamp.timestamp() > 0);
1440 }
1441
1442 #[test]
1443 fn test_health_alert_with_metrics() {
1444 let report = HealthReport::new("p").with_error_rate(0.9);
1445 let alert =
1446 HealthAlert::new(AlertLevel::Critical, "p", "error rate high").with_metrics(report);
1447 assert!(alert.metrics.is_some());
1448 let m = alert.metrics.expect("metrics present");
1449 assert_eq!(m.error_rate, Some(0.9));
1450 }
1451
1452 #[test]
1453 fn test_health_alert_serialization_roundtrip() {
1454 let alert = HealthAlert::new(AlertLevel::Critical, "p", "down")
1455 .with_metrics(HealthReport::new("p").with_latency_p99(999.0));
1456 let json = serde_json::to_string(&alert).expect("serialize");
1457 let back: HealthAlert = serde_json::from_str(&json).expect("deserialize");
1458 assert_eq!(back.level, AlertLevel::Critical);
1459 assert_eq!(back.pool_name, "p");
1460 assert_eq!(back.message, "down");
1461 assert!(back.metrics.is_some());
1462 assert_eq!(back.metrics.expect("metrics").p99_ms, Some(999.0));
1463 }
1464
1465 #[test]
1468 fn test_log_alert_channel_send_ok() {
1469 let ch = LogAlertChannel::new();
1470 let alert = HealthAlert::new(AlertLevel::Info, "p", "hi");
1471 assert!(ch.send(&alert).is_ok());
1472 assert_eq!(ch.name(), "log");
1473 }
1474
1475 #[test]
1476 fn test_webhook_alert_channel_records_alerts() {
1477 let ch = WebhookAlertChannel::new("https://example.com/hook".to_string());
1478 assert_eq!(ch.name(), "webhook");
1479 assert_eq!(ch.url(), "https://example.com/hook");
1480 assert!(ch.sent_alerts().is_empty());
1481
1482 let a1 = HealthAlert::new(AlertLevel::Warning, "p", "w");
1483 let a2 = HealthAlert::new(AlertLevel::Critical, "p", "c");
1484 assert!(ch.send(&a1).is_ok());
1485 assert!(ch.send(&a2).is_ok());
1486
1487 let sent = ch.sent_alerts();
1488 assert_eq!(sent.len(), 2);
1489 assert_eq!(sent[0].level, AlertLevel::Warning);
1490 assert_eq!(sent[1].level, AlertLevel::Critical);
1491 }
1492
1493 #[test]
1494 fn test_im_alert_channel_records_alerts() {
1495 let ch = ImAlertChannel::new("https://im.example.com/bot".to_string());
1496 assert_eq!(ch.name(), "im");
1497 assert_eq!(ch.webhook_url(), "https://im.example.com/bot");
1498 assert!(ch.sent_alerts().is_empty());
1499
1500 let a = HealthAlert::new(AlertLevel::Critical, "p", "down");
1501 assert!(ch.send(&a).is_ok());
1502 assert_eq!(ch.sent_alerts().len(), 1);
1503 assert_eq!(ch.sent_alerts()[0].message, "down");
1504 }
1505
1506 #[test]
1507 fn test_alert_channel_via_trait_object() {
1508 let ch: Arc<dyn AlertChannel> = Arc::new(LogAlertChannel::new());
1509 let alert = HealthAlert::new(AlertLevel::Info, "p", "x");
1510 assert!(ch.send(&alert).is_ok());
1511 assert_eq!(ch.name(), "log");
1512 }
1513
1514 #[test]
1517 fn test_alert_manager_empty_notify_returns_empty() {
1518 let mgr = AlertManager::new();
1519 let alert = HealthAlert::new(AlertLevel::Info, "p", "x");
1520 let results = mgr.notify(&alert);
1521 assert!(results.is_empty());
1522 assert!(mgr.channels().is_empty());
1523 }
1524
1525 #[test]
1526 fn test_alert_manager_register_and_notify() {
1527 let mut mgr = AlertManager::new();
1528 let log = Arc::new(LogAlertChannel::new());
1529 let webhook = Arc::new(WebhookAlertChannel::new("https://h".to_string()));
1530 let im = Arc::new(ImAlertChannel::new("https://i".to_string()));
1531 mgr.register(log);
1532 mgr.register(webhook);
1533 mgr.register(im);
1534
1535 let names = mgr.channels();
1536 assert_eq!(names, vec!["log", "webhook", "im"]);
1537
1538 let alert = HealthAlert::new(AlertLevel::Critical, "p", "down");
1539 let results = mgr.notify(&alert);
1540 assert_eq!(results.len(), 3);
1541 for r in &results {
1542 assert!(r.is_ok());
1543 }
1544 }
1545
1546 #[test]
1547 fn test_alert_manager_notify_partial_failure() {
1548 struct FailingChannel;
1549 impl AlertChannel for FailingChannel {
1550 fn send(&self, _alert: &HealthAlert) -> Result<(), String> {
1551 Err("network error".to_string())
1552 }
1553 fn name(&self) -> &str {
1554 "failing"
1555 }
1556 }
1557
1558 let mut mgr = AlertManager::new();
1559 mgr.register(Arc::new(LogAlertChannel::new()));
1560 mgr.register(Arc::new(FailingChannel));
1561 mgr.register(Arc::new(WebhookAlertChannel::new("u".to_string())));
1562
1563 let alert = HealthAlert::new(AlertLevel::Warning, "p", "x");
1564 let results = mgr.notify(&alert);
1565 assert_eq!(results.len(), 3);
1566 assert!(results[0].is_ok());
1567 assert!(results[1].is_err());
1568 assert!(results[2].is_ok());
1569 }
1570
1571 #[test]
1572 fn test_alert_manager_send_sync() {
1573 fn assert_send_sync<T: Send + Sync>() {}
1574 assert_send_sync::<AlertManager>();
1575 }
1576
1577 #[test]
1580 fn test_failover_policy_defaults_stay_on_primary() {
1581 let policy = FailoverPolicy::new();
1582 let r = HealthReport::new("p").set_healthy();
1583 assert_eq!(policy.evaluate(&r), FailoverAction::StayOnPrimary);
1584 }
1585
1586 #[test]
1587 fn test_failover_policy_unhealthy_circuit_open() {
1588 let policy = FailoverPolicy::new();
1589 let r = HealthReport::new("p").set_status(HealthStatus::Unhealthy);
1590 assert_eq!(policy.evaluate(&r), FailoverAction::CircuitOpen);
1591 }
1592
1593 #[test]
1594 fn test_failover_policy_error_rate_exceeds_threshold() {
1595 let policy = FailoverPolicy::new();
1596 let r = HealthReport::new("p").set_healthy().with_error_rate(0.6);
1597 assert_eq!(policy.evaluate(&r), FailoverAction::FailoverToSecondary);
1598 }
1599
1600 #[test]
1601 fn test_failover_policy_latency_exceeds_threshold() {
1602 let policy = FailoverPolicy::new();
1603 let r = HealthReport::new("p")
1604 .set_healthy()
1605 .with_latency_p99(6000.0);
1606 assert_eq!(policy.evaluate(&r), FailoverAction::FailoverToSecondary);
1607 }
1608
1609 #[test]
1610 fn test_failover_policy_boundary_equal_is_stay() {
1611 let policy = FailoverPolicy::new();
1614 let r = HealthReport::new("p")
1615 .set_healthy()
1616 .with_error_rate(0.5)
1617 .with_latency_p99(5000.0);
1618 assert_eq!(policy.evaluate(&r), FailoverAction::StayOnPrimary);
1619 }
1620
1621 #[test]
1622 fn test_failover_policy_custom_thresholds() {
1623 let policy = FailoverPolicy::new()
1624 .with_error_rate_threshold(0.1)
1625 .with_latency_threshold(100.0);
1626 let r = HealthReport::new("p")
1627 .set_healthy()
1628 .with_error_rate(0.2)
1629 .with_latency_p99(50.0);
1630 assert_eq!(policy.evaluate(&r), FailoverAction::FailoverToSecondary);
1632 }
1633
1634 #[test]
1635 fn test_failover_policy_no_metrics_stays() {
1636 let policy = FailoverPolicy::new();
1638 let r = HealthReport::new("p").set_healthy();
1639 assert_eq!(policy.evaluate(&r), FailoverAction::StayOnPrimary);
1640 }
1641
1642 #[test]
1643 fn test_failover_action_variants_distinct() {
1644 let a = FailoverAction::StayOnPrimary;
1645 let b = FailoverAction::FailoverToSecondary;
1646 let c = FailoverAction::FailoverToTertiary;
1647 let d = FailoverAction::CircuitOpen;
1648 assert_ne!(a, b);
1649 assert_ne!(b, c);
1650 assert_ne!(c, d);
1651 assert_ne!(a, d);
1652 }
1653
1654 #[test]
1657 fn test_multi_region_empty_aggregate_unknown() {
1658 let view = MultiRegionHealthView::new();
1659 assert_eq!(view.aggregate(), HealthStatus::Unknown);
1660 assert!(view.healthy_regions().is_empty());
1661 assert!(view.unhealthy_regions().is_empty());
1662 }
1663
1664 #[test]
1665 fn test_multi_region_all_healthy() {
1666 let mut view = MultiRegionHealthView::new();
1667 view.register("us-east-1", HealthStatus::Healthy);
1668 view.register("eu-west-1", HealthStatus::Healthy);
1669 assert_eq!(view.aggregate(), HealthStatus::Healthy);
1670 assert_eq!(view.healthy_regions().len(), 2);
1671 assert!(view.unhealthy_regions().is_empty());
1672 let healthy = view.healthy_regions();
1674 assert_eq!(healthy, vec!["eu-west-1", "us-east-1"]);
1675 }
1676
1677 #[test]
1678 fn test_multi_region_one_unhealthy_drags_aggregate() {
1679 let mut view = MultiRegionHealthView::new();
1680 view.register("us-east-1", HealthStatus::Healthy);
1681 view.register("ap-northeast-1", HealthStatus::Unhealthy);
1682 view.register("eu-west-1", HealthStatus::Healthy);
1683 assert_eq!(view.aggregate(), HealthStatus::Unhealthy);
1684 assert_eq!(view.healthy_regions().len(), 2);
1685 assert_eq!(view.unhealthy_regions(), vec!["ap-northeast-1"]);
1686 }
1687
1688 #[test]
1689 fn test_multi_region_one_unknown_no_unhealthy() {
1690 let mut view = MultiRegionHealthView::new();
1691 view.register("us-east-1", HealthStatus::Healthy);
1692 view.register("unknown-region", HealthStatus::Unknown);
1693 assert_eq!(view.aggregate(), HealthStatus::Unknown);
1694 assert_eq!(view.healthy_regions(), vec!["us-east-1"]);
1695 assert!(view.unhealthy_regions().is_empty());
1696 }
1697
1698 #[test]
1699 fn test_multi_region_overwrite_status() {
1700 let mut view = MultiRegionHealthView::new();
1701 view.register("r1", HealthStatus::Unhealthy);
1702 assert_eq!(view.aggregate(), HealthStatus::Unhealthy);
1703 view.register("r1", HealthStatus::Healthy);
1704 assert_eq!(view.aggregate(), HealthStatus::Healthy);
1705 assert!(view.unhealthy_regions().is_empty());
1706 }
1707
1708 #[test]
1711 fn test_circuit_breaker_starts_closed() {
1712 let mut cb = CircuitBreaker::new(3, std::time::Duration::from_millis(100));
1713 assert_eq!(cb.state(), CircuitState::Closed);
1714 assert!(cb.can_execute());
1715 }
1716
1717 #[test]
1718 fn test_circuit_breaker_trips_after_threshold() {
1719 let mut cb = CircuitBreaker::new(3, std::time::Duration::from_secs(60));
1720 assert!(cb.can_execute());
1721 cb.record_failure();
1722 cb.record_failure();
1723 assert_eq!(cb.state(), CircuitState::Closed);
1724 cb.record_failure();
1725 assert_eq!(cb.state(), CircuitState::Open);
1726 assert!(!cb.can_execute());
1727 }
1728
1729 #[test]
1730 fn test_circuit_breaker_success_resets() {
1731 let mut cb = CircuitBreaker::new(3, std::time::Duration::from_secs(60));
1732 cb.record_failure();
1733 cb.record_failure();
1734 cb.record_success();
1735 assert_eq!(cb.state(), CircuitState::Closed);
1736 cb.record_failure();
1738 cb.record_failure();
1739 assert_eq!(cb.state(), CircuitState::Closed);
1740 }
1741
1742 #[test]
1743 fn test_circuit_breaker_half_open_after_timeout() {
1744 let mut cb = CircuitBreaker::new(1, std::time::Duration::from_millis(10));
1745 cb.record_failure();
1746 assert_eq!(cb.state(), CircuitState::Open);
1747 assert!(!cb.can_execute());
1749 std::thread::sleep(std::time::Duration::from_millis(30));
1751 assert!(cb.can_execute());
1752 assert_eq!(cb.state(), CircuitState::HalfOpen);
1753 }
1754
1755 #[test]
1756 fn test_circuit_breaker_half_open_success_closes() {
1757 let mut cb = CircuitBreaker::new(1, std::time::Duration::from_millis(10));
1758 cb.record_failure();
1759 std::thread::sleep(std::time::Duration::from_millis(20));
1760 assert!(cb.can_execute());
1761 assert_eq!(cb.state(), CircuitState::HalfOpen);
1762 cb.record_success();
1763 assert_eq!(cb.state(), CircuitState::Closed);
1764 }
1765
1766 #[test]
1767 fn test_circuit_breaker_half_open_failure_reopens() {
1768 let mut cb = CircuitBreaker::new(1, std::time::Duration::from_millis(10));
1769 cb.record_failure();
1770 std::thread::sleep(std::time::Duration::from_millis(20));
1771 assert!(cb.can_execute());
1772 assert_eq!(cb.state(), CircuitState::HalfOpen);
1773 cb.record_failure();
1774 assert_eq!(cb.state(), CircuitState::Open);
1775 }
1776
1777 #[test]
1778 fn test_circuit_breaker_boundary_exactly_threshold() {
1779 let mut cb = CircuitBreaker::new(3, std::time::Duration::from_secs(60));
1781 cb.record_failure();
1782 cb.record_failure();
1783 assert_eq!(cb.state(), CircuitState::Closed);
1784 cb.record_failure();
1785 assert_eq!(cb.state(), CircuitState::Open);
1786 }
1787
1788 #[test]
1789 fn test_circuit_breaker_reset_from_open() {
1790 let mut cb = CircuitBreaker::new(2, std::time::Duration::from_secs(60));
1792 cb.record_failure();
1793 cb.record_failure();
1794 assert_eq!(cb.state(), CircuitState::Open);
1795 assert!(cb.reset());
1796 assert_eq!(cb.state(), CircuitState::Closed);
1797 assert_eq!(cb.consecutive_failures, 0);
1798 assert!(cb.can_execute());
1800 cb.record_failure();
1802 assert_eq!(cb.state(), CircuitState::Closed);
1803 }
1804
1805 #[test]
1806 fn test_circuit_breaker_reset_from_half_open() {
1807 let mut cb = CircuitBreaker::new(1, std::time::Duration::from_millis(10));
1808 cb.record_failure();
1809 assert_eq!(cb.state(), CircuitState::Open);
1810 std::thread::sleep(std::time::Duration::from_millis(20));
1811 assert!(cb.can_execute());
1812 assert_eq!(cb.state(), CircuitState::HalfOpen);
1813 assert!(cb.reset());
1815 assert_eq!(cb.state(), CircuitState::Closed);
1816 }
1817
1818 #[test]
1819 fn test_circuit_breaker_reset_idempotent_when_closed() {
1820 let mut cb = CircuitBreaker::new(3, std::time::Duration::from_secs(60));
1822 assert!(!cb.reset());
1823 assert_eq!(cb.state(), CircuitState::Closed);
1824 cb.record_failure();
1826 cb.record_failure();
1827 assert!(cb.reset());
1828 assert_eq!(cb.consecutive_failures, 0);
1829 assert!(!cb.reset());
1831 }
1832
1833 #[test]
1834 fn test_circuit_state_variants_distinct() {
1835 assert_ne!(CircuitState::Closed, CircuitState::Open);
1836 assert_ne!(CircuitState::Open, CircuitState::HalfOpen);
1837 assert_ne!(CircuitState::Closed, CircuitState::HalfOpen);
1838 }
1839
1840 #[test]
1843 fn test_backup_provider_no_backup_is_stale() {
1844 let provider = BackupHealthProvider::new(chrono::Duration::hours(24));
1845 assert!(provider.is_stale());
1846 assert_eq!(provider.check(), HealthStatus::Unhealthy);
1847 }
1848
1849 #[test]
1850 fn test_backup_provider_recent_backup_healthy() {
1851 let mut provider = BackupHealthProvider::new(chrono::Duration::hours(24));
1852 provider.set_last_backup(chrono::Utc::now());
1853 assert!(!provider.is_stale());
1854 assert_eq!(provider.check(), HealthStatus::Healthy);
1855 }
1856
1857 #[test]
1858 fn test_backup_provider_old_backup_unhealthy() {
1859 let mut provider = BackupHealthProvider::new(chrono::Duration::hours(24));
1860 let old = chrono::Utc::now() - chrono::Duration::hours(48);
1861 provider.set_last_backup(old);
1862 assert!(provider.is_stale());
1863 assert_eq!(provider.check(), HealthStatus::Unhealthy);
1864 }
1865
1866 #[test]
1867 fn test_backup_provider_boundary_exactly_max_age() {
1868 let max_age = chrono::Duration::hours(24);
1871 let mut provider = BackupHealthProvider::new(max_age);
1872 let ts = chrono::Utc::now() - max_age;
1873 provider.set_last_backup(ts);
1874 let _ = provider.is_stale();
1877 }
1878
1879 #[test]
1880 fn test_backup_provider_overwrite_timestamp() {
1881 let mut provider = BackupHealthProvider::new(chrono::Duration::hours(24));
1882 let old = chrono::Utc::now() - chrono::Duration::hours(48);
1883 provider.set_last_backup(old);
1884 assert!(provider.is_stale());
1885 provider.set_last_backup(chrono::Utc::now());
1886 assert!(!provider.is_stale());
1887 }
1888
1889 #[test]
1892 fn test_alert_manager_concurrent_notify() {
1893 use std::thread;
1894 let mut mgr = Arc::new(AlertManager::new());
1895 let webhook = Arc::new(WebhookAlertChannel::new("u".to_string()));
1896 {
1899 let m = Arc::get_mut(&mut mgr).expect("unique ref");
1900 m.register(webhook.clone());
1901 m.register(Arc::new(LogAlertChannel::new()));
1902 }
1903 let alert = HealthAlert::new(AlertLevel::Critical, "p", "down");
1904
1905 let mut handles = vec![];
1906 for _ in 0..4 {
1907 let m = mgr.clone();
1908 let a = alert.clone();
1909 handles.push(thread::spawn(move || {
1910 let results = m.notify(&a);
1911 assert_eq!(results.len(), 2);
1912 for r in &results {
1913 assert!(r.is_ok());
1914 }
1915 }));
1916 }
1917 for h in handles {
1918 h.join().expect("thread panicked");
1919 }
1920 assert_eq!(webhook.sent_alerts().len(), 4);
1922 }
1923
1924 #[test]
1925 fn test_failover_policy_integrates_with_report() {
1926 let policy = FailoverPolicy::new();
1928 let report = HealthReport::new("primary")
1929 .set_healthy()
1930 .with_error_rate(0.7)
1931 .with_latency_p99(8000.0)
1932 .with_uptime_ratio(0.9);
1933 assert_eq!(
1934 policy.evaluate(&report),
1935 FailoverAction::FailoverToSecondary
1936 );
1937
1938 let down = HealthReport::new("primary").set_status(HealthStatus::Unhealthy);
1940 assert_eq!(policy.evaluate(&down), FailoverAction::CircuitOpen);
1941 }
1942
1943 #[test]
1944 fn test_static_assertions_send_sync() {
1945 fn assert_send_sync<T: Send + Sync>() {}
1946 assert_send_sync::<AlertManager>();
1947 assert_send_sync::<WebhookAlertChannel>();
1948 assert_send_sync::<ImAlertChannel>();
1949 assert_send_sync::<LogAlertChannel>();
1950 assert_send_sync::<HealthAlert>();
1951 assert_send_sync::<FailoverPolicy>();
1952 assert_send_sync::<MultiRegionHealthView>();
1953 assert_send_sync::<CircuitBreaker>();
1954 assert_send_sync::<BackupHealthProvider>();
1955 }
1956}