1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, VecDeque};
6use std::time::{Duration, Instant};
7use uuid::Uuid;
8
9#[derive(Debug)]
11pub struct ErrorRecoverySystem {
12 config: ErrorRecoveryConfig,
13 recovery_strategies: HashMap<ErrorType, Vec<RecoveryStrategy>>,
14 error_history: VecDeque<ErrorEvent>,
15 recovery_history: VecDeque<RecoveryEvent>,
16 circuit_breaker: CircuitBreaker,
17 health_monitor: SystemHealthMonitor,
18 failsafe_manager: FailsafeManager,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ErrorRecoveryConfig {
24 pub enabled: bool,
25 pub max_retry_attempts: usize,
26 pub retry_delay_ms: u64,
27 pub circuit_breaker_threshold: usize,
28 pub health_check_interval_ms: u64,
29 pub auto_failsafe_enabled: bool,
30 pub error_history_limit: usize,
31 pub recovery_timeout_ms: u64,
32}
33
34impl Default for ErrorRecoveryConfig {
35 fn default() -> Self {
36 Self {
37 enabled: true,
38 max_retry_attempts: 3,
39 retry_delay_ms: 100,
40 circuit_breaker_threshold: 5,
41 health_check_interval_ms: 5000,
42 auto_failsafe_enabled: true,
43 error_history_limit: 1000,
44 recovery_timeout_ms: 30000,
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
51pub enum ErrorType {
52 TensorInspectionError,
53 GradientDebuggingError,
54 ModelDiagnosticsError,
55 VisualizationError,
56 MemoryProfilingError,
57 IOError,
58 NetworkError,
59 ResourceExhaustion,
60 ConfigurationError,
61 DataCorruption,
62 SystemFailure,
63 UserError,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum RecoveryStrategy {
69 Retry { max_attempts: usize, delay_ms: u64 },
70 Fallback { alternative_method: String },
71 GracefulDegradation { reduced_functionality: String },
72 ResourceCleanup { cleanup_type: String },
73 SystemReset { component: String },
74 EmergencyShutdown,
75 UserNotification { message: String },
76 AutomaticRepair { repair_action: String },
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ErrorEvent {
82 pub id: Uuid,
83 pub error_type: ErrorType,
84 pub error_message: String,
85 pub component: String,
86 pub severity: ErrorSeverity,
87 pub timestamp: chrono::DateTime<chrono::Utc>,
88 pub context: ErrorContext,
89 pub stack_trace: Option<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub enum ErrorSeverity {
95 Low,
96 Medium,
97 High,
98 Critical,
99 Fatal,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ErrorContext {
105 pub session_id: Uuid,
106 pub operation: String,
107 pub parameters: HashMap<String, String>,
108 pub system_state: SystemState,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SystemState {
114 pub memory_usage_mb: u64,
115 pub cpu_usage_percent: f64,
116 pub active_tensors: usize,
117 pub active_sessions: usize,
118 pub uptime_seconds: u64,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RecoveryEvent {
124 pub id: Uuid,
125 pub error_id: Uuid,
126 pub strategy: RecoveryStrategy,
127 pub start_time: chrono::DateTime<chrono::Utc>,
128 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
129 pub success: Option<bool>,
130 pub result_message: String,
131 pub attempts: usize,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct CircuitBreaker {
137 pub state: CircuitState,
138 pub failure_count: usize,
139 pub last_failure_time: Option<chrono::DateTime<chrono::Utc>>,
140 pub threshold: usize,
141 pub timeout_duration: Duration,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub enum CircuitState {
147 Closed,
148 Open,
149 HalfOpen,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SystemHealthMonitor {
155 pub overall_health: HealthStatus,
156 pub component_health: HashMap<String, HealthStatus>,
157 pub last_health_check: chrono::DateTime<chrono::Utc>,
158 pub health_metrics: HealthMetrics,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum HealthStatus {
164 Healthy,
165 Degraded,
166 Unhealthy,
167 Critical,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct HealthMetrics {
173 pub error_rate: f64,
176 pub recovery_success_rate: Option<f64>,
185 pub average_recovery_time_ms: Option<f64>,
192 pub memory_health_score: Option<f64>,
196 pub stability_score: Option<f64>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct FailsafeManager {
203 pub enabled: bool,
204 pub emergency_protocols: Vec<EmergencyProtocol>,
205 pub safe_mode_enabled: bool,
206 pub data_backup_enabled: bool,
207 pub last_backup: Option<chrono::DateTime<chrono::Utc>>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct EmergencyProtocol {
213 pub name: String,
214 pub trigger_conditions: Vec<String>,
215 pub actions: Vec<String>,
216 pub priority: u8,
217}
218
219impl ErrorRecoverySystem {
220 pub fn new(config: ErrorRecoveryConfig) -> Self {
222 let mut system = Self {
223 config,
224 recovery_strategies: HashMap::new(),
225 error_history: VecDeque::new(),
226 recovery_history: VecDeque::new(),
227 circuit_breaker: CircuitBreaker {
228 state: CircuitState::Closed,
229 failure_count: 0,
230 last_failure_time: None,
231 threshold: 5,
232 timeout_duration: Duration::from_secs(60),
233 },
234 health_monitor: SystemHealthMonitor {
235 overall_health: HealthStatus::Healthy,
236 component_health: HashMap::new(),
237 last_health_check: chrono::Utc::now(),
238 health_metrics: HealthMetrics {
239 error_rate: 0.0,
242 recovery_success_rate: None,
243 average_recovery_time_ms: None,
244 memory_health_score: None,
245 stability_score: None,
246 },
247 },
248 failsafe_manager: FailsafeManager {
249 enabled: true,
250 emergency_protocols: Vec::new(),
251 safe_mode_enabled: false,
252 data_backup_enabled: true,
253 last_backup: None,
254 },
255 };
256
257 system.initialize_default_strategies();
258 system.initialize_emergency_protocols();
259 system
260 }
261
262 pub async fn handle_error(&mut self, error: ErrorEvent) -> Result<RecoveryResult> {
264 if matches!(self.circuit_breaker.state, CircuitState::Open) {
266 return Ok(RecoveryResult {
267 success: false,
268 strategy_used: None,
269 message: "Circuit breaker is open - recovery attempts suspended".to_string(),
270 recovery_time: Duration::from_millis(0),
271 });
272 }
273
274 self.record_error(error.clone());
276
277 if self.should_trigger_emergency_protocol(&error) {
279 return self.execute_emergency_protocol(&error).await;
280 }
281
282 let recovery_result = self.attempt_recovery(&error).await?;
284
285 self.update_circuit_breaker(&recovery_result);
287 self.update_health_metrics(&error, &recovery_result);
288
289 Ok(recovery_result)
290 }
291
292 pub fn record_error(&mut self, error: ErrorEvent) {
294 self.error_history.push_back(error);
295
296 while self.error_history.len() > self.config.error_history_limit {
298 self.error_history.pop_front();
299 }
300 }
301
302 pub async fn attempt_recovery(&mut self, error: &ErrorEvent) -> Result<RecoveryResult> {
304 let strategies = self.get_recovery_strategies(&error.error_type);
305
306 for (attempt, strategy) in strategies.iter().enumerate() {
307 if attempt >= self.config.max_retry_attempts {
308 break;
309 }
310
311 let recovery_event = RecoveryEvent {
312 id: Uuid::new_v4(),
313 error_id: error.id,
314 strategy: strategy.clone(),
315 start_time: chrono::Utc::now(),
316 end_time: None,
317 success: None,
318 result_message: String::new(),
319 attempts: attempt + 1,
320 };
321
322 let result = self.execute_recovery_strategy(strategy, error).await?;
323
324 let mut updated_event = recovery_event;
325 updated_event.end_time = Some(chrono::Utc::now());
326 updated_event.success = Some(result.success);
327 updated_event.result_message = result.message.clone();
328
329 self.recovery_history.push_back(updated_event);
330
331 if result.success {
332 return Ok(result);
333 }
334
335 if attempt < strategies.len() - 1 {
337 tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
338 }
339 }
340
341 Ok(RecoveryResult {
342 success: false,
343 strategy_used: None,
344 message: "All recovery strategies failed".to_string(),
345 recovery_time: Duration::from_millis(0),
346 })
347 }
348
349 pub async fn execute_recovery_strategy(
351 &self,
352 strategy: &RecoveryStrategy,
353 error: &ErrorEvent,
354 ) -> Result<RecoveryResult> {
355 let start_time = Instant::now();
356
357 let result = match strategy {
358 RecoveryStrategy::Retry {
359 max_attempts,
360 delay_ms,
361 } => self.execute_retry_strategy(*max_attempts, *delay_ms, error).await,
362 RecoveryStrategy::Fallback { alternative_method } => {
363 self.execute_fallback_strategy(alternative_method, error).await
364 },
365 RecoveryStrategy::GracefulDegradation {
366 reduced_functionality,
367 } => self.execute_degradation_strategy(reduced_functionality, error).await,
368 RecoveryStrategy::ResourceCleanup { cleanup_type } => {
369 self.execute_cleanup_strategy(cleanup_type, error).await
370 },
371 RecoveryStrategy::SystemReset { component } => {
372 self.execute_reset_strategy(component, error).await
373 },
374 RecoveryStrategy::EmergencyShutdown => self.execute_shutdown_strategy(error).await,
375 RecoveryStrategy::UserNotification { message } => {
376 self.execute_notification_strategy(message, error).await
377 },
378 RecoveryStrategy::AutomaticRepair { repair_action } => {
379 self.execute_repair_strategy(repair_action, error).await
380 },
381 };
382
383 let recovery_time = start_time.elapsed();
384
385 match result {
386 Ok(mut recovery_result) => {
387 recovery_result.recovery_time = recovery_time;
388 recovery_result.strategy_used = Some(strategy.clone());
389 Ok(recovery_result)
390 },
391 Err(e) => Ok(RecoveryResult {
392 success: false,
393 strategy_used: Some(strategy.clone()),
394 message: format!("Recovery strategy failed: {}", e),
395 recovery_time,
396 }),
397 }
398 }
399
400 pub fn get_recovery_strategies(&self, error_type: &ErrorType) -> Vec<RecoveryStrategy> {
402 self.recovery_strategies.get(error_type).cloned().unwrap_or_default()
403 }
404
405 pub async fn check_system_health(&mut self) -> HealthStatus {
407 self.health_monitor.last_health_check = chrono::Utc::now();
409
410 let recent_errors = self
412 .error_history
413 .iter()
414 .filter(|e| {
415 let age = chrono::Utc::now() - e.timestamp;
416 age < chrono::Duration::minutes(5)
417 })
418 .count();
419
420 self.health_monitor.health_metrics.error_rate = recent_errors as f64 / 100.0; let recent_recoveries = self
424 .recovery_history
425 .iter()
426 .filter(|r| {
427 if let Some(end_time) = r.end_time {
428 let age = chrono::Utc::now() - end_time;
429 age < chrono::Duration::minutes(5)
430 } else {
431 false
432 }
433 })
434 .collect::<Vec<_>>();
435
436 if recent_recoveries.is_empty() {
437 self.health_monitor.health_metrics.recovery_success_rate = None;
440 self.health_monitor.health_metrics.average_recovery_time_ms = None;
441 } else {
442 let successful_recoveries =
443 recent_recoveries.iter().filter(|r| r.success.unwrap_or(false)).count();
444 self.health_monitor.health_metrics.recovery_success_rate =
445 Some(successful_recoveries as f64 / recent_recoveries.len() as f64);
446 let total_ms: f64 = recent_recoveries
448 .iter()
449 .filter_map(|r| {
450 r.end_time.map(|end| (end - r.start_time).num_milliseconds() as f64)
451 })
452 .sum();
453 self.health_monitor.health_metrics.average_recovery_time_ms =
454 Some(total_ms / recent_recoveries.len() as f64);
455 }
456
457 self.health_monitor.overall_health = if self.health_monitor.health_metrics.error_rate > 0.5
459 {
460 HealthStatus::Critical
461 } else if self.health_monitor.health_metrics.error_rate > 0.2 {
462 HealthStatus::Unhealthy
463 } else if self.health_monitor.health_metrics.error_rate > 0.1 {
464 HealthStatus::Degraded
465 } else {
466 HealthStatus::Healthy
467 };
468
469 self.health_monitor.overall_health.clone()
470 }
471
472 pub fn enable_safe_mode(&mut self) {
474 self.failsafe_manager.safe_mode_enabled = true;
475 tracing::warn!("Safe mode enabled - operating with reduced functionality");
476 }
477
478 pub fn disable_safe_mode(&mut self) {
480 self.failsafe_manager.safe_mode_enabled = false;
481 tracing::info!("Safe mode disabled - full functionality restored");
482 }
483
484 pub fn get_error_statistics(&self) -> ErrorStatistics {
486 let total_errors = self.error_history.len();
487 let error_type_counts = self.error_history.iter().fold(HashMap::new(), |mut acc, error| {
488 *acc.entry(error.error_type.clone()).or_insert(0) += 1;
489 acc
490 });
491
492 let severity_counts = self.error_history.iter().fold(HashMap::new(), |mut acc, error| {
493 *acc.entry(format!("{:?}", error.severity)).or_insert(0) += 1;
494 acc
495 });
496
497 ErrorStatistics {
498 total_errors,
499 error_type_counts,
500 severity_counts,
501 recovery_success_rate: self.health_monitor.health_metrics.recovery_success_rate,
502 circuit_breaker_state: self.circuit_breaker.state.clone(),
503 system_health: self.health_monitor.overall_health.clone(),
504 }
505 }
506
507 fn initialize_default_strategies(&mut self) {
510 self.recovery_strategies.insert(
512 ErrorType::TensorInspectionError,
513 vec![
514 RecoveryStrategy::Retry {
515 max_attempts: 3,
516 delay_ms: 100,
517 },
518 RecoveryStrategy::ResourceCleanup {
519 cleanup_type: "tensor_cache".to_string(),
520 },
521 RecoveryStrategy::Fallback {
522 alternative_method: "simplified_inspection".to_string(),
523 },
524 ],
525 );
526
527 self.recovery_strategies.insert(
528 ErrorType::GradientDebuggingError,
529 vec![
530 RecoveryStrategy::Retry {
531 max_attempts: 2,
532 delay_ms: 200,
533 },
534 RecoveryStrategy::GracefulDegradation {
535 reduced_functionality: "basic_gradient_info".to_string(),
536 },
537 ],
538 );
539
540 self.recovery_strategies.insert(
541 ErrorType::MemoryProfilingError,
542 vec![
543 RecoveryStrategy::ResourceCleanup {
544 cleanup_type: "memory_profiler".to_string(),
545 },
546 RecoveryStrategy::SystemReset {
547 component: "memory_tracker".to_string(),
548 },
549 ],
550 );
551
552 self.recovery_strategies.insert(
553 ErrorType::ResourceExhaustion,
554 vec![
555 RecoveryStrategy::ResourceCleanup {
556 cleanup_type: "all_caches".to_string(),
557 },
558 RecoveryStrategy::GracefulDegradation {
559 reduced_functionality: "essential_only".to_string(),
560 },
561 RecoveryStrategy::EmergencyShutdown,
562 ],
563 );
564
565 }
567
568 fn initialize_emergency_protocols(&mut self) {
569 self.failsafe_manager.emergency_protocols = vec![
570 EmergencyProtocol {
571 name: "Memory Exhaustion Protocol".to_string(),
572 trigger_conditions: vec!["memory_usage > 90%".to_string()],
573 actions: vec![
574 "clear_all_caches".to_string(),
575 "reduce_tracking".to_string(),
576 ],
577 priority: 1,
578 },
579 EmergencyProtocol {
580 name: "Critical Error Protocol".to_string(),
581 trigger_conditions: vec!["error_severity == Fatal".to_string()],
582 actions: vec!["emergency_backup".to_string(), "safe_shutdown".to_string()],
583 priority: 0,
584 },
585 ];
586 }
587
588 fn should_trigger_emergency_protocol(&self, error: &ErrorEvent) -> bool {
589 matches!(error.severity, ErrorSeverity::Fatal)
590 || error.context.system_state.memory_usage_mb > 8192 }
592
593 async fn execute_emergency_protocol(&mut self, error: &ErrorEvent) -> Result<RecoveryResult> {
594 tracing::error!(
595 "Executing emergency protocol for error: {}",
596 error.error_message
597 );
598
599 self.enable_safe_mode();
601
602 if self.failsafe_manager.data_backup_enabled {
604 self.create_emergency_backup().await?;
605 }
606
607 Ok(RecoveryResult {
608 success: true,
609 strategy_used: Some(RecoveryStrategy::EmergencyShutdown),
610 message: "Emergency protocol executed successfully".to_string(),
611 recovery_time: Duration::from_millis(0),
612 })
613 }
614
615 async fn create_emergency_backup(&mut self) -> Result<()> {
616 tracing::info!("Creating emergency backup");
617 self.failsafe_manager.last_backup = Some(chrono::Utc::now());
618 Ok(())
620 }
621
622 fn update_circuit_breaker(&mut self, result: &RecoveryResult) {
623 if result.success {
624 self.circuit_breaker.failure_count = 0;
625 self.circuit_breaker.state = CircuitState::Closed;
626 } else {
627 self.circuit_breaker.failure_count += 1;
628 self.circuit_breaker.last_failure_time = Some(chrono::Utc::now());
629
630 if self.circuit_breaker.failure_count >= self.circuit_breaker.threshold {
631 self.circuit_breaker.state = CircuitState::Open;
632 }
633 }
634 }
635
636 fn update_health_metrics(&mut self, _error: &ErrorEvent, _result: &RecoveryResult) {
637 }
640
641 fn unwired(capability: &str) -> RecoveryResult {
657 RecoveryResult {
658 success: false,
659 strategy_used: None,
660 message: format!(
661 "recovery not performed: {capability} -- ErrorRecoveryManager has no handle to \
662 act on"
663 ),
664 recovery_time: Duration::from_millis(0),
665 }
666 }
667
668 async fn execute_retry_strategy(
669 &self,
670 _max_attempts: usize,
671 _delay_ms: u64,
672 _error: &ErrorEvent,
673 ) -> Result<RecoveryResult> {
674 Ok(Self::unwired(
675 "no retryable operation was supplied with the error",
676 ))
677 }
678
679 async fn execute_fallback_strategy(
680 &self,
681 alternative: &str,
682 _error: &ErrorEvent,
683 ) -> Result<RecoveryResult> {
684 Ok(Self::unwired(&format!(
685 "no dispatcher exists for the alternative method {alternative:?}"
686 )))
687 }
688
689 async fn execute_degradation_strategy(
690 &self,
691 functionality: &str,
692 _error: &ErrorEvent,
693 ) -> Result<RecoveryResult> {
694 Ok(Self::unwired(&format!(
695 "no feature switch exists for {functionality:?}"
696 )))
697 }
698
699 async fn execute_cleanup_strategy(
700 &self,
701 cleanup_type: &str,
702 _error: &ErrorEvent,
703 ) -> Result<RecoveryResult> {
704 Ok(Self::unwired(&format!(
705 "no allocator or cache handle for {cleanup_type:?}"
706 )))
707 }
708
709 async fn execute_reset_strategy(
710 &self,
711 component: &str,
712 _error: &ErrorEvent,
713 ) -> Result<RecoveryResult> {
714 Ok(Self::unwired(&format!(
715 "no component registry entry for {component:?}"
716 )))
717 }
718
719 async fn execute_shutdown_strategy(&self, _error: &ErrorEvent) -> Result<RecoveryResult> {
720 Ok(Self::unwired(
721 "no process supervisor handle to initiate shutdown",
722 ))
723 }
724
725 async fn execute_notification_strategy(
728 &self,
729 message: &str,
730 _error: &ErrorEvent,
731 ) -> Result<RecoveryResult> {
732 tracing::warn!("User notification: {}", message);
733 Ok(RecoveryResult {
734 success: true,
735 strategy_used: None,
736 message: "User notified via the tracing facade".to_string(),
737 recovery_time: Duration::from_millis(0),
738 })
739 }
740
741 async fn execute_repair_strategy(
742 &self,
743 repair_action: &str,
744 _error: &ErrorEvent,
745 ) -> Result<RecoveryResult> {
746 Ok(Self::unwired(&format!(
747 "no repair executor for the action {repair_action:?}"
748 )))
749 }
750}
751
752#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct RecoveryResult {
755 pub success: bool,
756 pub strategy_used: Option<RecoveryStrategy>,
757 pub message: String,
758 pub recovery_time: Duration,
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct ErrorStatistics {
764 pub total_errors: usize,
765 pub error_type_counts: HashMap<ErrorType, usize>,
766 pub severity_counts: HashMap<String, usize>,
767 pub recovery_success_rate: Option<f64>,
770 pub circuit_breaker_state: CircuitState,
771 pub system_health: HealthStatus,
772}
773
774#[cfg(test)]
779mod tests {
780 use super::*;
781
782 fn sample_error() -> ErrorEvent {
783 ErrorEvent {
784 id: Uuid::new_v4(),
785 error_type: ErrorType::TensorInspectionError,
786 error_message: "boom".to_string(),
787 component: "tensor_inspector".to_string(),
788 severity: ErrorSeverity::Medium,
789 timestamp: chrono::Utc::now(),
790 context: ErrorContext {
791 session_id: Uuid::new_v4(),
792 operation: "inspect".to_string(),
793 parameters: HashMap::new(),
794 system_state: SystemState {
795 memory_usage_mb: 0,
796 cpu_usage_percent: 0.0,
797 active_tensors: 0,
798 active_sessions: 0,
799 uptime_seconds: 0,
800 },
801 },
802 stack_trace: None,
803 }
804 }
805
806 #[tokio::test]
809 async fn unwired_recovery_strategies_report_failure_not_success() {
810 let manager = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
811 let error = sample_error();
812
813 for result in [
814 manager.execute_retry_strategy(3, 10, &error).await.expect("call"),
815 manager.execute_fallback_strategy("other", &error).await.expect("call"),
816 manager.execute_degradation_strategy("feature", &error).await.expect("call"),
817 manager.execute_cleanup_strategy("cache", &error).await.expect("call"),
818 manager.execute_reset_strategy("component", &error).await.expect("call"),
819 manager.execute_shutdown_strategy(&error).await.expect("call"),
820 manager.execute_repair_strategy("fix", &error).await.expect("call"),
821 ] {
822 assert!(
823 !result.success,
824 "an unperformed recovery must not report success: {}",
825 result.message
826 );
827 assert!(
828 result.message.contains("recovery not performed"),
829 "the message must say what did not happen: {}",
830 result.message
831 );
832 }
833 }
834
835 #[tokio::test]
836 async fn the_notification_strategy_really_acts_and_reports_success() {
837 let manager = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
838 let error = sample_error();
839 let result = manager
840 .execute_notification_strategy("check the logs", &error)
841 .await
842 .expect("call");
843 assert!(result.success, "the notification is really emitted");
844 assert!(result.message.contains("tracing"));
845 }
846
847 fn make_error_event(error_type: ErrorType) -> ErrorEvent {
848 ErrorEvent {
849 id: Uuid::new_v4(),
850 error_type,
851 error_message: "test error".to_string(),
852 component: "test_component".to_string(),
853 severity: ErrorSeverity::Medium,
854 timestamp: chrono::Utc::now(),
855 context: ErrorContext {
856 session_id: Uuid::new_v4(),
857 operation: "test_op".to_string(),
858 parameters: HashMap::new(),
859 system_state: SystemState {
860 memory_usage_mb: 1024,
861 cpu_usage_percent: 50.0,
862 active_tensors: 4,
863 active_sessions: 1,
864 uptime_seconds: 100,
865 },
866 },
867 stack_trace: None,
868 }
869 }
870
871 #[test]
874 fn test_config_default_fields() {
875 let cfg = ErrorRecoveryConfig::default();
876 assert!(cfg.enabled);
877 assert!(cfg.max_retry_attempts > 0);
878 assert!(cfg.circuit_breaker_threshold > 0);
879 assert!(cfg.error_history_limit > 0);
880 }
881
882 #[test]
885 fn test_system_new_initializes_strategies() {
886 let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
887 let strategies = system.get_recovery_strategies(&ErrorType::TensorInspectionError);
889 assert!(!strategies.is_empty());
890 }
891
892 #[test]
893 fn test_system_new_circuit_breaker_closed() {
894 let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
895 assert!(matches!(system.circuit_breaker.state, CircuitState::Closed));
896 }
897
898 #[test]
901 fn test_record_error_adds_to_history() {
902 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
903 let event = make_error_event(ErrorType::IOError);
904 system.record_error(event);
905 assert_eq!(system.error_history.len(), 1);
906 }
907
908 #[test]
909 fn test_record_error_respects_history_limit() {
910 let mut cfg = ErrorRecoveryConfig::default();
911 cfg.error_history_limit = 3;
912 let mut system = ErrorRecoverySystem::new(cfg);
913 for _ in 0..5 {
914 system.record_error(make_error_event(ErrorType::NetworkError));
915 }
916 assert_eq!(system.error_history.len(), 3);
917 }
918
919 #[test]
922 fn test_enable_disable_safe_mode() {
923 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
924 assert!(!system.failsafe_manager.safe_mode_enabled);
925 system.enable_safe_mode();
926 assert!(system.failsafe_manager.safe_mode_enabled);
927 system.disable_safe_mode();
928 assert!(!system.failsafe_manager.safe_mode_enabled);
929 }
930
931 #[test]
934 fn test_error_statistics_empty() {
935 let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
936 let stats = system.get_error_statistics();
937 assert_eq!(stats.total_errors, 0);
938 assert!(matches!(stats.circuit_breaker_state, CircuitState::Closed));
939 assert!(matches!(stats.system_health, HealthStatus::Healthy));
940 }
941
942 #[test]
943 fn test_error_statistics_with_errors() {
944 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
945 system.record_error(make_error_event(ErrorType::IOError));
946 system.record_error(make_error_event(ErrorType::NetworkError));
947 let stats = system.get_error_statistics();
948 assert_eq!(stats.total_errors, 2);
949 assert_eq!(
950 stats.error_type_counts.get(&ErrorType::IOError).copied().unwrap_or(0),
951 1
952 );
953 }
954
955 #[test]
958 fn test_error_type_variants() {
959 let types = [
960 ErrorType::TensorInspectionError,
961 ErrorType::GradientDebuggingError,
962 ErrorType::ModelDiagnosticsError,
963 ErrorType::VisualizationError,
964 ErrorType::MemoryProfilingError,
965 ErrorType::IOError,
966 ErrorType::NetworkError,
967 ErrorType::ResourceExhaustion,
968 ErrorType::ConfigurationError,
969 ErrorType::DataCorruption,
970 ErrorType::SystemFailure,
971 ErrorType::UserError,
972 ];
973 for t in &types {
974 assert!(!format!("{:?}", t).is_empty());
975 }
976 }
977
978 #[test]
981 fn test_error_severity_variants() {
982 let severities = [
983 ErrorSeverity::Low,
984 ErrorSeverity::Medium,
985 ErrorSeverity::High,
986 ErrorSeverity::Critical,
987 ErrorSeverity::Fatal,
988 ];
989 for s in &severities {
990 assert!(!format!("{:?}", s).is_empty());
991 }
992 }
993
994 #[test]
997 fn test_recovery_strategy_variants() {
998 let strats = [
999 RecoveryStrategy::Retry {
1000 max_attempts: 3,
1001 delay_ms: 100,
1002 },
1003 RecoveryStrategy::Fallback {
1004 alternative_method: "alt".to_string(),
1005 },
1006 RecoveryStrategy::GracefulDegradation {
1007 reduced_functionality: "basic".to_string(),
1008 },
1009 RecoveryStrategy::ResourceCleanup {
1010 cleanup_type: "cache".to_string(),
1011 },
1012 RecoveryStrategy::SystemReset {
1013 component: "comp".to_string(),
1014 },
1015 RecoveryStrategy::EmergencyShutdown,
1016 RecoveryStrategy::UserNotification {
1017 message: "msg".to_string(),
1018 },
1019 RecoveryStrategy::AutomaticRepair {
1020 repair_action: "repair".to_string(),
1021 },
1022 ];
1023 for s in &strats {
1024 assert!(!format!("{:?}", s).is_empty());
1025 }
1026 }
1027
1028 #[test]
1031 fn test_circuit_state_variants() {
1032 let states = [
1033 CircuitState::Closed,
1034 CircuitState::Open,
1035 CircuitState::HalfOpen,
1036 ];
1037 for s in &states {
1038 assert!(!format!("{:?}", s).is_empty());
1039 }
1040 }
1041
1042 #[test]
1045 fn test_health_status_variants() {
1046 let statuses = [
1047 HealthStatus::Healthy,
1048 HealthStatus::Degraded,
1049 HealthStatus::Unhealthy,
1050 HealthStatus::Critical,
1051 ];
1052 for s in &statuses {
1053 assert!(!format!("{:?}", s).is_empty());
1054 }
1055 }
1056
1057 #[test]
1060 fn test_circuit_breaker_initial_state() {
1061 let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1062 assert_eq!(system.circuit_breaker.failure_count, 0);
1063 assert!(system.circuit_breaker.last_failure_time.is_none());
1064 assert_eq!(system.circuit_breaker.threshold, 5);
1065 }
1066
1067 #[test]
1070 fn test_system_state_construction() {
1071 let state = SystemState {
1072 memory_usage_mb: 2048,
1073 cpu_usage_percent: 75.5,
1074 active_tensors: 10,
1075 active_sessions: 2,
1076 uptime_seconds: 3600,
1077 };
1078 assert_eq!(state.memory_usage_mb, 2048);
1079 assert!((state.cpu_usage_percent - 75.5).abs() < 1e-6);
1080 }
1081
1082 #[test]
1085 fn test_health_metrics_initial_values() {
1086 let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1087 let m = &system.health_monitor.health_metrics;
1088 assert_eq!(m.error_rate, 0.0);
1092 assert_eq!(m.recovery_success_rate, None);
1093 assert_eq!(m.average_recovery_time_ms, None);
1094 assert_eq!(m.memory_health_score, None);
1095 assert_eq!(m.stability_score, None);
1096 }
1097
1098 #[tokio::test]
1102 async fn test_health_metrics_are_computed_from_real_recovery_outcomes() {
1103 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1104 let now = chrono::Utc::now();
1105 for (success, millis) in [(true, 40i64), (true, 60), (false, 200)] {
1106 system.recovery_history.push_back(RecoveryEvent {
1107 id: Uuid::new_v4(),
1108 error_id: Uuid::new_v4(),
1109 strategy: RecoveryStrategy::Retry {
1110 max_attempts: 1,
1111 delay_ms: 0,
1112 },
1113 start_time: now - chrono::Duration::milliseconds(millis),
1114 end_time: Some(now),
1115 success: Some(success),
1116 result_message: "test".to_string(),
1117 attempts: 1,
1118 });
1119 }
1120
1121 system.check_system_health().await;
1122 let m = &system.health_monitor.health_metrics;
1123 let rate = m.recovery_success_rate.expect("three attempts completed in the window");
1124 assert!(
1125 (rate - 2.0 / 3.0).abs() < 1e-9,
1126 "two of three succeeded, got {rate}"
1127 );
1128 let mean_ms = m.average_recovery_time_ms.expect("three attempts completed in the window");
1129 assert!(
1130 (mean_ms - 100.0).abs() < 5.0,
1131 "(40+60+200)/3 = 100ms, got {mean_ms}"
1132 );
1133
1134 assert_eq!(m.memory_health_score, None);
1136 assert_eq!(m.stability_score, None);
1137 }
1138
1139 #[tokio::test]
1142 async fn test_health_check_reports_absence_when_nothing_recovered_recently() {
1143 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1144 system.check_system_health().await;
1145 assert_eq!(
1146 system.health_monitor.health_metrics.recovery_success_rate,
1147 None
1148 );
1149 assert_eq!(system.get_error_statistics().recovery_success_rate, None);
1150 }
1151
1152 #[tokio::test]
1155 async fn test_handle_error_with_open_circuit_breaker() {
1156 let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1157 system.circuit_breaker.state = CircuitState::Open;
1158 let event = make_error_event(ErrorType::IOError);
1159 let result = system.handle_error(event).await.expect("should succeed");
1160 assert!(!result.success);
1161 assert!(result.message.contains("Circuit breaker"));
1162 }
1163}