use ipfrs_core::Cid;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
pub type PeerId = String;
#[derive(Debug, Clone)]
pub struct PeerScoringConfig {
pub latency_weight: f64,
pub bandwidth_weight: f64,
pub reliability_weight: f64,
pub ewma_alpha: f64,
pub inactivity_decay: f64,
pub min_score: f64,
pub max_failures: u32,
}
impl Default for PeerScoringConfig {
fn default() -> Self {
Self {
latency_weight: 0.3,
bandwidth_weight: 0.4,
reliability_weight: 0.3,
ewma_alpha: 0.3,
inactivity_decay: 0.01,
min_score: 0.1,
max_failures: 10,
}
}
}
#[derive(Debug, Clone)]
pub struct PeerMetrics {
pub latency_ms: f64,
pub bandwidth_bps: f64,
pub reliability: f64,
pub requests_sent: u64,
pub requests_completed: u64,
pub requests_failed: u64,
pub bytes_sent: u64,
pub bytes_recv: u64,
pub last_update: Instant,
pub last_success: Option<Instant>,
pub consecutive_failures: u32,
}
impl Default for PeerMetrics {
fn default() -> Self {
Self {
latency_ms: 100.0, bandwidth_bps: 1_000_000.0, reliability: 1.0, requests_sent: 0,
requests_completed: 0,
requests_failed: 0,
bytes_sent: 0,
bytes_recv: 0,
last_update: Instant::now(),
last_success: None,
consecutive_failures: 0,
}
}
}
impl PeerMetrics {
pub fn update_latency(&mut self, latency_ms: f64, alpha: f64) {
self.latency_ms = alpha * latency_ms + (1.0 - alpha) * self.latency_ms;
self.last_update = Instant::now();
}
pub fn update_bandwidth(&mut self, bytes: u64, duration: Duration, alpha: f64) {
if duration.as_secs_f64() > 0.0 {
let bps = bytes as f64 / duration.as_secs_f64();
self.bandwidth_bps = alpha * bps + (1.0 - alpha) * self.bandwidth_bps;
}
self.last_update = Instant::now();
}
pub fn record_success(&mut self, bytes: u64, latency: Duration, alpha: f64) {
self.requests_completed += 1;
self.bytes_recv += bytes;
self.consecutive_failures = 0;
self.last_success = Some(Instant::now());
self.last_update = Instant::now();
let total = self.requests_completed + self.requests_failed;
if total > 0 {
self.reliability = self.requests_completed as f64 / total as f64;
}
self.update_latency(latency.as_secs_f64() * 1000.0, alpha);
self.update_bandwidth(bytes, latency, alpha);
}
pub fn record_failure(&mut self, alpha: f64) {
self.requests_failed += 1;
self.consecutive_failures += 1;
self.last_update = Instant::now();
let total = self.requests_completed + self.requests_failed;
if total > 0 {
self.reliability = alpha * 0.0 + (1.0 - alpha) * self.reliability;
}
}
pub fn score(&self, config: &PeerScoringConfig) -> f64 {
let latency_score = (10.0 / self.latency_ms.max(1.0)).min(1.0);
let bandwidth_score = (self.bandwidth_bps / 10_000_000.0).min(1.0);
let reliability_score = self.reliability;
let time_since_update = self.last_update.elapsed().as_secs_f64();
let decay = (1.0 - config.inactivity_decay * time_since_update).max(0.1);
let score = (config.latency_weight * latency_score
+ config.bandwidth_weight * bandwidth_score
+ config.reliability_weight * reliability_score)
* decay;
score.clamp(0.0, 1.0)
}
pub fn debt_ratio(&self) -> f64 {
if self.bytes_sent == 0 {
return f64::INFINITY;
}
self.bytes_recv as f64 / self.bytes_sent as f64
}
}
#[derive(Debug, Clone)]
pub struct BlacklistEntry {
pub blacklisted_at: Instant,
pub reason: BlacklistReason,
pub expires_at: Option<Instant>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlacklistReason {
RepeatedFailures,
LowScore,
InvalidData,
ProtocolViolation,
Manual,
}
impl std::fmt::Display for BlacklistReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlacklistReason::RepeatedFailures => write!(f, "repeated failures"),
BlacklistReason::LowScore => write!(f, "low score"),
BlacklistReason::InvalidData => write!(f, "invalid data"),
BlacklistReason::ProtocolViolation => write!(f, "protocol violation"),
BlacklistReason::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionStrategy {
FastestFirst,
HighestBandwidth,
BestScore,
RoundRobin,
Random,
LeastLoaded,
}
#[derive(Debug, Clone)]
pub struct PeerState {
pub id: PeerId,
pub metrics: PeerMetrics,
pub has_cids: HashSet<Cid>,
pub doesnt_have_cids: HashSet<Cid>,
pub active_requests: u32,
pub max_concurrent: u32,
pub connected: bool,
}
impl PeerState {
pub fn new(id: PeerId) -> Self {
Self {
id,
metrics: PeerMetrics::default(),
has_cids: HashSet::new(),
doesnt_have_cids: HashSet::new(),
active_requests: 0,
max_concurrent: 16, connected: true,
}
}
pub fn can_accept_request(&self) -> bool {
self.connected && self.active_requests < self.max_concurrent
}
pub fn might_have(&self, cid: &Cid) -> bool {
!self.doesnt_have_cids.contains(cid)
}
pub fn has(&self, cid: &Cid) -> bool {
self.has_cids.contains(cid)
}
}
pub struct PeerManager {
peers: HashMap<PeerId, PeerState>,
blacklist: HashMap<PeerId, BlacklistEntry>,
config: PeerScoringConfig,
round_robin_idx: usize,
#[allow(dead_code)]
recent_peers: VecDeque<PeerId>,
}
impl PeerManager {
pub fn new(config: PeerScoringConfig) -> Self {
Self {
peers: HashMap::new(),
blacklist: HashMap::new(),
config,
round_robin_idx: 0,
recent_peers: VecDeque::with_capacity(100),
}
}
pub fn with_defaults() -> Self {
Self::new(PeerScoringConfig::default())
}
pub fn add_peer(&mut self, id: PeerId) {
if !self.is_blacklisted(&id) {
self.peers
.entry(id.clone())
.or_insert_with(|| PeerState::new(id));
}
}
pub fn remove_peer(&mut self, id: &PeerId) {
self.peers.remove(id);
}
pub fn get_peer(&self, id: &PeerId) -> Option<&PeerState> {
self.peers.get(id)
}
pub fn get_peer_mut(&mut self, id: &PeerId) -> Option<&mut PeerState> {
self.peers.get_mut(id)
}
pub fn record_success(&mut self, peer_id: &PeerId, bytes: u64, latency: Duration) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.metrics
.record_success(bytes, latency, self.config.ewma_alpha);
peer.active_requests = peer.active_requests.saturating_sub(1);
}
}
pub fn record_failure(&mut self, peer_id: &PeerId) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.metrics.record_failure(self.config.ewma_alpha);
peer.active_requests = peer.active_requests.saturating_sub(1);
if peer.metrics.consecutive_failures >= self.config.max_failures {
self.blacklist_peer(
peer_id.clone(),
BlacklistReason::RepeatedFailures,
Some(Duration::from_secs(3600)), );
} else if peer.metrics.score(&self.config) < self.config.min_score {
self.blacklist_peer(
peer_id.clone(),
BlacklistReason::LowScore,
Some(Duration::from_secs(1800)), );
}
}
}
pub fn record_has(&mut self, peer_id: &PeerId, cid: Cid) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.has_cids.insert(cid);
peer.doesnt_have_cids.remove(&cid);
}
}
pub fn record_doesnt_have(&mut self, peer_id: &PeerId, cid: Cid) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.doesnt_have_cids.insert(cid);
peer.has_cids.remove(&cid);
}
}
pub fn mark_request_sent(&mut self, peer_id: &PeerId) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.metrics.requests_sent += 1;
peer.active_requests += 1;
}
}
pub fn blacklist_peer(
&mut self,
peer_id: PeerId,
reason: BlacklistReason,
duration: Option<Duration>,
) {
let expires_at = duration.map(|d| Instant::now() + d);
self.blacklist.insert(
peer_id.clone(),
BlacklistEntry {
blacklisted_at: Instant::now(),
reason,
expires_at,
},
);
self.peers.remove(&peer_id);
}
pub fn unblacklist_peer(&mut self, peer_id: &PeerId) {
self.blacklist.remove(peer_id);
}
pub fn is_blacklisted(&self, peer_id: &PeerId) -> bool {
if let Some(entry) = self.blacklist.get(peer_id) {
if let Some(expires) = entry.expires_at {
if Instant::now() >= expires {
return false;
}
}
true
} else {
false
}
}
pub fn cleanup_blacklist(&mut self) {
let now = Instant::now();
self.blacklist
.retain(|_, entry| entry.expires_at.is_none_or(|exp| exp > now));
}
pub fn select_peers(
&mut self,
cid: &Cid,
count: usize,
strategy: SelectionStrategy,
) -> Vec<PeerId> {
self.cleanup_blacklist();
let available: Vec<_> = self
.peers
.values()
.filter(|p| p.can_accept_request() && p.might_have(cid))
.collect();
if available.is_empty() {
return Vec::new();
}
match strategy {
SelectionStrategy::FastestFirst => {
let mut sorted: Vec<_> = available.into_iter().collect();
sorted.sort_by(|a, b| {
a.metrics
.latency_ms
.partial_cmp(&b.metrics.latency_ms)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
.into_iter()
.take(count)
.map(|p| p.id.clone())
.collect()
}
SelectionStrategy::HighestBandwidth => {
let mut sorted: Vec<_> = available.into_iter().collect();
sorted.sort_by(|a, b| {
b.metrics
.bandwidth_bps
.partial_cmp(&a.metrics.bandwidth_bps)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
.into_iter()
.take(count)
.map(|p| p.id.clone())
.collect()
}
SelectionStrategy::BestScore => {
let mut sorted: Vec<_> = available.into_iter().collect();
sorted.sort_by(|a, b| {
let score_a = a.metrics.score(&self.config);
let score_b = b.metrics.score(&self.config);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
.into_iter()
.take(count)
.map(|p| p.id.clone())
.collect()
}
SelectionStrategy::RoundRobin => {
let mut result = Vec::with_capacity(count);
let peer_ids: Vec<_> = available.iter().map(|p| p.id.clone()).collect();
let len = peer_ids.len();
for i in 0..count.min(len) {
let idx = (self.round_robin_idx + i) % len;
result.push(peer_ids[idx].clone());
}
self.round_robin_idx = (self.round_robin_idx + count) % len.max(1);
result
}
SelectionStrategy::Random => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as usize)
.unwrap_or(0);
let mut peer_ids: Vec<_> = available.iter().map(|p| p.id.clone()).collect();
for i in (1..peer_ids.len()).rev() {
let j = (now.wrapping_add(i)) % (i + 1);
peer_ids.swap(i, j);
}
peer_ids.into_iter().take(count).collect()
}
SelectionStrategy::LeastLoaded => {
let mut sorted: Vec<_> = available.into_iter().collect();
sorted.sort_by_key(|p| p.active_requests);
sorted
.into_iter()
.take(count)
.map(|p| p.id.clone())
.collect()
}
}
}
pub fn select_providers(&mut self, cid: &Cid, count: usize) -> Vec<PeerId> {
self.cleanup_blacklist();
let mut providers: Vec<_> = self
.peers
.values()
.filter(|p| p.can_accept_request() && p.has(cid))
.collect();
providers.sort_by(|a, b| {
let score_a = a.metrics.score(&self.config);
let score_b = b.metrics.score(&self.config);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
providers
.into_iter()
.take(count)
.map(|p| p.id.clone())
.collect()
}
pub fn peer_ids(&self) -> Vec<PeerId> {
self.peers.keys().cloned().collect()
}
pub fn connected_count(&self) -> usize {
self.peers.values().filter(|p| p.connected).count()
}
pub fn blacklisted_count(&self) -> usize {
self.blacklist.len()
}
pub fn get_scores(&self) -> HashMap<PeerId, f64> {
self.peers
.iter()
.map(|(id, peer)| (id.clone(), peer.metrics.score(&self.config)))
.collect()
}
pub fn best_peer(&self) -> Option<&PeerId> {
self.peers
.iter()
.filter(|(_, p)| p.connected)
.max_by(|(_, a), (_, b)| {
let score_a = a.metrics.score(&self.config);
let score_b = b.metrics.score(&self.config);
score_a
.partial_cmp(&score_b)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(id, _)| id)
}
pub fn set_connected(&mut self, peer_id: &PeerId, connected: bool) {
if let Some(peer) = self.peers.get_mut(peer_id) {
peer.connected = connected;
}
}
pub fn stats(&self) -> PeerManagerStats {
let total_peers = self.peers.len();
let connected_peers = self.peers.values().filter(|p| p.connected).count();
let blacklisted_peers = self.blacklist.len();
let avg_score = if total_peers > 0 {
self.peers
.values()
.map(|p| p.metrics.score(&self.config))
.sum::<f64>()
/ total_peers as f64
} else {
0.0
};
let avg_latency = if total_peers > 0 {
self.peers
.values()
.map(|p| p.metrics.latency_ms)
.sum::<f64>()
/ total_peers as f64
} else {
0.0
};
let total_requests: u64 = self.peers.values().map(|p| p.metrics.requests_sent).sum();
let total_completed: u64 = self
.peers
.values()
.map(|p| p.metrics.requests_completed)
.sum();
let total_failed: u64 = self.peers.values().map(|p| p.metrics.requests_failed).sum();
PeerManagerStats {
total_peers,
connected_peers,
blacklisted_peers,
avg_score,
avg_latency_ms: avg_latency,
total_requests,
total_completed,
total_failed,
}
}
}
#[derive(Debug, Clone)]
pub struct PeerManagerStats {
pub total_peers: usize,
pub connected_peers: usize,
pub blacklisted_peers: usize,
pub avg_score: f64,
pub avg_latency_ms: f64,
pub total_requests: u64,
pub total_completed: u64,
pub total_failed: u64,
}
pub struct ConcurrentPeerManager {
inner: Arc<RwLock<PeerManager>>,
}
impl ConcurrentPeerManager {
pub fn new(config: PeerScoringConfig) -> Self {
Self {
inner: Arc::new(RwLock::new(PeerManager::new(config))),
}
}
pub fn with_defaults() -> Self {
Self::new(PeerScoringConfig::default())
}
pub fn add_peer(&self, id: PeerId) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.add_peer(id);
}
pub fn remove_peer(&self, id: &PeerId) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.remove_peer(id);
}
pub fn record_success(&self, peer_id: &PeerId, bytes: u64, latency: Duration) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.record_success(peer_id, bytes, latency);
}
pub fn record_failure(&self, peer_id: &PeerId) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.record_failure(peer_id);
}
pub fn record_has(&self, peer_id: &PeerId, cid: Cid) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.record_has(peer_id, cid);
}
pub fn record_doesnt_have(&self, peer_id: &PeerId, cid: Cid) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.record_doesnt_have(peer_id, cid);
}
pub fn mark_request_sent(&self, peer_id: &PeerId) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.mark_request_sent(peer_id);
}
pub fn blacklist_peer(
&self,
peer_id: PeerId,
reason: BlacklistReason,
duration: Option<Duration>,
) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.blacklist_peer(peer_id, reason, duration);
}
pub fn is_blacklisted(&self, peer_id: &PeerId) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.is_blacklisted(peer_id)
}
pub fn select_peers(
&self,
cid: &Cid,
count: usize,
strategy: SelectionStrategy,
) -> Vec<PeerId> {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.select_peers(cid, count, strategy)
}
pub fn select_providers(&self, cid: &Cid, count: usize) -> Vec<PeerId> {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.select_providers(cid, count)
}
pub fn stats(&self) -> PeerManagerStats {
self.inner.read().unwrap_or_else(|e| e.into_inner()).stats()
}
pub fn get_scores(&self) -> HashMap<PeerId, f64> {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.get_scores()
}
pub fn set_connected(&self, peer_id: &PeerId, connected: bool) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.set_connected(peer_id, connected);
}
pub fn clone_inner(&self) -> Arc<RwLock<PeerManager>> {
Arc::clone(&self.inner)
}
}
impl Clone for ConcurrentPeerManager {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub backoff_multiplier: f64,
pub jitter_factor: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(30),
backoff_multiplier: 2.0,
jitter_factor: 0.2,
}
}
}
pub struct RetryPolicy {
config: RetryConfig,
attempt: u32,
last_attempt: Option<Instant>,
}
impl RetryPolicy {
pub fn new(config: RetryConfig) -> Self {
Self {
config,
attempt: 0,
last_attempt: None,
}
}
pub fn can_retry(&self) -> bool {
self.attempt < self.config.max_retries
}
pub fn next_backoff(&mut self) -> Duration {
self.attempt += 1;
self.last_attempt = Some(Instant::now());
let base_backoff = self.config.initial_backoff.as_millis() as f64
* self.config.backoff_multiplier.powi(self.attempt as i32 - 1);
let capped_backoff = base_backoff.min(self.config.max_backoff.as_millis() as f64);
let jitter = if self.config.jitter_factor > 0.0 {
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
let hash = RandomState::new().hash_one(self.attempt);
let jitter_range = capped_backoff * self.config.jitter_factor;
((hash % 1000) as f64 / 1000.0) * jitter_range
} else {
0.0
};
Duration::from_millis((capped_backoff + jitter) as u64)
}
pub fn reset(&mut self) {
self.attempt = 0;
self.last_attempt = None;
}
pub fn attempt(&self) -> u32 {
self.attempt
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerConfig {
pub failure_threshold: u32,
pub timeout: Duration,
pub success_threshold: u32,
pub window_duration: Duration,
}
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
timeout: Duration::from_secs(30),
success_threshold: 2,
window_duration: Duration::from_secs(60),
}
}
}
pub struct CircuitBreaker {
config: CircuitBreakerConfig,
state: Arc<RwLock<CircuitState>>,
failure_count: Arc<RwLock<u32>>,
success_count: Arc<RwLock<u32>>,
last_failure_time: Arc<RwLock<Option<Instant>>>,
opened_at: Arc<RwLock<Option<Instant>>>,
failure_timestamps: Arc<RwLock<VecDeque<Instant>>>,
}
impl CircuitBreaker {
pub fn new(config: CircuitBreakerConfig) -> Self {
Self {
config,
state: Arc::new(RwLock::new(CircuitState::Closed)),
failure_count: Arc::new(RwLock::new(0)),
success_count: Arc::new(RwLock::new(0)),
last_failure_time: Arc::new(RwLock::new(None)),
opened_at: Arc::new(RwLock::new(None)),
failure_timestamps: Arc::new(RwLock::new(VecDeque::new())),
}
}
pub fn state(&self) -> CircuitState {
*self.state.read().unwrap_or_else(|e| e.into_inner())
}
pub fn is_request_allowed(&self) -> bool {
let mut state = self.state.write().unwrap_or_else(|e| e.into_inner());
match *state {
CircuitState::Closed => true,
CircuitState::Open => {
let opened_at = self.opened_at.read().unwrap_or_else(|e| e.into_inner());
if let Some(opened_time) = *opened_at {
if opened_time.elapsed() >= self.config.timeout {
*state = CircuitState::HalfOpen;
*self
.success_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
true
} else {
false
}
} else {
false
}
}
CircuitState::HalfOpen => true,
}
}
pub fn record_success(&self) {
let mut state = self.state.write().unwrap_or_else(|e| e.into_inner());
match *state {
CircuitState::Closed => {
*self
.failure_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
self.failure_timestamps
.write()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
CircuitState::HalfOpen => {
let mut success_count = self
.success_count
.write()
.unwrap_or_else(|e| e.into_inner());
*success_count += 1;
if *success_count >= self.config.success_threshold {
*state = CircuitState::Closed;
*self
.failure_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
*success_count = 0;
self.failure_timestamps
.write()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
}
CircuitState::Open => {}
}
}
pub fn record_failure(&self) {
let now = Instant::now();
let mut state = self.state.write().unwrap_or_else(|e| e.into_inner());
{
let mut timestamps = self
.failure_timestamps
.write()
.unwrap_or_else(|e| e.into_inner());
timestamps.push_back(now);
while let Some(&oldest) = timestamps.front() {
if oldest.elapsed() > self.config.window_duration {
timestamps.pop_front();
} else {
break;
}
}
}
*self
.last_failure_time
.write()
.unwrap_or_else(|e| e.into_inner()) = Some(now);
match *state {
CircuitState::Closed => {
let mut failure_count = self
.failure_count
.write()
.unwrap_or_else(|e| e.into_inner());
*failure_count += 1;
let window_failures = self
.failure_timestamps
.read()
.unwrap_or_else(|e| e.into_inner())
.len() as u32;
if window_failures >= self.config.failure_threshold {
*state = CircuitState::Open;
*self.opened_at.write().unwrap_or_else(|e| e.into_inner()) = Some(now);
}
}
CircuitState::HalfOpen => {
*state = CircuitState::Open;
*self.opened_at.write().unwrap_or_else(|e| e.into_inner()) = Some(now);
*self
.success_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
}
CircuitState::Open => {}
}
}
pub fn reset(&self) {
*self.state.write().unwrap_or_else(|e| e.into_inner()) = CircuitState::Closed;
*self
.failure_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
*self
.success_count
.write()
.unwrap_or_else(|e| e.into_inner()) = 0;
*self
.last_failure_time
.write()
.unwrap_or_else(|e| e.into_inner()) = None;
*self.opened_at.write().unwrap_or_else(|e| e.into_inner()) = None;
self.failure_timestamps
.write()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
pub fn stats(&self) -> CircuitBreakerStats {
CircuitBreakerStats {
state: self.state(),
failure_count: *self.failure_count.read().unwrap_or_else(|e| e.into_inner()),
success_count: *self.success_count.read().unwrap_or_else(|e| e.into_inner()),
window_failures: self
.failure_timestamps
.read()
.unwrap_or_else(|e| e.into_inner())
.len() as u32,
}
}
}
impl Default for CircuitBreaker {
fn default() -> Self {
Self::new(CircuitBreakerConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreakerStats {
pub state: CircuitState,
pub failure_count: u32,
pub success_count: u32,
pub window_failures: u32,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_cid() -> Cid {
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse()
.expect("test: parse CID from known-good string")
}
#[test]
fn test_peer_metrics_score() {
let config = PeerScoringConfig::default();
let mut metrics = PeerMetrics::default();
let initial_score = metrics.score(&config);
assert!(initial_score > 0.0);
assert!(initial_score <= 1.0);
metrics.latency_ms = 5.0;
metrics.bandwidth_bps = 50_000_000.0;
metrics.reliability = 1.0;
let good_score = metrics.score(&config);
assert!(good_score > initial_score);
metrics.latency_ms = 500.0;
metrics.bandwidth_bps = 100_000.0;
metrics.reliability = 0.5;
let poor_score = metrics.score(&config);
assert!(poor_score < good_score);
}
#[test]
fn test_peer_manager_add_remove() {
let mut manager = PeerManager::with_defaults();
manager.add_peer("peer1".to_string());
assert!(manager.get_peer(&"peer1".to_string()).is_some());
manager.remove_peer(&"peer1".to_string());
assert!(manager.get_peer(&"peer1".to_string()).is_none());
}
#[test]
fn test_peer_selection_fastest_first() {
let mut manager = PeerManager::with_defaults();
let cid = test_cid();
manager.add_peer("slow".to_string());
manager.add_peer("fast".to_string());
if let Some(peer) = manager.get_peer_mut(&"slow".to_string()) {
peer.metrics.latency_ms = 100.0;
}
if let Some(peer) = manager.get_peer_mut(&"fast".to_string()) {
peer.metrics.latency_ms = 10.0;
}
let selected = manager.select_peers(&cid, 1, SelectionStrategy::FastestFirst);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0], "fast");
}
#[test]
fn test_blacklisting() {
let mut manager = PeerManager::with_defaults();
manager.add_peer("bad_peer".to_string());
assert!(!manager.is_blacklisted(&"bad_peer".to_string()));
manager.blacklist_peer(
"bad_peer".to_string(),
BlacklistReason::Manual,
None, );
assert!(manager.is_blacklisted(&"bad_peer".to_string()));
assert!(manager.get_peer(&"bad_peer".to_string()).is_none());
}
#[test]
fn test_temporary_blacklist_expiry() {
let mut manager = PeerManager::with_defaults();
manager.add_peer("temp_bad".to_string());
manager.blacklist_peer(
"temp_bad".to_string(),
BlacklistReason::RepeatedFailures,
Some(Duration::from_millis(10)), );
assert!(manager.is_blacklisted(&"temp_bad".to_string()));
std::thread::sleep(Duration::from_millis(20));
assert!(!manager.is_blacklisted(&"temp_bad".to_string()));
}
#[test]
fn test_has_doesnt_have_tracking() {
let mut manager = PeerManager::with_defaults();
let cid = test_cid();
manager.add_peer("peer1".to_string());
manager.record_has(&"peer1".to_string(), cid);
let peer = manager
.get_peer(&"peer1".to_string())
.expect("test: peer1 was just added to manager");
assert!(peer.has(&cid));
let providers = manager.select_providers(&cid, 1);
assert_eq!(providers.len(), 1);
assert_eq!(providers[0], "peer1");
}
#[test]
fn test_concurrent_peer_manager() {
let manager = ConcurrentPeerManager::with_defaults();
manager.add_peer("peer1".to_string());
manager.record_success(&"peer1".to_string(), 1000, Duration::from_millis(10));
let stats = manager.stats();
assert_eq!(stats.total_peers, 1);
assert_eq!(stats.total_completed, 1);
}
#[test]
fn test_retry_policy() {
let config = RetryConfig::default();
let mut policy = RetryPolicy::new(config);
assert!(policy.can_retry());
assert_eq!(policy.attempt(), 0);
let backoff1 = policy.next_backoff();
assert!(backoff1.as_millis() >= 100);
let backoff2 = policy.next_backoff();
assert!(backoff2 > backoff1);
policy.reset();
assert_eq!(policy.attempt(), 0);
}
#[test]
fn test_circuit_breaker() {
let config = CircuitBreakerConfig {
failure_threshold: 3,
timeout: Duration::from_millis(100),
success_threshold: 2,
window_duration: Duration::from_secs(60),
};
let breaker = CircuitBreaker::new(config);
assert_eq!(breaker.state(), CircuitState::Closed);
assert!(breaker.is_request_allowed());
breaker.record_failure();
breaker.record_failure();
breaker.record_failure();
assert_eq!(breaker.state(), CircuitState::Open);
assert!(!breaker.is_request_allowed());
std::thread::sleep(Duration::from_millis(150));
assert!(breaker.is_request_allowed());
assert_eq!(breaker.state(), CircuitState::HalfOpen);
breaker.record_success();
breaker.record_success();
assert_eq!(breaker.state(), CircuitState::Closed);
}
#[test]
fn test_circuit_breaker_stats() {
let breaker = CircuitBreaker::default();
breaker.record_failure();
breaker.record_failure();
let stats = breaker.stats();
assert_eq!(stats.failure_count, 2);
assert_eq!(stats.window_failures, 2);
assert_eq!(stats.state, CircuitState::Closed);
}
}