use std::collections::{HashMap, HashSet};
use crate::ids::PeerId;
pub const DEFAULT_FAILURE_THRESHOLD: u32 = 5;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BlockReason {
Blocklisted,
NotAllowlisted,
Cooldown {
retry_ns: u64,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PeerHealth {
pub consecutive_failures: u32,
pub last_event_ns: u64,
pub down: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny(BlockReason),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LifecycleTransition {
None,
WentDown,
CameUp,
}
pub struct PeerGovernor {
blocklist: HashSet<PeerId>,
allowlist: Option<HashSet<PeerId>>,
health: HashMap<PeerId, PeerHealth>,
failure_threshold: u32,
}
impl Default for PeerGovernor {
fn default() -> Self {
Self::new()
}
}
impl PeerGovernor {
pub fn new() -> Self {
Self {
blocklist: HashSet::new(),
allowlist: None,
health: HashMap::new(),
failure_threshold: DEFAULT_FAILURE_THRESHOLD,
}
}
pub fn with_failure_threshold(mut self, threshold: u32) -> Self {
self.failure_threshold = threshold.max(1);
self
}
pub fn block(&mut self, peer: PeerId) {
self.blocklist.insert(peer);
}
pub fn unblock(&mut self, peer: PeerId) {
self.blocklist.remove(&peer);
}
pub fn set_allowlist(&mut self, allowlist: Option<HashSet<PeerId>>) {
self.allowlist = allowlist;
}
pub fn peer_health(&self, peer: PeerId) -> Option<PeerHealth> {
self.health.get(&peer).copied()
}
pub fn is_down(&self, peer: PeerId) -> bool {
self.health.get(&peer).is_some_and(|h| h.down)
}
pub fn check_inbound(&self, peer: PeerId) -> Decision {
if self.blocklist.contains(&peer) {
return Decision::Deny(BlockReason::Blocklisted);
}
if let Some(allow) = &self.allowlist {
if !allow.contains(&peer) {
return Decision::Deny(BlockReason::NotAllowlisted);
}
}
Decision::Allow
}
pub fn check_outbound(
&self,
peer: PeerId,
backoff: &super::BackoffTable,
now_ns: u64,
) -> Decision {
if self.blocklist.contains(&peer) {
return Decision::Deny(BlockReason::Blocklisted);
}
if let Some(allow) = &self.allowlist {
if !allow.contains(&peer) {
return Decision::Deny(BlockReason::NotAllowlisted);
}
}
if !backoff.should_retry(peer, now_ns) {
let retry_ns = backoff
.state(peer)
.map(|s| s.next_retry_ns)
.unwrap_or(now_ns);
return Decision::Deny(BlockReason::Cooldown { retry_ns });
}
Decision::Allow
}
pub fn record_success(&mut self, peer: PeerId, now_ns: u64) -> LifecycleTransition {
let was_down = self.health.get(&peer).map(|h| h.down).unwrap_or(false);
self.health.insert(
peer,
PeerHealth {
consecutive_failures: 0,
last_event_ns: now_ns,
down: false,
},
);
if was_down {
LifecycleTransition::CameUp
} else {
LifecycleTransition::None
}
}
pub fn record_failure(&mut self, peer: PeerId, now_ns: u64) -> LifecycleTransition {
let prev = self.health.get(&peer).copied().unwrap_or_default();
let consecutive_failures = prev.consecutive_failures.saturating_add(1);
let just_went_down = !prev.down && consecutive_failures >= self.failure_threshold;
self.health.insert(
peer,
PeerHealth {
consecutive_failures,
last_event_ns: now_ns,
down: prev.down || just_went_down,
},
);
if just_went_down {
LifecycleTransition::WentDown
} else {
LifecycleTransition::None
}
}
pub fn tracked_peers(&self) -> usize {
self.health.len()
}
pub fn blocklist(&self) -> &HashSet<PeerId> {
&self.blocklist
}
pub fn allowlist(&self) -> Option<&HashSet<PeerId>> {
self.allowlist.as_ref()
}
pub fn iter_health(&self) -> impl Iterator<Item = (PeerId, PeerHealth)> + '_ {
self.health.iter().map(|(p, h)| (*p, *h))
}
pub fn failure_threshold(&self) -> u32 {
self.failure_threshold
}
pub fn restore_health(&mut self, peer: PeerId, health: PeerHealth) {
self.health.insert(peer, health);
}
pub fn set_failure_threshold(&mut self, threshold: u32) {
self.failure_threshold = threshold.max(1);
}
}