Skip to main content

oxirs_stream/fault_tolerance/
mod.rs

1//! # Advanced Fault Tolerance for Stream Processing
2//!
3//! Provides production-grade fault tolerance primitives for stream processing:
4//! health monitoring, bulkhead isolation, configurable retry policies, and
5//! worker supervision.
6//!
7//! ## Components
8//!
9//! - [`StreamHealthMonitor`]: Monitors stream health with configurable thresholds
10//! - [`BulkheadIsolator`]: Isolates stream failures using the bulkhead pattern
11//! - [`StreamRetryPolicy`]: Configurable retry with exponential backoff
12//! - [`StreamSupervisor`]: Supervises stream workers and restarts on failure
13//! - [`checkpoint::MarkerPropagator`] / [`checkpoint::CheckpointController`]:
14//!   Chandy-Lamport-style marker propagation for distributed checkpoints
15//! - [`exactly_once::EndToEndExactlyOnceCoordinator`]: end-to-end exactly-once
16//!   coordinator combining dedup + idempotent producers + atomic transactions
17
18pub mod checkpoint;
19pub mod checkpoint_recovery;
20pub mod exactly_once;
21pub use checkpoint::{
22    CheckpointController, CheckpointControllerConfig, CheckpointError, CheckpointId,
23    CheckpointProgress, CheckpointResult, CheckpointStore, InMemoryCheckpointStore, InputEdgeId,
24    Marker, MarkerPropagator, MarkerPropagatorEvent, OperatorId, OperatorSnapshot,
25};
26pub use checkpoint_recovery::*;
27pub use exactly_once::{
28    EndToEndExactlyOnceCoordinator, ExactlyOnceCoordinatorConfig, ExactlyOnceCoordinatorStats,
29    ExactlyOnceError, ExactlyOnceResult, ExactlyOnceStatsSnapshot, IdempotentProducer,
30    IdempotentProducerConfig, ProducerStamp,
31};
32
33use std::collections::HashMap;
34use std::sync::Arc;
35use std::time::{Duration, Instant, SystemTime};
36
37use parking_lot::RwLock;
38use serde::{Deserialize, Serialize};
39use thiserror::Error;
40use tracing::{debug, info, warn};
41
42// ─── Error Types ─────────────────────────────────────────────────────────────
43
44/// Errors in fault tolerance operations
45#[derive(Error, Debug, Clone)]
46pub enum FaultToleranceError {
47    #[error("Bulkhead full: compartment {compartment} has reached capacity {capacity}")]
48    BulkheadFull {
49        compartment: String,
50        capacity: usize,
51    },
52
53    #[error("Max retries exceeded: {attempts} attempts for operation {operation}")]
54    MaxRetriesExceeded { attempts: u32, operation: String },
55
56    #[error("Worker {worker_id} failed to restart after {attempts} attempts")]
57    SupervisorRestartFailed { worker_id: String, attempts: u32 },
58
59    #[error("Health check failed: metric {metric} value {value} exceeds threshold {threshold}")]
60    HealthCheckFailed {
61        metric: String,
62        value: f64,
63        threshold: f64,
64    },
65
66    #[error("Operation timeout after {elapsed_ms}ms (limit {timeout_ms}ms)")]
67    OperationTimeout { elapsed_ms: u64, timeout_ms: u64 },
68}
69
70/// Result type for fault tolerance operations
71pub type FaultResult<T> = Result<T, FaultToleranceError>;
72
73// ─── Stream Health Monitor ────────────────────────────────────────────────────
74
75/// A threshold rule that can trigger a health alert
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct HealthThreshold {
78    /// Name of the metric (e.g. "error_rate", "latency_p99_ms")
79    pub metric_name: String,
80    /// Value above which the health is considered degraded
81    pub warn_threshold: f64,
82    /// Value above which the health is considered critical
83    pub critical_threshold: f64,
84}
85
86/// Severity of a health alert
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub enum HealthAlertSeverity {
89    /// Metric crossed the warning threshold
90    Warning,
91    /// Metric crossed the critical threshold
92    Critical,
93    /// Metric has recovered below warning threshold
94    Recovered,
95}
96
97/// A health alert emitted by the monitor
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct HealthAlert {
100    /// Metric that triggered the alert
101    pub metric_name: String,
102    /// Current metric value
103    pub current_value: f64,
104    /// Applicable threshold that was crossed
105    pub threshold: f64,
106    /// Alert severity
107    pub severity: HealthAlertSeverity,
108    /// When the alert was raised
109    pub raised_at: SystemTime,
110}
111
112/// Overall health status of the stream
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub enum StreamHealthStatus {
115    /// All metrics are within normal thresholds
116    Healthy,
117    /// One or more metrics have crossed warning thresholds
118    Degraded,
119    /// One or more metrics have crossed critical thresholds
120    Critical,
121    /// Health data is too old to be reliable
122    Unknown,
123}
124
125/// A snapshot of all health metrics
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct HealthSnapshot {
128    /// Current health status
129    pub status: StreamHealthStatus,
130    /// All current metric values
131    pub metrics: HashMap<String, f64>,
132    /// Active alerts
133    pub active_alerts: Vec<HealthAlert>,
134    /// Timestamp of this snapshot
135    pub snapshot_time: SystemTime,
136}
137
138/// Configuration for the stream health monitor
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct HealthMonitorConfig {
141    /// Health thresholds per metric
142    pub thresholds: Vec<HealthThreshold>,
143    /// Maximum age of a metric before it is considered stale
144    pub metric_staleness: Duration,
145    /// How often to evaluate thresholds
146    pub check_interval: Duration,
147}
148
149impl Default for HealthMonitorConfig {
150    fn default() -> Self {
151        Self {
152            thresholds: vec![
153                HealthThreshold {
154                    metric_name: "error_rate".to_string(),
155                    warn_threshold: 0.01,
156                    critical_threshold: 0.05,
157                },
158                HealthThreshold {
159                    metric_name: "latency_p99_ms".to_string(),
160                    warn_threshold: 100.0,
161                    critical_threshold: 500.0,
162                },
163                HealthThreshold {
164                    metric_name: "backpressure_ratio".to_string(),
165                    warn_threshold: 0.5,
166                    critical_threshold: 0.9,
167                },
168            ],
169            metric_staleness: Duration::from_secs(60),
170            check_interval: Duration::from_secs(5),
171        }
172    }
173}
174
175/// Monitors stream health with configurable thresholds.
176///
177/// Accepts metric updates from the stream pipeline and evaluates them against
178/// configured thresholds to produce health alerts.
179pub struct StreamHealthMonitor {
180    config: HealthMonitorConfig,
181    /// Current metric values with timestamps
182    metrics: Arc<RwLock<HashMap<String, (f64, Instant)>>>,
183    /// Currently active alerts
184    active_alerts: Arc<RwLock<Vec<HealthAlert>>>,
185    /// Alert history (capped to last 1000 alerts)
186    alert_history: Arc<RwLock<Vec<HealthAlert>>>,
187    /// Total alerts raised since creation
188    total_alerts_raised: Arc<RwLock<u64>>,
189}
190
191impl StreamHealthMonitor {
192    /// Creates a new health monitor with the given configuration.
193    pub fn new(config: HealthMonitorConfig) -> Self {
194        Self {
195            config,
196            metrics: Arc::new(RwLock::new(HashMap::new())),
197            active_alerts: Arc::new(RwLock::new(Vec::new())),
198            alert_history: Arc::new(RwLock::new(Vec::new())),
199            total_alerts_raised: Arc::new(RwLock::new(0)),
200        }
201    }
202
203    /// Records a new metric value and evaluates thresholds.
204    ///
205    /// Returns any new alerts that were raised.
206    pub fn record_metric(&self, metric_name: &str, value: f64) -> Vec<HealthAlert> {
207        self.metrics
208            .write()
209            .insert(metric_name.to_string(), (value, Instant::now()));
210        self.evaluate_thresholds(metric_name, value)
211    }
212
213    /// Returns the current health snapshot.
214    pub fn snapshot(&self) -> HealthSnapshot {
215        let metrics = self.metrics.read();
216        let now = Instant::now();
217        let stale_limit = self.config.metric_staleness;
218
219        // Check for stale metrics
220        let all_fresh = metrics
221            .values()
222            .all(|(_, ts)| now.duration_since(*ts) < stale_limit);
223
224        let metric_values: HashMap<String, f64> =
225            metrics.iter().map(|(k, (v, _))| (k.clone(), *v)).collect();
226
227        let active_alerts = self.active_alerts.read().clone();
228
229        let status = if !all_fresh || metric_values.is_empty() {
230            StreamHealthStatus::Unknown
231        } else if active_alerts
232            .iter()
233            .any(|a| a.severity == HealthAlertSeverity::Critical)
234        {
235            StreamHealthStatus::Critical
236        } else if active_alerts
237            .iter()
238            .any(|a| a.severity == HealthAlertSeverity::Warning)
239        {
240            StreamHealthStatus::Degraded
241        } else {
242            StreamHealthStatus::Healthy
243        };
244
245        HealthSnapshot {
246            status,
247            metrics: metric_values,
248            active_alerts,
249            snapshot_time: SystemTime::now(),
250        }
251    }
252
253    /// Returns current metric value for a named metric, if present.
254    pub fn current_metric(&self, name: &str) -> Option<f64> {
255        self.metrics.read().get(name).map(|(v, _)| *v)
256    }
257
258    /// Returns the total number of alerts ever raised.
259    pub fn total_alerts_raised(&self) -> u64 {
260        *self.total_alerts_raised.read()
261    }
262
263    fn evaluate_thresholds(&self, metric_name: &str, value: f64) -> Vec<HealthAlert> {
264        let mut new_alerts = Vec::new();
265        let thresholds = self.config.thresholds.clone();
266
267        for threshold in &thresholds {
268            if threshold.metric_name != metric_name {
269                continue;
270            }
271            let severity = if value >= threshold.critical_threshold {
272                Some(HealthAlertSeverity::Critical)
273            } else if value >= threshold.warn_threshold {
274                Some(HealthAlertSeverity::Warning)
275            } else {
276                // Potentially recovered — remove existing alert for this metric
277                let mut active = self.active_alerts.write();
278                active.retain(|a| a.metric_name != metric_name);
279                None
280            };
281
282            if let Some(sev) = severity {
283                let threshold_val = if sev == HealthAlertSeverity::Critical {
284                    threshold.critical_threshold
285                } else {
286                    threshold.warn_threshold
287                };
288                let alert = HealthAlert {
289                    metric_name: metric_name.to_string(),
290                    current_value: value,
291                    threshold: threshold_val,
292                    severity: sev,
293                    raised_at: SystemTime::now(),
294                };
295                // Upsert active alert for this metric
296                let mut active = self.active_alerts.write();
297                active.retain(|a| a.metric_name != metric_name);
298                active.push(alert.clone());
299                drop(active);
300
301                // Append to history (cap at 1000)
302                let mut history = self.alert_history.write();
303                if history.len() >= 1000 {
304                    history.remove(0);
305                }
306                history.push(alert.clone());
307
308                *self.total_alerts_raised.write() += 1;
309                new_alerts.push(alert);
310                debug!("Health alert raised for metric {}: {}", metric_name, value);
311            }
312        }
313        new_alerts
314    }
315}
316
317// ─── Bulkhead Isolator ────────────────────────────────────────────────────────
318
319/// Statistics for a single bulkhead compartment
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct CompartmentStats {
322    /// Compartment identifier
323    pub compartment_id: String,
324    /// Maximum concurrent operations
325    pub capacity: usize,
326    /// Currently active operations
327    pub active: usize,
328    /// Total operations rejected due to full compartment
329    pub rejected: u64,
330    /// Total operations accepted
331    pub accepted: u64,
332}
333
334/// Configuration for the bulkhead isolator
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct BulkheadConfig {
337    /// Capacity per named compartment
338    pub compartment_capacities: HashMap<String, usize>,
339    /// Default capacity for compartments not explicitly listed
340    pub default_capacity: usize,
341}
342
343impl Default for BulkheadConfig {
344    fn default() -> Self {
345        let mut compartments = HashMap::new();
346        compartments.insert("critical".to_string(), 100);
347        compartments.insert("standard".to_string(), 50);
348        compartments.insert("background".to_string(), 20);
349        Self {
350            compartment_capacities: compartments,
351            default_capacity: 30,
352        }
353    }
354}
355
356/// A permit acquired from the bulkhead; releases on drop.
357pub struct BulkheadPermit {
358    compartment_id: String,
359    active_counter: Arc<RwLock<usize>>,
360}
361
362impl Drop for BulkheadPermit {
363    fn drop(&mut self) {
364        let mut active = self.active_counter.write();
365        if *active > 0 {
366            *active -= 1;
367        }
368        debug!(
369            "Bulkhead permit released for compartment {}",
370            self.compartment_id
371        );
372    }
373}
374
375/// Internal state for a single compartment
376struct Compartment {
377    capacity: usize,
378    active: Arc<RwLock<usize>>,
379    rejected: Arc<RwLock<u64>>,
380    accepted: Arc<RwLock<u64>>,
381}
382
383/// Isolates stream failures using the bulkhead pattern.
384///
385/// Divides the system into isolated compartments with independent concurrency
386/// limits; a failure or overload in one compartment does not affect others.
387pub struct BulkheadIsolator {
388    compartments: Arc<RwLock<HashMap<String, Compartment>>>,
389    default_capacity: usize,
390}
391
392impl BulkheadIsolator {
393    /// Creates a new bulkhead isolator with the given configuration.
394    pub fn new(config: BulkheadConfig) -> Self {
395        let mut compartments = HashMap::new();
396        for (id, capacity) in &config.compartment_capacities {
397            compartments.insert(
398                id.clone(),
399                Compartment {
400                    capacity: *capacity,
401                    active: Arc::new(RwLock::new(0)),
402                    rejected: Arc::new(RwLock::new(0)),
403                    accepted: Arc::new(RwLock::new(0)),
404                },
405            );
406        }
407        Self {
408            compartments: Arc::new(RwLock::new(compartments)),
409            default_capacity: config.default_capacity,
410        }
411    }
412
413    /// Attempts to acquire a permit for the named compartment.
414    ///
415    /// Returns `Ok(BulkheadPermit)` if capacity is available; `Err` if full.
416    pub fn acquire(&self, compartment_id: &str) -> FaultResult<BulkheadPermit> {
417        let mut compartments = self.compartments.write();
418        // Auto-create compartment with default capacity if unknown
419        let compartment = compartments
420            .entry(compartment_id.to_string())
421            .or_insert_with(|| Compartment {
422                capacity: self.default_capacity,
423                active: Arc::new(RwLock::new(0)),
424                rejected: Arc::new(RwLock::new(0)),
425                accepted: Arc::new(RwLock::new(0)),
426            });
427
428        let current = *compartment.active.read();
429        if current >= compartment.capacity {
430            *compartment.rejected.write() += 1;
431            return Err(FaultToleranceError::BulkheadFull {
432                compartment: compartment_id.to_string(),
433                capacity: compartment.capacity,
434            });
435        }
436        *compartment.active.write() += 1;
437        *compartment.accepted.write() += 1;
438        debug!(
439            "Bulkhead permit acquired for compartment {} ({}/{})",
440            compartment_id,
441            current + 1,
442            compartment.capacity
443        );
444
445        Ok(BulkheadPermit {
446            compartment_id: compartment_id.to_string(),
447            active_counter: Arc::clone(&compartment.active),
448        })
449    }
450
451    /// Returns statistics for all compartments.
452    pub fn stats(&self) -> Vec<CompartmentStats> {
453        self.compartments
454            .read()
455            .iter()
456            .map(|(id, c)| CompartmentStats {
457                compartment_id: id.clone(),
458                capacity: c.capacity,
459                active: *c.active.read(),
460                rejected: *c.rejected.read(),
461                accepted: *c.accepted.read(),
462            })
463            .collect()
464    }
465
466    /// Returns statistics for a specific compartment.
467    pub fn compartment_stats(&self, compartment_id: &str) -> Option<CompartmentStats> {
468        self.compartments
469            .read()
470            .get(compartment_id)
471            .map(|c| CompartmentStats {
472                compartment_id: compartment_id.to_string(),
473                capacity: c.capacity,
474                active: *c.active.read(),
475                rejected: *c.rejected.read(),
476                accepted: *c.accepted.read(),
477            })
478    }
479}
480
481// ─── Stream Retry Policy ──────────────────────────────────────────────────────
482
483/// Configures when and how many times an operation should be retried
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct StreamRetryPolicy {
486    /// Maximum number of retry attempts (0 = no retries)
487    pub max_attempts: u32,
488    /// Initial delay before first retry
489    pub initial_delay: Duration,
490    /// Multiplier applied to delay after each retry (exponential backoff)
491    pub backoff_multiplier: f64,
492    /// Maximum delay cap (prevents unbounded backoff)
493    pub max_delay: Duration,
494    /// Whether to add random jitter (fraction of delay) to prevent thundering herd
495    pub jitter: bool,
496}
497
498impl Default for StreamRetryPolicy {
499    fn default() -> Self {
500        Self {
501            max_attempts: 3,
502            initial_delay: Duration::from_millis(100),
503            backoff_multiplier: 2.0,
504            max_delay: Duration::from_secs(30),
505            jitter: true,
506        }
507    }
508}
509
510impl StreamRetryPolicy {
511    /// Returns the delay before the nth retry attempt (0-indexed).
512    ///
513    /// Incorporates exponential backoff and an optional pseudo-random jitter
514    /// derived deterministically from the attempt number (no rand crate needed).
515    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
516        let factor = self.backoff_multiplier.powi(attempt as i32);
517        let base_ms = self.initial_delay.as_millis() as f64 * factor;
518        let capped_ms = base_ms.min(self.max_delay.as_millis() as f64);
519
520        let jitter_ms = if self.jitter {
521            // Deterministic jitter: 0..25% of capped_ms using linear congruential
522            let pseudo = ((attempt as u64)
523                .wrapping_mul(6364136223846793005)
524                .wrapping_add(1))
525                % 1000;
526            let ratio = pseudo as f64 / 4000.0; // 0..0.25
527            capped_ms * ratio
528        } else {
529            0.0
530        };
531
532        Duration::from_millis((capped_ms + jitter_ms) as u64)
533    }
534
535    /// Executes a fallible synchronous closure with retries.
536    ///
537    /// Uses `std::thread::sleep` for backoff delays. For async contexts see
538    /// [`StreamRetryPolicy::retry_async`].
539    pub fn retry<F, T, E>(&self, operation_name: &str, mut f: F) -> FaultResult<T>
540    where
541        F: FnMut() -> Result<T, E>,
542        E: std::fmt::Debug,
543    {
544        for attempt in 0..=self.max_attempts {
545            match f() {
546                Ok(result) => {
547                    if attempt > 0 {
548                        info!(
549                            "Operation {} succeeded after {} retries",
550                            operation_name, attempt
551                        );
552                    }
553                    return Ok(result);
554                }
555                Err(err) => {
556                    if attempt >= self.max_attempts {
557                        warn!(
558                            "Operation {} failed after {} attempts: {:?}",
559                            operation_name,
560                            attempt + 1,
561                            err
562                        );
563                        return Err(FaultToleranceError::MaxRetriesExceeded {
564                            attempts: attempt + 1,
565                            operation: operation_name.to_string(),
566                        });
567                    }
568                    let delay = self.delay_for_attempt(attempt);
569                    debug!(
570                        "Operation {} attempt {} failed, retrying in {:?}",
571                        operation_name,
572                        attempt + 1,
573                        delay
574                    );
575                    std::thread::sleep(delay);
576                }
577            }
578        }
579        // Unreachable but satisfies the type checker
580        Err(FaultToleranceError::MaxRetriesExceeded {
581            attempts: self.max_attempts + 1,
582            operation: operation_name.to_string(),
583        })
584    }
585
586    /// Executes a fallible async closure with retries and tokio async sleep.
587    pub async fn retry_async<F, Fut, T, E>(&self, operation_name: &str, mut f: F) -> FaultResult<T>
588    where
589        F: FnMut() -> Fut,
590        Fut: std::future::Future<Output = Result<T, E>>,
591        E: std::fmt::Debug,
592    {
593        for attempt in 0..=self.max_attempts {
594            match f().await {
595                Ok(result) => {
596                    if attempt > 0 {
597                        info!(
598                            "Async operation {} succeeded after {} retries",
599                            operation_name, attempt
600                        );
601                    }
602                    return Ok(result);
603                }
604                Err(err) => {
605                    if attempt >= self.max_attempts {
606                        warn!(
607                            "Async operation {} failed after {} attempts: {:?}",
608                            operation_name,
609                            attempt + 1,
610                            err
611                        );
612                        return Err(FaultToleranceError::MaxRetriesExceeded {
613                            attempts: attempt + 1,
614                            operation: operation_name.to_string(),
615                        });
616                    }
617                    let delay = self.delay_for_attempt(attempt);
618                    debug!(
619                        "Async operation {} attempt {} failed, retrying in {:?}",
620                        operation_name,
621                        attempt + 1,
622                        delay
623                    );
624                    tokio::time::sleep(delay).await;
625                }
626            }
627        }
628        Err(FaultToleranceError::MaxRetriesExceeded {
629            attempts: self.max_attempts + 1,
630            operation: operation_name.to_string(),
631        })
632    }
633}
634
635// ─── Stream Supervisor ────────────────────────────────────────────────────────
636
637/// The status of a supervised worker
638#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
639pub enum WorkerStatus {
640    /// Worker is running normally
641    Running,
642    /// Worker has failed and is waiting for restart
643    Failed,
644    /// Worker is being restarted
645    Restarting,
646    /// Worker has been stopped intentionally
647    Stopped,
648    /// Worker has exceeded max restart attempts and is permanently failed
649    Exhausted,
650}
651
652/// A record of a worker restart
653#[derive(Debug, Clone, Serialize, Deserialize)]
654pub struct RestartRecord {
655    /// Worker ID
656    pub worker_id: String,
657    /// Restart attempt number
658    pub attempt: u32,
659    /// Reason for restart
660    pub reason: String,
661    /// When the restart was attempted
662    pub restarted_at: SystemTime,
663    /// Whether the restart succeeded
664    pub success: bool,
665}
666
667/// Internal worker state tracked by the supervisor
668#[derive(Debug, Clone)]
669struct WorkerState {
670    worker_id: String,
671    status: WorkerStatus,
672    restart_count: u32,
673    max_restarts: u32,
674    last_failure: Option<SystemTime>,
675    last_restart: Option<SystemTime>,
676}
677
678/// Configuration for the stream supervisor
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct SupervisorConfig {
681    /// Maximum number of restart attempts per worker before giving up
682    pub max_restarts: u32,
683    /// Restart backoff policy
684    pub restart_policy: StreamRetryPolicy,
685    /// Whether to propagate failure to sibling workers (one-for-all strategy)
686    pub one_for_all: bool,
687}
688
689impl Default for SupervisorConfig {
690    fn default() -> Self {
691        Self {
692            max_restarts: 5,
693            restart_policy: StreamRetryPolicy {
694                max_attempts: 5,
695                initial_delay: Duration::from_millis(500),
696                backoff_multiplier: 2.0,
697                max_delay: Duration::from_secs(60),
698                jitter: true,
699            },
700            one_for_all: false,
701        }
702    }
703}
704
705/// Statistics for the stream supervisor
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct SupervisorStats {
708    /// Total workers registered
709    pub total_workers: usize,
710    /// Workers currently running
711    pub running_workers: usize,
712    /// Workers permanently failed
713    pub exhausted_workers: usize,
714    /// Total restart events
715    pub total_restarts: u64,
716    /// Total restart records
717    pub restart_history_len: usize,
718}
719
720/// Supervises stream workers and restarts them on failure.
721///
722/// Tracks worker health, enforces restart limits, and optionally propagates
723/// failures to sibling workers (one-for-all supervision strategy).
724pub struct StreamSupervisor {
725    config: SupervisorConfig,
726    workers: Arc<RwLock<HashMap<String, WorkerState>>>,
727    restart_history: Arc<RwLock<Vec<RestartRecord>>>,
728    total_restarts: Arc<RwLock<u64>>,
729}
730
731impl StreamSupervisor {
732    /// Creates a new supervisor with the given configuration.
733    pub fn new(config: SupervisorConfig) -> Self {
734        Self {
735            config,
736            workers: Arc::new(RwLock::new(HashMap::new())),
737            restart_history: Arc::new(RwLock::new(Vec::new())),
738            total_restarts: Arc::new(RwLock::new(0)),
739        }
740    }
741
742    /// Registers a worker with the supervisor.
743    pub fn register_worker(&self, worker_id: impl Into<String>) {
744        let id = worker_id.into();
745        self.workers.write().insert(
746            id.clone(),
747            WorkerState {
748                worker_id: id,
749                status: WorkerStatus::Running,
750                restart_count: 0,
751                max_restarts: self.config.max_restarts,
752                last_failure: None,
753                last_restart: None,
754            },
755        );
756    }
757
758    /// Notifies the supervisor that a worker has failed.
759    ///
760    /// The supervisor will attempt to restart the worker unless the restart
761    /// limit has been reached.
762    ///
763    /// Returns the new worker status.
764    pub fn report_failure(&self, worker_id: &str, reason: &str) -> FaultResult<WorkerStatus> {
765        let new_status = {
766            let mut workers = self.workers.write();
767            let worker = workers.get_mut(worker_id).ok_or_else(|| {
768                FaultToleranceError::SupervisorRestartFailed {
769                    worker_id: worker_id.to_string(),
770                    attempts: 0,
771                }
772            })?;
773
774            worker.last_failure = Some(SystemTime::now());
775
776            if worker.restart_count >= worker.max_restarts {
777                worker.status = WorkerStatus::Exhausted;
778                warn!(
779                    "Worker {} permanently failed after {} restarts",
780                    worker_id, worker.restart_count
781                );
782                WorkerStatus::Exhausted
783            } else {
784                worker.status = WorkerStatus::Restarting;
785                worker.restart_count += 1;
786                worker.last_restart = Some(SystemTime::now());
787                WorkerStatus::Restarting
788            }
789        };
790
791        // Record restart attempt
792        let attempt = self
793            .workers
794            .read()
795            .get(worker_id)
796            .map(|w| w.restart_count)
797            .unwrap_or(0);
798        let record = RestartRecord {
799            worker_id: worker_id.to_string(),
800            attempt,
801            reason: reason.to_string(),
802            restarted_at: SystemTime::now(),
803            success: new_status == WorkerStatus::Restarting,
804        };
805        let mut history = self.restart_history.write();
806        if history.len() >= 10_000 {
807            history.remove(0);
808        }
809        history.push(record);
810
811        if new_status == WorkerStatus::Restarting {
812            *self.total_restarts.write() += 1;
813            info!("Restarting worker {} (attempt {})", worker_id, attempt);
814
815            // If one-for-all strategy, mark siblings for restart too
816            if self.config.one_for_all {
817                let siblings: Vec<String> = self
818                    .workers
819                    .read()
820                    .keys()
821                    .filter(|k| k.as_str() != worker_id)
822                    .cloned()
823                    .collect();
824                for sibling_id in siblings {
825                    let mut workers = self.workers.write();
826                    if let Some(sibling) = workers.get_mut(&sibling_id) {
827                        if sibling.status == WorkerStatus::Running {
828                            sibling.status = WorkerStatus::Restarting;
829                            sibling.restart_count += 1;
830                            sibling.last_restart = Some(SystemTime::now());
831                        }
832                    }
833                }
834            }
835        }
836
837        Ok(new_status)
838    }
839
840    /// Acknowledges that a worker has successfully restarted.
841    pub fn acknowledge_restart(&self, worker_id: &str) -> FaultResult<()> {
842        let mut workers = self.workers.write();
843        let worker = workers.get_mut(worker_id).ok_or_else(|| {
844            FaultToleranceError::SupervisorRestartFailed {
845                worker_id: worker_id.to_string(),
846                attempts: 0,
847            }
848        })?;
849        worker.status = WorkerStatus::Running;
850        info!("Worker {} successfully restarted", worker_id);
851        Ok(())
852    }
853
854    /// Stops a worker intentionally.
855    pub fn stop_worker(&self, worker_id: &str) -> FaultResult<()> {
856        let mut workers = self.workers.write();
857        let worker = workers.get_mut(worker_id).ok_or_else(|| {
858            FaultToleranceError::SupervisorRestartFailed {
859                worker_id: worker_id.to_string(),
860                attempts: 0,
861            }
862        })?;
863        worker.status = WorkerStatus::Stopped;
864        info!("Worker {} stopped", worker_id);
865        Ok(())
866    }
867
868    /// Returns the current status of a worker.
869    pub fn worker_status(&self, worker_id: &str) -> Option<WorkerStatus> {
870        self.workers.read().get(worker_id).map(|w| w.status.clone())
871    }
872
873    /// Returns all workers whose status matches the given status.
874    pub fn workers_with_status(&self, status: &WorkerStatus) -> Vec<String> {
875        self.workers
876            .read()
877            .values()
878            .filter(|w| &w.status == status)
879            .map(|w| w.worker_id.clone())
880            .collect()
881    }
882
883    /// Returns supervisor statistics.
884    pub fn stats(&self) -> SupervisorStats {
885        let workers = self.workers.read();
886        let running_workers = workers
887            .values()
888            .filter(|w| w.status == WorkerStatus::Running)
889            .count();
890        let exhausted_workers = workers
891            .values()
892            .filter(|w| w.status == WorkerStatus::Exhausted)
893            .count();
894        SupervisorStats {
895            total_workers: workers.len(),
896            running_workers,
897            exhausted_workers,
898            total_restarts: *self.total_restarts.read(),
899            restart_history_len: self.restart_history.read().len(),
900        }
901    }
902
903    /// Returns the full restart history.
904    pub fn restart_history(&self) -> Vec<RestartRecord> {
905        self.restart_history.read().clone()
906    }
907}
908
909// ─── Tests ───────────────────────────────────────────────────────────────────
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    // ── StreamHealthMonitor tests ────────────────────────────────────────────
916
917    #[test]
918    fn test_health_monitor_healthy_state() {
919        let config = HealthMonitorConfig::default();
920        let monitor = StreamHealthMonitor::new(config);
921        monitor.record_metric("error_rate", 0.001);
922        monitor.record_metric("latency_p99_ms", 50.0);
923        monitor.record_metric("backpressure_ratio", 0.1);
924
925        let snap = monitor.snapshot();
926        assert_eq!(snap.status, StreamHealthStatus::Healthy);
927        assert!(snap.active_alerts.is_empty());
928    }
929
930    #[test]
931    fn test_health_monitor_warning_alert() {
932        let config = HealthMonitorConfig::default();
933        let monitor = StreamHealthMonitor::new(config);
934
935        let alerts = monitor.record_metric("error_rate", 0.02);
936        assert_eq!(alerts.len(), 1);
937        assert_eq!(alerts[0].severity, HealthAlertSeverity::Warning);
938
939        let snap = monitor.snapshot();
940        assert_eq!(snap.status, StreamHealthStatus::Degraded);
941    }
942
943    #[test]
944    fn test_health_monitor_critical_alert() {
945        let config = HealthMonitorConfig::default();
946        let monitor = StreamHealthMonitor::new(config);
947
948        let alerts = monitor.record_metric("error_rate", 0.10);
949        assert_eq!(alerts.len(), 1);
950        assert_eq!(alerts[0].severity, HealthAlertSeverity::Critical);
951
952        let snap = monitor.snapshot();
953        assert_eq!(snap.status, StreamHealthStatus::Critical);
954    }
955
956    #[test]
957    fn test_health_monitor_recovery() {
958        let config = HealthMonitorConfig::default();
959        let monitor = StreamHealthMonitor::new(config);
960
961        monitor.record_metric("error_rate", 0.10); // critical
962        let snap = monitor.snapshot();
963        assert_eq!(snap.status, StreamHealthStatus::Critical);
964
965        monitor.record_metric("error_rate", 0.001); // recovered
966        let snap = monitor.snapshot();
967        assert!(snap.active_alerts.is_empty());
968    }
969
970    #[test]
971    fn test_health_monitor_total_alerts_count() {
972        let config = HealthMonitorConfig::default();
973        let monitor = StreamHealthMonitor::new(config);
974        monitor.record_metric("error_rate", 0.02);
975        monitor.record_metric("latency_p99_ms", 200.0);
976        assert_eq!(monitor.total_alerts_raised(), 2);
977    }
978
979    // ── BulkheadIsolator tests ───────────────────────────────────────────────
980
981    #[test]
982    fn test_bulkhead_acquire_and_release() {
983        let mut config = BulkheadConfig::default();
984        config.compartment_capacities.insert("test".to_string(), 2);
985        let isolator = BulkheadIsolator::new(config);
986
987        let p1 = isolator
988            .acquire("test")
989            .expect("first permit should succeed");
990        let p2 = isolator
991            .acquire("test")
992            .expect("second permit should succeed");
993
994        let result = isolator.acquire("test");
995        assert!(
996            matches!(result, Err(FaultToleranceError::BulkheadFull { .. })),
997            "third permit should be rejected"
998        );
999
1000        let stats = isolator
1001            .compartment_stats("test")
1002            .expect("stats should exist");
1003        assert_eq!(stats.active, 2);
1004        assert_eq!(stats.rejected, 1);
1005
1006        drop(p1);
1007        drop(p2);
1008
1009        let stats = isolator
1010            .compartment_stats("test")
1011            .expect("stats should exist");
1012        assert_eq!(stats.active, 0);
1013    }
1014
1015    #[test]
1016    fn test_bulkhead_auto_creates_compartment() {
1017        let config = BulkheadConfig {
1018            compartment_capacities: HashMap::new(),
1019            default_capacity: 5,
1020        };
1021        let isolator = BulkheadIsolator::new(config);
1022        let permit = isolator
1023            .acquire("new-compartment")
1024            .expect("should succeed with default capacity");
1025        drop(permit);
1026    }
1027
1028    #[test]
1029    fn test_bulkhead_different_compartments_isolated() {
1030        let mut config = BulkheadConfig::default();
1031        config.compartment_capacities.insert("a".to_string(), 1);
1032        config.compartment_capacities.insert("b".to_string(), 1);
1033        let isolator = BulkheadIsolator::new(config);
1034
1035        let _pa = isolator.acquire("a").expect("a should succeed");
1036        // a is full
1037        let result_a = isolator.acquire("a");
1038        assert!(matches!(
1039            result_a,
1040            Err(FaultToleranceError::BulkheadFull { .. })
1041        ));
1042
1043        // b should still be available
1044        let _pb = isolator.acquire("b").expect("b should be independent");
1045    }
1046
1047    // ── StreamRetryPolicy tests ──────────────────────────────────────────────
1048
1049    #[test]
1050    fn test_retry_policy_delay_increases() {
1051        let policy = StreamRetryPolicy {
1052            max_attempts: 5,
1053            initial_delay: Duration::from_millis(100),
1054            backoff_multiplier: 2.0,
1055            max_delay: Duration::from_secs(60),
1056            jitter: false,
1057        };
1058        let d0 = policy.delay_for_attempt(0);
1059        let d1 = policy.delay_for_attempt(1);
1060        let d2 = policy.delay_for_attempt(2);
1061        assert!(d0 < d1, "delay should increase");
1062        assert!(d1 < d2, "delay should increase");
1063    }
1064
1065    #[test]
1066    fn test_retry_policy_max_delay_cap() {
1067        let policy = StreamRetryPolicy {
1068            max_attempts: 10,
1069            initial_delay: Duration::from_millis(100),
1070            backoff_multiplier: 10.0,
1071            max_delay: Duration::from_millis(500),
1072            jitter: false,
1073        };
1074        let d = policy.delay_for_attempt(5);
1075        assert!(
1076            d <= Duration::from_millis(500) + Duration::from_millis(10),
1077            "delay should not exceed max"
1078        );
1079    }
1080
1081    #[test]
1082    fn test_retry_succeeds_on_first_attempt() {
1083        let policy = StreamRetryPolicy {
1084            max_attempts: 3,
1085            initial_delay: Duration::from_millis(1),
1086            backoff_multiplier: 2.0,
1087            max_delay: Duration::from_secs(1),
1088            jitter: false,
1089        };
1090        let result: FaultResult<i32> = policy.retry("test-op", || Ok::<i32, &str>(42));
1091        assert!(matches!(result, Ok(42)));
1092    }
1093
1094    #[test]
1095    fn test_retry_exhausts_attempts() {
1096        let policy = StreamRetryPolicy {
1097            max_attempts: 2,
1098            initial_delay: Duration::from_millis(1),
1099            backoff_multiplier: 1.0,
1100            max_delay: Duration::from_millis(5),
1101            jitter: false,
1102        };
1103        let mut calls = 0u32;
1104        let result: FaultResult<i32> = policy.retry("always-fail", || {
1105            calls += 1;
1106            Err::<i32, &str>("always fails")
1107        });
1108        assert!(matches!(
1109            result,
1110            Err(FaultToleranceError::MaxRetriesExceeded { .. })
1111        ));
1112        // Called max_attempts + 1 = 3 times
1113        assert_eq!(calls, 3);
1114    }
1115
1116    #[test]
1117    fn test_retry_succeeds_after_failures() {
1118        let policy = StreamRetryPolicy {
1119            max_attempts: 5,
1120            initial_delay: Duration::from_millis(1),
1121            backoff_multiplier: 1.0,
1122            max_delay: Duration::from_millis(10),
1123            jitter: false,
1124        };
1125        let mut calls = 0u32;
1126        let result: FaultResult<i32> = policy.retry("eventually-succeeds", || {
1127            calls += 1;
1128            if calls < 3 {
1129                Err::<i32, &str>("not yet")
1130            } else {
1131                Ok(99)
1132            }
1133        });
1134        assert!(matches!(result, Ok(99)));
1135        assert_eq!(calls, 3);
1136    }
1137
1138    #[tokio::test]
1139    async fn test_retry_async_succeeds() {
1140        let policy = StreamRetryPolicy {
1141            max_attempts: 3,
1142            initial_delay: Duration::from_millis(1),
1143            backoff_multiplier: 1.0,
1144            max_delay: Duration::from_millis(5),
1145            jitter: false,
1146        };
1147        let calls = Arc::new(RwLock::new(0u32));
1148        let calls_clone = Arc::clone(&calls);
1149        let result: FaultResult<i32> = policy
1150            .retry_async("async-op", move || {
1151                let c = Arc::clone(&calls_clone);
1152                async move {
1153                    let mut lock = c.write();
1154                    *lock += 1;
1155                    let v = *lock;
1156                    drop(lock);
1157                    if v < 2 {
1158                        Err::<i32, &str>("not ready")
1159                    } else {
1160                        Ok(7)
1161                    }
1162                }
1163            })
1164            .await;
1165        assert!(matches!(result, Ok(7)));
1166    }
1167
1168    // ── StreamSupervisor tests ───────────────────────────────────────────────
1169
1170    #[test]
1171    fn test_supervisor_register_and_failure_restart() {
1172        let config = SupervisorConfig::default();
1173        let supervisor = StreamSupervisor::new(config);
1174        supervisor.register_worker("worker-1");
1175
1176        let status = supervisor
1177            .report_failure("worker-1", "connection lost")
1178            .expect("should handle failure");
1179        assert_eq!(status, WorkerStatus::Restarting);
1180
1181        supervisor
1182            .acknowledge_restart("worker-1")
1183            .expect("ack should succeed");
1184        assert_eq!(
1185            supervisor.worker_status("worker-1"),
1186            Some(WorkerStatus::Running)
1187        );
1188    }
1189
1190    #[test]
1191    fn test_supervisor_exhausted_after_max_restarts() {
1192        let config = SupervisorConfig {
1193            max_restarts: 2,
1194            ..Default::default()
1195        };
1196        let supervisor = StreamSupervisor::new(config);
1197        supervisor.register_worker("worker-x");
1198
1199        for _ in 0..2 {
1200            let status = supervisor
1201                .report_failure("worker-x", "crash")
1202                .expect("failure should be handled");
1203            if status == WorkerStatus::Restarting {
1204                supervisor.acknowledge_restart("worker-x").ok();
1205            }
1206        }
1207
1208        let final_status = supervisor
1209            .report_failure("worker-x", "final crash")
1210            .expect("final failure should be handled");
1211        assert_eq!(final_status, WorkerStatus::Exhausted);
1212
1213        let stats = supervisor.stats();
1214        assert_eq!(stats.exhausted_workers, 1);
1215    }
1216
1217    #[test]
1218    fn test_supervisor_stop_worker() {
1219        let config = SupervisorConfig::default();
1220        let supervisor = StreamSupervisor::new(config);
1221        supervisor.register_worker("w1");
1222        supervisor.stop_worker("w1").expect("stop should succeed");
1223        assert_eq!(supervisor.worker_status("w1"), Some(WorkerStatus::Stopped));
1224    }
1225
1226    #[test]
1227    fn test_supervisor_one_for_all() {
1228        let config = SupervisorConfig {
1229            max_restarts: 5,
1230            one_for_all: true,
1231            ..Default::default()
1232        };
1233        let supervisor = StreamSupervisor::new(config);
1234        supervisor.register_worker("w1");
1235        supervisor.register_worker("w2");
1236        supervisor.register_worker("w3");
1237
1238        supervisor
1239            .report_failure("w1", "cascade test")
1240            .expect("failure should be handled");
1241
1242        // w2 and w3 should also be restarting due to one-for-all
1243        let restarting = supervisor.workers_with_status(&WorkerStatus::Restarting);
1244        // w1 + at least w2 and w3
1245        assert!(
1246            restarting.len() >= 2,
1247            "siblings should also restart: {:?}",
1248            restarting
1249        );
1250    }
1251
1252    #[test]
1253    fn test_supervisor_restart_history() {
1254        let config = SupervisorConfig::default();
1255        let supervisor = StreamSupervisor::new(config);
1256        supervisor.register_worker("wh");
1257
1258        supervisor.report_failure("wh", "reason-1").ok();
1259        supervisor.acknowledge_restart("wh").ok();
1260        supervisor.report_failure("wh", "reason-2").ok();
1261
1262        let history = supervisor.restart_history();
1263        assert!(history.len() >= 2);
1264        assert_eq!(history[0].reason, "reason-1");
1265    }
1266
1267    #[test]
1268    fn test_supervisor_stats() {
1269        let config = SupervisorConfig::default();
1270        let supervisor = StreamSupervisor::new(config);
1271        supervisor.register_worker("s1");
1272        supervisor.register_worker("s2");
1273
1274        supervisor.report_failure("s1", "err").ok();
1275        supervisor.acknowledge_restart("s1").ok();
1276
1277        let stats = supervisor.stats();
1278        assert_eq!(stats.total_workers, 2);
1279        assert_eq!(stats.running_workers, 2); // s1 restarted, s2 never failed
1280        assert_eq!(stats.total_restarts, 1);
1281    }
1282}