use std::{
fmt,
fmt::{Display, Formatter},
time::{Duration, Instant},
};
use crate::{connectivity::peer_health::PeerHealthMetrics, utils::datetime::format_duration};
#[derive(Debug, Clone, Default)]
pub struct PeerConnectionStats {
pub last_connected_at: Option<Instant>,
pub last_connection_attempt: LastConnectionAttempt,
pub health_metrics: PeerHealthMetrics,
}
impl PeerConnectionStats {
pub fn new() -> Self {
Default::default()
}
pub fn set_connection_success(&mut self) {
self.last_connected_at = Some(Instant::now());
self.last_connection_attempt = LastConnectionAttempt::Succeeded(Instant::now());
self.health_metrics.record_success(None);
}
pub fn set_connection_success_with_latency(&mut self, latency: Duration) {
self.last_connected_at = Some(Instant::now());
self.last_connection_attempt = LastConnectionAttempt::Succeeded(Instant::now());
self.health_metrics.record_success(Some(latency));
}
pub fn set_connection_failed(&mut self) {
self.last_connection_attempt = LastConnectionAttempt::Failed {
failed_at: Instant::now(),
num_attempts: self.failed_attempts() + 1,
};
use super::config::ConnectivityConfig;
let default_config = ConnectivityConfig::default();
self.health_metrics
.record_failure(default_config.circuit_breaker_failure_threshold);
}
pub fn set_connection_failed_with_threshold(&mut self, circuit_breaker_threshold: usize) {
self.last_connection_attempt = LastConnectionAttempt::Failed {
failed_at: Instant::now(),
num_attempts: self.failed_attempts() + 1,
};
self.health_metrics.record_failure(circuit_breaker_threshold);
}
pub fn failed_attempts(&self) -> usize {
match self.last_connection_attempt {
LastConnectionAttempt::Failed { num_attempts, .. } => num_attempts,
_ => 0,
}
}
pub fn last_failed_at(&self) -> Option<Instant> {
match &self.last_connection_attempt {
LastConnectionAttempt::Failed { failed_at, .. } => Some(*failed_at),
_ => None,
}
}
pub fn should_allow_connection(&self, retry_interval: Duration) -> bool {
self.health_metrics.should_allow_connection(retry_interval)
}
pub fn success_rate(&self, window: Duration) -> f32 {
self.health_metrics.success_rate(window)
}
pub fn health_score(&self, window: Duration) -> f32 {
self.health_metrics.health_score(window)
}
pub fn try_half_open(&mut self, retry_interval: Duration) -> bool {
self.health_metrics.try_half_open(retry_interval)
}
pub fn cleanup_old_health_data(&mut self, window: Duration) {
self.health_metrics.cleanup_old_attempts(window);
}
pub fn health_metrics(&self) -> &PeerHealthMetrics {
&self.health_metrics
}
}
impl fmt::Display for PeerConnectionStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.last_failed_at() {
Some(_) => {
write!(f, "{}", self.last_connection_attempt)?;
},
None => match self.last_connected_at.as_ref() {
Some(dt) => {
write!(f, "Last connected {} ago", format_duration(dt.elapsed()))?;
},
None => {
write!(f, "{}", self.last_connection_attempt)?;
},
},
}
Ok(())
}
}
#[derive(Default, Debug, Clone, PartialOrd, PartialEq, Eq)]
pub enum LastConnectionAttempt {
#[default]
Never,
Succeeded(Instant),
Failed {
failed_at: Instant,
num_attempts: usize,
},
}
impl Display for LastConnectionAttempt {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
use LastConnectionAttempt::{Failed, Never, Succeeded};
match self {
Never => write!(f, "Connection never attempted"),
Succeeded(succeeded_at) => write!(
f,
"Connection succeeded {} ago",
format_duration(succeeded_at.elapsed())
),
Failed {
failed_at,
num_attempts,
} => write!(
f,
"Connection failed {} ago ({} attempt(s))",
format_duration(failed_at.elapsed()),
num_attempts
),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn peer_connection_stats() {
let state = PeerConnectionStats::new();
assert!(state.last_failed_at().is_none());
assert_eq!(state.failed_attempts(), 0);
let mut state = PeerConnectionStats::new();
state.set_connection_success();
assert!(state.last_failed_at().is_none());
assert_eq!(state.failed_attempts(), 0);
let mut state = PeerConnectionStats::new();
state.set_connection_failed();
state.set_connection_failed();
state.set_connection_failed();
assert!(state.last_failed_at().is_some());
assert_eq!(state.failed_attempts(), 3);
state.set_connection_success();
assert_eq!(state.failed_attempts(), 0);
assert!(state.last_failed_at().is_none());
}
}