use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
const BACKOFF_BASE_MS: u64 = 2_000;
const BACKOFF_MAX_MS: u64 = 300_000; const CONNECTING_STUCK_MS: u64 = 30_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Connecting,
Connected,
Backoff,
}
#[derive(Debug, Clone, Copy)]
struct PeerDial {
phase: Phase,
attempts: u32,
next_ms: u64,
since_ms: u64,
}
impl Default for PeerDial {
fn default() -> Self {
PeerDial {
phase: Phase::Idle,
attempts: 0,
next_ms: 0,
since_ms: 0,
}
}
}
pub(crate) struct Supervisor {
peers: Mutex<HashMap<String, PeerDial>>,
}
impl Supervisor {
pub(crate) fn new() -> Self {
Supervisor {
peers: Mutex::new(HashMap::new()),
}
}
pub(crate) fn reconcile(&self, connected: &HashSet<String>) -> Vec<String> {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
let mut newly = Vec::new();
for id in connected {
let entry = map.entry(id.clone()).or_default();
if entry.phase != Phase::Connected {
newly.push(id.clone());
}
entry.phase = Phase::Connected;
entry.attempts = 0;
}
for (id, entry) in map.iter_mut() {
if entry.phase == Phase::Connected && !connected.contains(id) {
entry.phase = Phase::Idle;
}
}
newly
}
pub(crate) fn eligible(&self, node_id: &str, now_ms: u64) -> bool {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
let entry = map.entry(node_id.to_string()).or_default();
match entry.phase {
Phase::Idle => true,
Phase::Connected => false,
Phase::Backoff => now_ms >= entry.next_ms,
Phase::Connecting => now_ms.saturating_sub(entry.since_ms) >= CONNECTING_STUCK_MS,
}
}
pub(crate) fn mark_connecting(&self, node_id: &str, now_ms: u64) {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
let entry = map.entry(node_id.to_string()).or_default();
entry.phase = Phase::Connecting;
entry.since_ms = now_ms;
}
pub(crate) fn mark_connected(&self, node_id: &str) {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
let entry = map.entry(node_id.to_string()).or_default();
entry.phase = Phase::Connected;
entry.attempts = 0;
}
pub(crate) fn mark_failed(&self, node_id: &str, now_ms: u64) {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
let entry = map.entry(node_id.to_string()).or_default();
entry.attempts = entry.attempts.saturating_add(1);
let backoff = backoff_ms(entry.attempts, node_id);
entry.phase = Phase::Backoff;
entry.next_ms = now_ms.saturating_add(backoff);
}
pub(crate) fn reset_backoff_all(&self) {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
for entry in map.values_mut() {
if entry.phase == Phase::Backoff {
entry.phase = Phase::Idle;
entry.attempts = 0;
}
}
}
pub(crate) fn retain(&self, keep: &HashSet<String>) {
let mut map = self.peers.lock().unwrap_or_else(|e| e.into_inner());
map.retain(|id, _| keep.contains(id));
}
}
fn backoff_ms(attempts: u32, node_id: &str) -> u64 {
let shift = attempts.saturating_sub(1).min(8); let base = BACKOFF_BASE_MS.saturating_mul(1u64 << shift);
let capped = base.min(BACKOFF_MAX_MS);
let mut h = std::collections::hash_map::DefaultHasher::new();
node_id.hash(&mut h);
let jitter = h.finish() % (capped / 5 + 1);
capped.saturating_add(jitter)
}
#[cfg(test)]
mod tests {
use super::*;
fn set(ids: &[&str]) -> HashSet<String> {
ids.iter().map(|s| s.to_string()).collect()
}
#[test]
fn idle_peer_is_eligible() {
let s = Supervisor::new();
assert!(s.eligible("aa", 0), "unseen peer starts Idle → eligible");
}
#[test]
fn connecting_blocks_redial_until_stuck() {
let s = Supervisor::new();
s.mark_connecting("aa", 1_000);
assert!(
!s.eligible("aa", 1_000),
"in-flight dial blocks a second dial"
);
assert!(
!s.eligible("aa", 1_000 + CONNECTING_STUCK_MS - 1),
"still blocked just before the stuck threshold"
);
assert!(
s.eligible("aa", 1_000 + CONNECTING_STUCK_MS),
"a stuck Connecting becomes eligible again"
);
}
#[test]
fn connected_peer_not_eligible() {
let s = Supervisor::new();
s.mark_connected("aa");
assert!(!s.eligible("aa", 10_000));
}
#[test]
fn backoff_grows_and_caps() {
let mut prev = 0;
for attempts in 1..=12u32 {
let d = backoff_ms(attempts, "node-x");
assert!(d >= prev, "backoff is non-decreasing: {d} >= {prev}");
assert!(
d <= BACKOFF_MAX_MS + BACKOFF_MAX_MS / 5 + 1,
"delay within cap+jitter: {d}"
);
prev = d.min(BACKOFF_MAX_MS);
}
}
#[test]
fn jitter_decorrelates_peers() {
let a = backoff_ms(5, "alpha");
let b = backoff_ms(5, "bravo");
assert_ne!(a, b);
}
#[test]
fn failed_then_eligible_after_delay() {
let s = Supervisor::new();
s.mark_failed("aa", 1_000);
assert!(
!s.eligible("aa", 1_000),
"not eligible immediately after failure"
);
assert!(s.eligible("aa", 1_000 + BACKOFF_MAX_MS + BACKOFF_MAX_MS));
}
#[test]
fn reconcile_reports_newly_connected_once() {
let s = Supervisor::new();
let first = s.reconcile(&set(&["aa", "bb"]));
assert_eq!(first.len(), 2, "both newly connected on first sighting");
let second = s.reconcile(&set(&["aa", "bb"]));
assert!(
second.is_empty(),
"already-connected peers are not re-reported"
);
}
#[test]
fn reconcile_demotes_dropped_peer_to_eligible() {
let s = Supervisor::new();
s.reconcile(&set(&["aa"]));
assert!(!s.eligible("aa", 5_000), "connected → not eligible");
s.reconcile(&set(&[])); assert!(
s.eligible("aa", 6_000),
"dropped peer becomes eligible to re-dial"
);
}
#[test]
fn success_resets_backoff() {
let s = Supervisor::new();
s.mark_failed("aa", 0);
s.mark_failed("aa", 0); s.mark_connected("aa");
s.reconcile(&set(&[])); s.mark_failed("aa", 0); let first_delay = backoff_ms(1, "aa");
let two_delay = backoff_ms(2, "aa");
assert!(first_delay < two_delay, "attempts reset after a success");
}
#[test]
fn reset_backoff_makes_failed_peers_eligible() {
let s = Supervisor::new();
s.mark_failed("aa", 1_000);
s.mark_failed("bb", 1_000);
assert!(!s.eligible("aa", 1_000), "backing off before reset");
s.reset_backoff_all();
assert!(s.eligible("aa", 1_000), "eligible immediately after reset");
assert!(s.eligible("bb", 1_000));
}
#[test]
fn reset_backoff_leaves_connecting_and_connected_alone() {
let s = Supervisor::new();
s.mark_connecting("aa", 1_000);
s.mark_connected("bb");
s.reset_backoff_all();
assert!(
!s.eligible("aa", 1_000),
"in-flight dial not disturbed by reset"
);
assert!(
!s.eligible("bb", 1_000),
"connected peer not disturbed by reset"
);
}
#[test]
fn retain_prunes_unknown_peers() {
let s = Supervisor::new();
s.mark_failed("aa", 0);
s.mark_failed("bb", 0);
s.retain(&set(&["aa"]));
assert!(s.eligible("bb", 0), "pruned peer resets to default");
assert!(!s.eligible("aa", 0), "kept peer retains its Backoff");
}
}