use std::time::{Duration, Instant};
use crate::client::policy::TransportPolicy;
use crate::seed::SeedRng;
use crate::transport::{TransportKind, TransportManager};
const MIN_BACKOFF: Duration = Duration::from_secs(1);
const MAX_BACKOFF: Duration = Duration::from_secs(60);
pub struct SelfHealing {
pub heal_count: u32,
last_heal: Option<Instant>,
backoff: Duration,
}
impl SelfHealing {
pub fn new() -> Self {
Self {
heal_count: 0,
last_heal: None,
backoff: MIN_BACKOFF,
}
}
pub fn should_heal(&self, mgr: &TransportManager) -> bool {
let any_blocked = mgr.active_kinds().iter().any(|&k| mgr.is_blocked(k));
if !any_blocked {
return false;
}
match self.last_heal {
Some(t) => t.elapsed() >= self.backoff,
None => true,
}
}
pub fn heal(
&mut self,
rng: &mut SeedRng,
policy: &TransportPolicy,
mgr: &TransportManager,
) -> Vec<TransportKind> {
self.heal_count += 1;
self.last_heal = Some(Instant::now());
self.backoff = (self.backoff * 2).min(MAX_BACKOFF);
let mut s = rng.seed_bytes();
s[0] ^= (self.heal_count as u8).wrapping_mul(0x9e);
s[1] ^= ((self.heal_count >> 8) & 0xff) as u8;
s[2] ^= ((self.heal_count >> 16) & 0xff) as u8;
rng.reseed(s);
let healthy = mgr.healthy_kinds();
let all = mgr.active_kinds();
let blocked: Vec<_> = all
.iter()
.filter(|k| !healthy.contains(k))
.copied()
.collect();
let mut result = policy.recommend(&healthy);
let blocked_sorted = policy.recommend(&blocked);
result.extend(blocked_sorted);
result
}
pub fn reseed_only(&mut self, rng: &mut SeedRng) {
self.heal_count += 1;
self.last_heal = Some(Instant::now());
self.backoff = (self.backoff * 2).min(MAX_BACKOFF);
let mut s = rng.seed_bytes();
s[0] ^= (self.heal_count as u8).wrapping_mul(0x9e);
s[1] ^= ((self.heal_count >> 8) & 0xff) as u8;
s[2] ^= ((self.heal_count >> 16) & 0xff) as u8;
rng.reseed(s);
}
pub fn record_success(&mut self) {
self.backoff = MIN_BACKOFF;
}
pub fn backoff(&self) -> Duration {
self.backoff
}
}
impl Default for SelfHealing {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::policy::NetworkEnvironment;
#[test]
fn should_heal_false_when_no_transports() {
let mgr = TransportManager::new();
let sh = SelfHealing::new();
assert!(!sh.should_heal(&mgr));
}
#[test]
fn heal_reseeds_and_prefers_healthy() {
let mut rng = SeedRng::new([1u8; 32]);
let before = rng.next_u64();
let policy = TransportPolicy::new(NetworkEnvironment::Wifi);
let mgr = TransportManager::new();
let mut sh = SelfHealing::new();
let order = sh.heal(&mut rng, &policy, &mgr);
assert_eq!(sh.heal_count, 1);
assert_ne!(rng.next_u64(), before);
assert!(order.is_empty());
}
#[test]
fn backoff_doubles_after_heal() {
let mut rng = SeedRng::new([2u8; 32]);
let policy = TransportPolicy::new(NetworkEnvironment::Unknown);
let mgr = TransportManager::new();
let mut sh = SelfHealing::new();
assert_eq!(sh.backoff(), MIN_BACKOFF);
sh.heal(&mut rng, &policy, &mgr);
assert_eq!(sh.backoff(), MIN_BACKOFF * 2);
sh.heal(&mut rng, &policy, &mgr);
assert_eq!(sh.backoff(), MIN_BACKOFF * 4);
}
#[test]
fn success_resets_backoff() {
let mut rng = SeedRng::new([3u8; 32]);
let policy = TransportPolicy::new(NetworkEnvironment::Unknown);
let mgr = TransportManager::new();
let mut sh = SelfHealing::new();
sh.heal(&mut rng, &policy, &mgr);
sh.heal(&mut rng, &policy, &mgr);
assert!(sh.backoff() > MIN_BACKOFF);
sh.record_success();
assert_eq!(sh.backoff(), MIN_BACKOFF);
}
}