Skip to main content

sz_orm_health/
lib.rs

1//! # SZ-ORM Health — 健康检查
2//!
3//! 提供资源健康状态聚合与运行时指标上报,包含连接数、慢查询、错误率与 p50/p95
4//! 延迟等 SLA 指标,用于探活与可观测性。
5//!
6//! ## 主要类型
7//!
8//! - [`HealthStatus`] — 健康/不健康/未知
9//! - [`HealthReport`] — 单资源健康报告
10//!
11//! ## 高级健康检查功能(`advanced` 模块)
12//!
13//! - [`advanced::HealthCheckCache`] — 带 TTL 的健康检查缓存
14//! - [`advanced::CascadingHealthChecker`] — 级联健康检查(依赖链)
15//! - [`advanced::ProbeManager`] — Readiness / Liveness 探针管理
16//! - [`advanced::TimeoutHealthChecker`] — 带超时的健康检查
17
18pub mod advanced;
19
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23
24/// Aggregated health status for a single resource (e.g. a connection pool).
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
26pub enum HealthStatus {
27    Healthy,
28    Unhealthy,
29    #[default]
30    Unknown,
31}
32
33/// Detailed health report for one resource, including runtime metrics.
34#[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    /// SLA: error rate (0.0..=1.0). `None` when not measured.
42    #[serde(default)]
43    pub error_rate: Option<f64>,
44    /// SLA: p50 latency in milliseconds.
45    #[serde(default)]
46    pub p50_ms: Option<f64>,
47    /// SLA: p95 latency in milliseconds.
48    #[serde(default)]
49    pub p95_ms: Option<f64>,
50    /// SLA: p99 latency in milliseconds.
51    #[serde(default)]
52    pub p99_ms: Option<f64>,
53    /// SLA: saturation ratio (0.0..=1.0), e.g. CPU/connection utilization.
54    #[serde(default)]
55    pub saturation: Option<f64>,
56    /// SLA: uptime ratio over a window (0.0..=1.0).
57    #[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/// Snapshot of runtime metrics for a resource, supplied by an external provider.
135#[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
172/// Provider that supplies the live status for a given pool.
173/// Implementations are expected to read real runtime state (e.g. from a
174/// connection pool) rather than return hardcoded values.
175pub trait HealthStatusProvider: Send + Sync {
176    fn snapshot(&self, pool: &str) -> HealthSnapshot;
177}
178
179/// Trait for health checkers. `check` returns the report for a single pool,
180/// while `check_all` aggregates across multiple pools.
181pub trait DbHealthChecker: Send + Sync {
182    fn check(&self, pool: &str) -> HealthReport;
183    fn check_all(&self, pools: &[&str]) -> Vec<HealthReport>;
184}
185
186/// Default in-memory health checker. Stores per-pool status snapshots that
187/// can be updated externally via `set_status`. When a `HealthStatusProvider`
188/// is registered for a pool, it is consulted on each `check`; otherwise the
189/// last manually-set status is used.
190pub struct DefaultHealthChecker {
191    /// Manual overrides / last known status per pool.
192    statuses: RwLock<HashMap<String, HealthSnapshot>>,
193    /// Optional external providers per pool.
194    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    /// Manually set the health snapshot for a pool. This overrides any
212    /// previously stored manual status. If a provider is registered, it
213    /// still takes precedence on the next `check` call.
214    pub fn set_status(&self, pool: &str, snapshot: HealthSnapshot) {
215        // lock poisoned 时降级为 no-op,避免级联 panic。
216        if let Ok(mut statuses) = self.statuses.write() {
217            statuses.insert(pool.to_string(), snapshot);
218        }
219    }
220
221    /// Convenience: mark a pool healthy with given metrics.
222    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    /// Convenience: mark a pool unhealthy with a message.
235    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    /// Register an external provider for a pool. When set, `check` will
248    /// always delegate to the provider rather than the manual snapshot.
249    pub fn register_provider(&self, pool: &str, provider: Arc<dyn HealthStatusProvider>) {
250        // lock poisoned 时降级为 no-op。
251        if let Ok(mut providers) = self.providers.write() {
252            providers.insert(pool.to_string(), provider);
253        }
254    }
255
256    /// Remove a previously registered provider, falling back to manual status.
257    pub fn unregister_provider(&self, pool: &str) -> bool {
258        // lock poisoned 时返回 false(未找到),避免级联 panic。
259        match self.providers.write() {
260            Ok(mut providers) => providers.remove(pool).is_some(),
261            Err(_) => false,
262        }
263    }
264
265    /// Read the snapshot for a pool: provider first, then manual status,
266    /// finally `Unknown` if nothing has been recorded.
267    fn read_snapshot(&self, pool: &str) -> HealthSnapshot {
268        // Check provider first (no holding locks across calls).
269        // lock poisoned 时返回 Unknown,避免级联 panic。
270        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    /// Aggregate the overall status across multiple pools. Returns
303    /// `Unhealthy` if any pool is unhealthy, `Unknown` if any is unknown
304    /// (and none are unhealthy), otherwise `Healthy`.
305    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
346/// A simple provider that always returns the same snapshot. Useful for tests
347/// and for wiring a static status into the checker.
348pub 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
364/// A provider that derives status from connection-count and slow-query
365/// thresholds, mimicking a real pool monitor.
366pub 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// ============================================================================
423// L4: Financial-grade alerting & disaster recovery
424// ============================================================================
425
426/// Severity level for a [`HealthAlert`]. This is independent of any
427/// `AlertLevel` defined in `sz-orm-tracing` so that the health package can
428// be used standalone.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
430pub enum AlertLevel {
431    Info,
432    Warning,
433    Critical,
434}
435
436/// A structured alert emitted by the health system. Carries an optional
437/// [`HealthReport`] so receivers can inspect the metrics that triggered it.
438#[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
468/// A channel that delivers [`HealthAlert`]s to some downstream sink.
469/// Implementations must be `Send + Sync` so they can be shared across
470/// threads by [`AlertManager`].
471pub trait AlertChannel: Send + Sync {
472    /// Deliver the alert. Returns `Err(String)` describing the failure
473    /// (e.g. network error) so the caller can retry or log.
474    fn send(&self, alert: &HealthAlert) -> Result<(), String>;
475    /// Human-readable name of the channel (e.g. `"log"`, `"webhook"`).
476    fn name(&self) -> &str;
477}
478
479/// Channel that writes alerts to stderr via `eprintln`. Always succeeds.
480pub 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
515/// Channel that records alerts in-memory, simulating a webhook sink.
516/// Useful for tests and for wiring into an actual HTTP client later.
517pub 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        // lock poisoned 时返回空 Vec,避免级联 panic。
538        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        // In-memory simulation: in production this would POST to `self.url`.
545        // lock poisoned 时返回错误而非 panic。
546        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
559/// Channel that records alerts in-memory, simulating an IM notification
560/// (Slack/DingTalk/Feishu style). Useful for tests.
561pub 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        // lock poisoned 时返回空 Vec,避免级联 panic。
582        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        // In-memory simulation: in production this would POST an IM payload.
589        // lock poisoned 时返回错误而非 panic。
590        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
603/// Manager that fans out a [`HealthAlert`] to all registered channels.
604/// `Send + Sync` because it only holds `Arc<dyn AlertChannel>` (which is
605/// itself `Send + Sync`). Registration requires `&mut self`, so no interior
606/// mutability is needed.
607pub 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    /// Notify every registered channel. Returns one result per channel,
629    /// in registration order. Never short-circuits: all channels are tried.
630    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/// Action recommended by [`FailoverPolicy::evaluate`].
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub enum FailoverAction {
642    StayOnPrimary,
643    FailoverToSecondary,
644    FailoverToTertiary,
645    CircuitOpen,
646}
647
648/// Policy that inspects a [`HealthReport`] and decides whether to stay on
649/// the primary or fail over. Default thresholds:
650///   * error_rate > 0.5           -> failover
651///   * latency_p99 > 5000 (ms)    -> failover
652///   * status == Unhealthy        -> CircuitOpen (failover cannot help)
653pub 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    /// Evaluate the report and recommend an action.
683    ///   * If status is `Unhealthy` -> `CircuitOpen`
684    ///   * Else if error_rate > threshold OR p99 > latency threshold -> `FailoverToSecondary`
685    ///   * Else -> `StayOnPrimary`
686    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
706/// Aggregated view of health across multiple regions (data centers).
707/// Any `Unhealthy` region drags the aggregate to `Unhealthy`; otherwise
708/// `Unknown` if any region is unknown, else `Healthy`.
709pub 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/// State machine for a circuit breaker.
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum CircuitState {
775    /// Normal operation: requests flow through.
776    Closed,
777    /// Tripped: requests are blocked until `reset_timeout` elapses.
778    Open,
779    /// Probe: a single trial request is allowed after the reset timeout.
780    HalfOpen,
781}
782
783/// A simple circuit breaker. Trips after `failure_threshold` consecutive
784/// failures, then enters `HalfOpen` after `reset_timeout`, transitioning
785/// back to `Closed` on success or `Open` on failure.
786pub 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    /// Returns `true` if a request may proceed. May transition `Open` ->
824    /// `HalfOpen` if the reset timeout has elapsed.
825    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    /// 手动重置断路器到 `Closed` 状态,清空失败计数与最后失败时间。
845    ///
846    /// 与 `record_success` 的区别:
847    /// - `record_success`:请求成功后调用,语义上是“一次成功请求”的自然反馈;
848    /// - `reset`:管理员/运维主动强制重置,无视当前状态(含 `Open`),
849    ///   常用于故障排除后手动恢复、测试准备场景。
850    ///
851    /// 返回是否实际发生了状态变更(`Open` 或 `HalfOpen` → `Closed` 视为变更,
852    /// 已处于 `Closed` 且无失败计数则返回 `false`)。
853    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
862/// Provider that tracks the last backup timestamp and reports `Unhealthy`
863/// when the backup is older than the configured `max_age`.
864pub 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    /// Returns `true` if no backup has been recorded, or the last backup
882    /// is older than `max_age`.
883    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
903/// 启动健康检查 HTTP server
904///
905/// 在指定地址暴露健康检查端点,返回各资源池的 `HealthReport` JSON 数组。
906/// HTTP 状态码:全部健康返回 200,任一不健康返回 503。
907///
908/// # 参数
909///
910/// - `checker`: 健康检查器( wrapped in `Arc` 以便跨 task 共享)
911/// - `pools`: 需要检查的资源池名称列表
912/// - `addr`: 监听地址
913pub 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        // 'b' never set -> Unknown
1066        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        // Manual status is unhealthy...
1079        checker.set_unhealthy("p", "manual");
1080
1081        // ...but a provider says it's healthy.
1082        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        // Exactly at the limit should still be healthy (not exceed).
1152        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    /// A dynamic provider that returns different snapshots over time,
1198    /// simulating a real monitoring source.
1199    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        // Observe that successive set_status calls are reflected in check().
1273        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        // Trait object has no set_status, so we just verify check returns Unknown.
1320        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    // ===================== L4: SLA fields on HealthReport =====================
1350
1351    #[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        // Old JSON without the new SLA fields must still deserialize, with
1396        // those fields defaulting to None.
1397        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    // ===================== L4: AlertLevel / HealthAlert =====================
1423
1424    #[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        // timestamp should be ~now (just sanity check it's after epoch).
1439        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    // ===================== L4: AlertChannel implementations =====================
1466
1467    #[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    // ===================== L4: AlertManager =====================
1515
1516    #[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    // ===================== L4: FailoverPolicy =====================
1578
1579    #[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        // Exactly at threshold (0.5 and 5000) should NOT trigger failover
1612        // (strictly greater-than comparison).
1613        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        // error_rate 0.2 > 0.1 -> failover, even though latency is fine.
1631        assert_eq!(policy.evaluate(&r), FailoverAction::FailoverToSecondary);
1632    }
1633
1634    #[test]
1635    fn test_failover_policy_no_metrics_stays() {
1636        // No error_rate / p99 set -> cannot exceed threshold -> stay.
1637        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    // ===================== L4: MultiRegionHealthView =====================
1655
1656    #[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        // Sorted output.
1673        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    // ===================== L4: CircuitBreaker =====================
1709
1710    #[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        // After success, should need 3 failures again to trip.
1737        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        // Immediately: still open.
1748        assert!(!cb.can_execute());
1749        // Wait for reset timeout.
1750        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        // threshold = 3 means 3 failures should trip (>= comparison).
1780        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        // 故障排除后手动重置:Open → Closed
1791        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        // 重置后立即可执行
1799        assert!(cb.can_execute());
1800        // 仍需累计到阈值才会再次跳闸
1801        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        // HalfOpen 状态下手动重置
1814        assert!(cb.reset());
1815        assert_eq!(cb.state(), CircuitState::Closed);
1816    }
1817
1818    #[test]
1819    fn test_circuit_breaker_reset_idempotent_when_closed() {
1820        // 已处于 Closed 且无失败计数时,reset 返回 false(无变更)
1821        let mut cb = CircuitBreaker::new(3, std::time::Duration::from_secs(60));
1822        assert!(!cb.reset());
1823        assert_eq!(cb.state(), CircuitState::Closed);
1824        // 存在失败计数但未跳闸时,reset 视为变更
1825        cb.record_failure();
1826        cb.record_failure();
1827        assert!(cb.reset());
1828        assert_eq!(cb.consecutive_failures, 0);
1829        // 再次 reset 无变更
1830        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    // ===================== L4: BackupHealthProvider =====================
1841
1842    #[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        // A backup exactly at max_age should NOT be considered stale
1869        // (strictly greater-than comparison).
1870        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        // Note: time may have advanced slightly, so we accept either result
1875        // as long as the logic is consistent. The boundary is `> max_age`.
1876        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    // ===================== L4: Integration / concurrency =====================
1890
1891    #[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        // We can't register after wrapping in Arc without &mut, so register
1897        // before sharing. Use Arc::get_mut for setup.
1898        {
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        // Webhook channel should have received 4 alerts.
1921        assert_eq!(webhook.sent_alerts().len(), 4);
1922    }
1923
1924    #[test]
1925    fn test_failover_policy_integrates_with_report() {
1926        // End-to-end: build a report with metrics and evaluate policy.
1927        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        // If the primary is actually Unhealthy, the circuit is open.
1939        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}