use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::error::TrySendError;
use tracing::debug;
use crate::{Error, Message, Result};
const AGE_BONUS_DIVISOR: f64 = 300.0;
const MAX_AGE_BONUS: f64 = 0.2;
const CONSECUTIVE_FAILURE_PENALTY: f64 = 0.1;
const MAX_CONSECUTIVE_PENALTY: f64 = 0.5;
const MAX_CONSECUTIVE_FAILURES: u64 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerState {
Connecting,
Connected,
Stale,
Disconnected,
}
#[derive(Debug, Clone)]
pub struct PeerInfo {
pub addr: SocketAddr,
pub state: PeerState,
pub last_seen: Instant,
pub connected_at: Instant,
pub messages_received: u64,
pub messages_sent: u64,
pub message_failures: u64,
pub consecutive_failures: u64,
}
impl PeerInfo {
pub fn new(addr: SocketAddr) -> Self {
let now = Instant::now();
Self {
addr,
state: PeerState::Connecting,
last_seen: now,
connected_at: now,
messages_received: 0,
messages_sent: 0,
message_failures: 0,
consecutive_failures: 0,
}
}
pub fn mark_connected(&mut self) {
self.state = PeerState::Connected;
}
pub fn mark_stale(&mut self) {
self.state = PeerState::Stale;
}
pub fn mark_disconnected(&mut self) {
self.state = PeerState::Disconnected;
}
pub fn update_last_seen(&mut self) {
self.last_seen = Instant::now();
if matches!(self.state, PeerState::Connecting | PeerState::Stale) {
self.state = PeerState::Connected;
}
}
pub fn is_stale(&self, timeout: Duration) -> bool {
self.last_seen.elapsed() > timeout
}
pub fn increment_received(&mut self) {
self.messages_received = self.messages_received.saturating_add(1);
self.update_last_seen();
}
pub fn increment_sent(&mut self) {
self.messages_sent = self.messages_sent.saturating_add(1);
self.consecutive_failures = 0;
}
pub fn record_failure(&mut self) {
self.message_failures = self.message_failures.saturating_add(1);
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
}
pub fn health_score(&self) -> f64 {
let total_attempts = self.messages_sent.saturating_add(self.message_failures);
if total_attempts == 0 {
return 1.0;
}
let sent = f64::from(u32::try_from(self.messages_sent).unwrap_or(u32::MAX));
let total = f64::from(u32::try_from(total_attempts).unwrap_or(u32::MAX));
let success_rate = sent / total;
let age_seconds =
f64::from(u32::try_from(self.connected_at.elapsed().as_secs()).unwrap_or(u32::MAX));
let age_bonus = (age_seconds / AGE_BONUS_DIVISOR).min(MAX_AGE_BONUS);
let consecutive_penalty =
(f64::from(u32::try_from(self.consecutive_failures).unwrap_or(u32::MAX))
* CONSECUTIVE_FAILURE_PENALTY)
.min(MAX_CONSECUTIVE_PENALTY);
(success_rate + age_bonus - consecutive_penalty).clamp(0.0, 1.0)
}
pub fn should_disconnect(&self) -> bool {
self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES
}
}
#[derive(Debug)]
pub struct Peer {
pub info: PeerInfo,
sender: Sender<Message>,
}
impl Peer {
pub fn new(addr: SocketAddr, sender: Sender<Message>) -> Self {
Self {
info: PeerInfo::new(addr),
sender,
}
}
pub fn send(&mut self, message: Message) -> Result<()> {
match self.sender.try_send(message) {
Ok(()) => {
self.info.increment_sent();
Ok(())
}
Err(TrySendError::Full(_)) => {
debug!(
"Write channel full for {}, dropping message",
self.info.addr
);
Ok(())
}
Err(TrySendError::Closed(_)) => {
self.info.record_failure();
Err(Error::Channel("peer write channel closed".to_string()))
}
}
}
pub fn addr(&self) -> SocketAddr {
self.info.addr
}
pub fn state(&self) -> PeerState {
self.info.state
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
use crate::Payload;
fn message(seq: u64) -> Message {
let addr = "127.0.0.1:8000".parse().unwrap();
Message::new(
addr,
seq,
Payload::Application(Bytes::from(format!("m{seq}"))),
)
}
#[test]
fn update_last_seen_drives_state_machine() {
let addr = "127.0.0.1:8000".parse().unwrap();
let mut info = PeerInfo::new(addr);
assert_eq!(info.state, PeerState::Connecting);
info.update_last_seen();
assert_eq!(info.state, PeerState::Connected);
info.mark_stale();
assert_eq!(info.state, PeerState::Stale);
info.update_last_seen();
assert_eq!(info.state, PeerState::Connected);
info.mark_disconnected();
info.update_last_seen();
assert_eq!(info.state, PeerState::Disconnected);
}
#[test]
fn peer_info_saturating_counters() {
let addr = "127.0.0.1:8000".parse().unwrap();
let mut info = PeerInfo::new(addr);
info.messages_received = u64::MAX;
info.increment_received();
assert_eq!(info.messages_received, u64::MAX);
info.messages_sent = u64::MAX;
info.increment_sent();
assert_eq!(info.messages_sent, u64::MAX);
}
#[test]
fn peer_send_success() {
let addr = "127.0.0.1:8000".parse().unwrap();
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
let mut peer = Peer::new(addr, tx);
let msg = message(1);
assert_eq!(peer.info.messages_sent, 0);
assert!(peer.send(msg.clone()).is_ok());
assert_eq!(peer.info.messages_sent, 1);
let received = rx.try_recv().expect("message was queued");
assert_eq!(received.id, msg.id);
}
#[test]
fn peer_send_failure_channel_closed() {
let addr = "127.0.0.1:8000".parse().unwrap();
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(16);
drop(rx);
let mut peer = Peer::new(addr, tx);
let result = peer.send(message(1));
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::Channel(_)));
}
#[test]
fn peer_multiple_sends() {
let addr = "127.0.0.1:8000".parse().unwrap();
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
let mut peer = Peer::new(addr, tx);
for i in 1..=5 {
assert!(peer.send(message(i)).is_ok());
assert_eq!(peer.info.messages_sent, i);
let received = rx.try_recv().unwrap();
assert_eq!(received.id.sequence, i);
}
}
#[test]
fn send_drops_newest_when_channel_full() {
let addr = "127.0.0.1:8000".parse().unwrap();
let (tx, _rx) = tokio::sync::mpsc::channel::<Message>(1);
let mut peer = Peer::new(addr, tx);
assert!(peer.send(message(1)).is_ok());
assert_eq!(peer.info.messages_sent, 1);
assert!(peer.send(message(2)).is_ok());
assert_eq!(
peer.info.messages_sent, 1,
"a dropped frame is not counted as sent"
);
assert_eq!(
peer.info.consecutive_failures, 0,
"backpressure is not a peer failure"
);
}
}