1#![allow(dead_code)]
11use std::collections::HashMap;
38use std::time::{Duration, Instant};
39use tracing::{debug, info, warn};
40
41#[derive(Debug, Clone)]
47pub struct HealingConfig {
48 pub max_retries: u32,
50 pub base_delay_ms: u64,
52 pub max_delay_ms: u64,
54 pub jitter: f64,
56 pub circuit_breaker_threshold: u32,
58 pub circuit_breaker_recovery_secs: u64,
60 pub worker_dead_after_secs: u64,
62 pub max_failed_tasks: u32,
64}
65
66impl Default for HealingConfig {
67 fn default() -> Self {
68 Self {
69 max_retries: 3,
70 base_delay_ms: 1000,
71 max_delay_ms: 30000,
72 jitter: 0.3,
73 circuit_breaker_threshold: 5,
74 circuit_breaker_recovery_secs: 30,
75 worker_dead_after_secs: 60,
76 max_failed_tasks: 10,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HealingCircuitState {
88 Closed,
90 Open,
92 HalfOpen,
94}
95
96#[derive(Debug, Clone)]
98pub struct HealingCircuitBreaker {
99 pub state: HealingCircuitState,
101 pub failure_count: u32,
103 pub last_failure_time: Option<Instant>,
105 pub open_duration: Duration,
107 pub threshold: u32,
109}
110
111impl HealingCircuitBreaker {
112 pub fn new(open_duration_secs: u64, threshold: u32) -> Self {
114 Self {
115 state: HealingCircuitState::Closed,
116 failure_count: 0,
117 last_failure_time: None,
118 open_duration: Duration::from_secs(open_duration_secs),
119 threshold,
120 }
121 }
122
123 pub fn record_success(&mut self) {
125 self.failure_count = 0;
126 self.state = HealingCircuitState::Closed;
127 }
128
129 pub fn record_failure(&mut self) {
131 self.failure_count += 1;
132 self.last_failure_time = Some(Instant::now());
133 if self.failure_count >= self.threshold {
134 self.state = HealingCircuitState::Open;
135 warn!(
136 failure_count = self.failure_count,
137 threshold = self.threshold,
138 "Circuit breaker opened"
139 );
140 }
141 }
142
143 pub fn can_execute(&mut self) -> bool {
146 match self.state {
147 HealingCircuitState::Closed => true,
148 HealingCircuitState::Open => {
149 if let Some(last) = self.last_failure_time {
150 if last.elapsed() >= self.open_duration {
151 self.state = HealingCircuitState::HalfOpen;
152 info!("Circuit breaker half-open — allowing test request");
153 true
154 } else {
155 false
156 }
157 } else {
158 false
159 }
160 }
161 HealingCircuitState::HalfOpen => true,
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
172pub struct FailureRecord {
173 pub agent_id: String,
175 pub failure_count: u32,
177 pub first_failure: Instant,
179 pub last_failure: Instant,
181 pub last_error: String,
183 pub is_dead: bool,
185}
186
187impl FailureRecord {
188 pub fn new(agent_id: &str, error: &str) -> Self {
190 Self {
191 agent_id: agent_id.to_string(),
192 failure_count: 1,
193 first_failure: Instant::now(),
194 last_failure: Instant::now(),
195 last_error: error.to_string(),
196 is_dead: false,
197 }
198 }
199
200 pub fn add_failure(&mut self, error: &str) {
202 self.failure_count += 1;
203 self.last_failure = Instant::now();
204 self.last_error = error.to_string();
205 }
206
207 pub fn record_success(&mut self) {
209 self.failure_count = 0;
210 self.is_dead = false;
211 }
212
213 pub fn time_since_last_failure(&self) -> Duration {
215 self.last_failure.elapsed()
216 }
217}
218
219#[derive(Debug)]
230pub struct SelfHealingEngine {
231 pub config: HealingConfig,
233 failure_records: HashMap<String, FailureRecord>,
235 circuit_breakers: HashMap<String, HealingCircuitBreaker>,
237}
238
239impl SelfHealingEngine {
240 pub fn new() -> Self {
242 Self {
243 config: HealingConfig::default(),
244 failure_records: HashMap::new(),
245 circuit_breakers: HashMap::new(),
246 }
247 }
248
249 pub fn with_config(config: HealingConfig) -> Self {
251 Self {
252 config,
253 failure_records: HashMap::new(),
254 circuit_breakers: HashMap::new(),
255 }
256 }
257
258 pub fn record_failure(&mut self, agent_id: &str, error: &str) -> u32 {
264 let record = self
266 .failure_records
267 .entry(agent_id.to_string())
268 .and_modify(|r| r.add_failure(error))
269 .or_insert_with(|| FailureRecord::new(agent_id, error));
270
271 if record.failure_count >= self.config.max_failed_tasks {
273 record.is_dead = true;
274 warn!(
275 agent_id = %agent_id,
276 failure_count = record.failure_count,
277 max_failed = self.config.max_failed_tasks,
278 "Agent marked as dead"
279 );
280 }
281
282 let cb = self
284 .circuit_breakers
285 .entry(agent_id.to_string())
286 .or_insert_with(|| {
287 HealingCircuitBreaker::new(
288 self.config.circuit_breaker_recovery_secs,
289 self.config.circuit_breaker_threshold,
290 )
291 });
292 cb.record_failure();
293
294 record.failure_count
295 }
296
297 pub fn record_success(&mut self, agent_id: &str) {
299 if let Some(record) = self.failure_records.get_mut(agent_id) {
300 record.record_success();
301 }
302 if let Some(cb) = self.circuit_breakers.get_mut(agent_id) {
303 cb.record_success();
304 }
305 }
306
307 pub fn is_healthy(&mut self, agent_id: &str) -> bool {
315 if let Some(cb) = self.circuit_breakers.get_mut(agent_id) {
317 if !cb.can_execute() {
318 debug!(
319 agent_id = %agent_id,
320 state = ?cb.state,
321 "Agent blocked by circuit breaker"
322 );
323 return false;
324 }
325 }
326
327 if let Some(record) = self.failure_records.get(agent_id) {
329 if record.is_dead {
330 if record.time_since_last_failure()
332 > Duration::from_secs(self.config.worker_dead_after_secs)
333 {
334 debug!(
335 agent_id = %agent_id,
336 "Agent revived after dead timeout"
337 );
338 return true;
339 }
340 return false;
341 }
342 }
343
344 true
345 }
346
347 pub fn failure_count(&self, agent_id: &str) -> u32 {
349 self.failure_records
350 .get(agent_id)
351 .map(|r| r.failure_count)
352 .unwrap_or(0)
353 }
354
355 pub fn circuit_state(&self, agent_id: &str) -> Option<HealingCircuitState> {
357 self.circuit_breakers.get(agent_id).map(|cb| cb.state)
358 }
359
360 pub fn dead_workers(&self) -> Vec<String> {
364 self.failure_records
365 .iter()
366 .filter(|(_, r)| {
367 r.is_dead
368 && r.time_since_last_failure()
369 > Duration::from_secs(self.config.worker_dead_after_secs)
370 })
371 .map(|(id, _)| id.clone())
372 .collect()
373 }
374
375 pub fn all_dead_workers(&self) -> Vec<String> {
377 self.failure_records
378 .iter()
379 .filter(|(_, r)| r.is_dead)
380 .map(|(id, _)| id.clone())
381 .collect()
382 }
383
384 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
388 use rand::Rng;
389 let exp = 2u64.pow(attempt);
390 let base = self.config.base_delay_ms * exp;
391 let capped = base.min(self.config.max_delay_ms);
392 let jitter_range = (capped as f64) * self.config.jitter;
393 let jitter = rand::thread_rng().gen_range(-jitter_range..=jitter_range) as u64;
394 let delay = capped.saturating_add(jitter).max(self.config.base_delay_ms);
395 Duration::from_millis(delay)
396 }
397
398 pub async fn retry_with_backoff<F, T>(
407 &self,
408 agent_id: &str,
409 is_transient: impl Fn(&str) -> bool,
410 mut operation: F,
411 ) -> Result<T, String>
412 where
413 F: FnMut(u32) -> Result<T, String>,
414 {
415 let max_attempts = self.config.max_retries + 1; let mut last_error = String::from("Unknown error");
417
418 for attempt in 0..max_attempts {
419 if attempt > 0 {
420 let delay = self.delay_for_attempt(attempt - 1);
421 debug!(
422 agent_id = %agent_id,
423 attempt = attempt,
424 delay_ms = delay.as_millis(),
425 "Retrying after backoff"
426 );
427 tokio::time::sleep(delay).await;
428 }
429
430 match operation(attempt) {
431 Ok(result) => {
432 if attempt > 0 {
433 info!(
434 agent_id = %agent_id,
435 attempt = attempt,
436 "Operation succeeded after retry"
437 );
438 }
439 return Ok(result);
440 }
441 Err(e) => {
442 last_error = e.clone();
443 if !is_transient(&e) {
444 debug!(
445 agent_id = %agent_id,
446 attempt = attempt,
447 error = %e,
448 "Non-transient error — not retrying"
449 );
450 return Err(e);
451 }
452 warn!(
453 agent_id = %agent_id,
454 attempt = attempt,
455 max_attempts = max_attempts,
456 error = %e,
457 "Transient error — will retry"
458 );
459 }
460 }
461 }
462
463 Err(format!(
464 "All {} attempts failed for agent '{}': {}",
465 max_attempts, agent_id, last_error
466 ))
467 }
468
469 pub fn reset(&mut self) {
473 self.failure_records.clear();
474 self.circuit_breakers.clear();
475 info!("Self-healing engine reset — all failure records cleared");
476 }
477
478 pub fn reset_agent(&mut self, agent_id: &str) {
480 self.failure_records.remove(agent_id);
481 self.circuit_breakers.remove(agent_id);
482 debug!(agent_id = %agent_id, "Self-healing record reset for agent");
483 }
484
485 pub fn tracked_agents(&self) -> usize {
489 self.failure_records.len()
490 }
491
492 pub fn dead_agent_count(&self) -> usize {
494 self.all_dead_workers().len()
495 }
496
497 pub fn open_circuit_count(&self) -> usize {
499 self.circuit_breakers
500 .values()
501 .filter(|cb| cb.state == HealingCircuitState::Open)
502 .count()
503 }
504}
505
506impl Default for SelfHealingEngine {
507 fn default() -> Self {
508 Self::new()
509 }
510}
511
512#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_healing_config_default() {
522 let config = HealingConfig::default();
523 assert_eq!(config.max_retries, 3);
524 assert_eq!(config.base_delay_ms, 1000);
525 assert_eq!(config.max_delay_ms, 30000);
526 assert_eq!(config.circuit_breaker_threshold, 5);
527 assert_eq!(config.circuit_breaker_recovery_secs, 30);
528 }
529
530 #[test]
531 fn test_circuit_breaker_initial_state() {
532 let mut cb = HealingCircuitBreaker::new(30, 5);
533 assert_eq!(cb.state, HealingCircuitState::Closed);
534 assert_eq!(cb.failure_count, 0);
535 assert!(cb.can_execute());
536 }
537
538 #[test]
539 fn test_circuit_breaker_opens_after_threshold() {
540 let mut cb = HealingCircuitBreaker::new(30, 3);
541 assert!(cb.can_execute());
542
543 cb.record_failure();
544 assert!(cb.can_execute());
545 assert_eq!(cb.failure_count, 1);
546
547 cb.record_failure();
548 assert!(cb.can_execute());
549 assert_eq!(cb.failure_count, 2);
550
551 cb.record_failure();
552 assert_eq!(cb.state, HealingCircuitState::Open);
553 assert!(!cb.can_execute());
554 assert_eq!(cb.failure_count, 3);
555 }
556
557 #[test]
558 fn test_circuit_breaker_success_resets() {
559 let mut cb = HealingCircuitBreaker::new(30, 3);
560 cb.record_failure();
561 cb.record_failure();
562 assert_eq!(cb.failure_count, 2);
563
564 cb.record_success();
565 assert_eq!(cb.state, HealingCircuitState::Closed);
566 assert_eq!(cb.failure_count, 0);
567 }
568
569 #[test]
570 fn test_failure_record_new() {
571 let record = FailureRecord::new("agent-1", "timeout");
572 assert_eq!(record.agent_id, "agent-1");
573 assert_eq!(record.failure_count, 1);
574 assert_eq!(record.last_error, "timeout");
575 assert!(!record.is_dead);
576 }
577
578 #[test]
579 fn test_failure_record_add_failure() {
580 let mut record = FailureRecord::new("agent-1", "timeout");
581 record.add_failure("connection refused");
582 assert_eq!(record.failure_count, 2);
583 assert_eq!(record.last_error, "connection refused");
584 }
585
586 #[test]
587 fn test_failure_record_success_resets() {
588 let mut record = FailureRecord::new("agent-1", "timeout");
589 record.add_failure("connection refused");
590 assert_eq!(record.failure_count, 2);
591
592 record.record_success();
593 assert_eq!(record.failure_count, 0);
594 assert!(!record.is_dead);
595 }
596
597 #[test]
598 fn test_self_healing_engine_new() {
599 let engine = SelfHealingEngine::new();
600 assert_eq!(engine.tracked_agents(), 0);
601 assert_eq!(engine.dead_agent_count(), 0);
602 }
603
604 #[test]
605 fn test_self_healing_engine_record_failure() {
606 let mut engine = SelfHealingEngine::new();
607 let count = engine.record_failure("agent-1", "timeout");
608 assert_eq!(count, 1);
609 assert_eq!(engine.failure_count("agent-1"), 1);
610 assert!(engine.is_healthy("agent-1"));
611 }
612
613 #[test]
614 fn test_self_healing_engine_marks_dead() {
615 let mut engine = SelfHealingEngine::new();
616 engine.config.max_failed_tasks = 3;
617
618 engine.record_failure("agent-1", "error 1");
619 engine.record_failure("agent-1", "error 2");
620 engine.record_failure("agent-1", "error 3");
621
622 assert!(!engine.is_healthy("agent-1"));
623 assert!(engine.all_dead_workers().contains(&"agent-1".to_string()));
624 }
625
626 #[test]
627 fn test_self_healing_engine_circuit_breaker() {
628 let mut engine = SelfHealingEngine::new();
629 engine.config.circuit_breaker_threshold = 3;
630
631 engine.record_failure("agent-1", "error 1");
633 engine.record_failure("agent-1", "error 2");
634 engine.record_failure("agent-1", "error 3");
635
636 assert_eq!(
637 engine.circuit_state("agent-1"),
638 Some(HealingCircuitState::Open)
639 );
640 assert!(!engine.is_healthy("agent-1"));
641 }
642
643 #[test]
644 fn test_self_healing_engine_success_resets() {
645 let mut engine = SelfHealingEngine::new();
646 engine.config.circuit_breaker_threshold = 3;
647
648 engine.record_failure("agent-1", "error 1");
649 engine.record_failure("agent-1", "error 2");
650 assert_eq!(engine.failure_count("agent-1"), 2);
651
652 engine.record_success("agent-1");
653 assert_eq!(engine.failure_count("agent-1"), 0);
654 assert_eq!(
655 engine.circuit_state("agent-1"),
656 Some(HealingCircuitState::Closed)
657 );
658 }
659
660 #[test]
661 fn test_delay_for_attempt() {
662 let engine = SelfHealingEngine::new();
663 let d0 = engine.delay_for_attempt(0);
665 assert!(d0.as_millis() >= 700); assert!(d0.as_millis() <= 1300); let d1 = engine.delay_for_attempt(1);
670 assert!(d1.as_millis() >= 1400);
671 assert!(d1.as_millis() <= 2600);
672
673 let d4 = engine.delay_for_attempt(4);
675 assert!(d4.as_millis() <= 30000 + 9000); }
677
678 #[test]
679 fn test_reset() {
680 let mut engine = SelfHealingEngine::new();
681 engine.record_failure("agent-1", "error");
682 engine.record_failure("agent-2", "error");
683 assert_eq!(engine.tracked_agents(), 2);
684
685 engine.reset();
686 assert_eq!(engine.tracked_agents(), 0);
687 }
688
689 #[test]
690 fn test_reset_agent() {
691 let mut engine = SelfHealingEngine::new();
692 engine.record_failure("agent-1", "error");
693 engine.record_failure("agent-2", "error");
694 assert_eq!(engine.tracked_agents(), 2);
695
696 engine.reset_agent("agent-1");
697 assert_eq!(engine.tracked_agents(), 1);
698 assert_eq!(engine.failure_count("agent-1"), 0);
699 }
700
701 #[test]
702 fn test_dead_workers_empty_initially() {
703 let engine = SelfHealingEngine::new();
704 assert!(engine.dead_workers().is_empty());
705 assert!(engine.all_dead_workers().is_empty());
706 }
707
708 #[test]
709 fn test_open_circuit_count() {
710 let mut engine = SelfHealingEngine::new();
711 engine.config.circuit_breaker_threshold = 2;
712
713 engine.record_failure("agent-1", "error");
714 engine.record_failure("agent-1", "error");
715 engine.record_failure("agent-2", "error");
716 engine.record_failure("agent-2", "error");
717
718 assert_eq!(engine.open_circuit_count(), 2);
719 }
720
721 #[tokio::test]
722 async fn test_retry_with_backoff_succeeds_on_first_attempt() {
723 let engine = SelfHealingEngine::new();
724 let result = engine
725 .retry_with_backoff("agent-1", |_| true, |_| Ok::<_, String>(42))
726 .await;
727 assert_eq!(result, Ok(42));
728 }
729
730 #[tokio::test]
731 async fn test_retry_with_backoff_succeeds_after_retries() {
732 let engine = SelfHealingEngine::new();
733 let attempt = std::sync::atomic::AtomicU32::new(0);
734
735 let result = engine
736 .retry_with_backoff(
737 "agent-1",
738 |_| true,
739 |_| {
740 let prev = attempt.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
741 if prev < 2 {
742 Err("transient error".to_string())
743 } else {
744 Ok(42)
745 }
746 },
747 )
748 .await;
749 assert_eq!(result, Ok(42));
750 }
751
752 #[tokio::test]
753 async fn test_retry_with_backoff_fails_on_non_transient() {
754 let engine = SelfHealingEngine::new();
755 let result: Result<i32, String> = engine
756 .retry_with_backoff(
757 "agent-1",
758 |e| !e.contains("non-transient"),
759 |_| Err("non-transient error".to_string()),
760 )
761 .await;
762 assert!(result.is_err());
763 assert!(result.unwrap_err().contains("non-transient"));
764 }
765
766 #[tokio::test]
767 async fn test_retry_with_backoff_exhausts_retries() {
768 let mut engine = SelfHealingEngine::new();
769 engine.config.max_retries = 2; let result: Result<i32, String> = engine
772 .retry_with_backoff(
773 "agent-1",
774 |_| true,
775 |_| Err("persistent transient error".to_string()),
776 )
777 .await;
778 assert!(result.is_err());
779 let err = result.unwrap_err();
780 assert!(err.contains("All 3 attempts failed"));
781 assert!(err.contains("persistent transient error"));
782 }
783}