use crate::config::Config;
use crate::errors::*;
use crate::p2p::proto::PeerAddr;
use chrono::{DateTime, TimeDelta, Utc};
use colored::{Color, Colorize};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio::time;
const EXPIRE_ERROR_THRESHOLD: usize = 30;
const EXPIRE_UNLESS_ADVERTISED_SINCE: Duration = Duration::from_secs(3600 * 24 * 14);
const PEERDB_EXPIRE_INTERVAL: Duration = Duration::from_secs(60);
const PEERDB_SAMPLE_SIZE: usize = 5;
#[derive(Debug)]
pub enum Req {
AddAdvertisedPeers(Vec<PeerAddr>),
Sample {
max_success_age: Option<Duration>,
tx: mpsc::Sender<Vec<PeerAddr>>,
},
Metric {
metric: MetricType,
value: MetricValue,
addr: PeerAddr,
},
Write,
}
#[derive(Debug, Clone)]
pub struct Client {
tx: mpsc::Sender<Req>,
}
impl Client {
pub fn new() -> (Self, mpsc::Receiver<Req>) {
let (tx, rx) = mpsc::channel(1024);
(Self { tx }, rx)
}
async fn request<T>(&self, req: Req, mut rx: mpsc::Receiver<T>) -> Result<T> {
self.tx
.send(req)
.await
.map_err(|_| anyhow!("PeerDb server disconnected"))?;
let ret = rx.recv().await.context("PeerDb server disconnected")?;
Ok(ret)
}
fn lossy_send(&self, req: Req) {
if let Err(TrySendError::Full(req)) = self.tx.try_send(req) {
warn!("Discarding peerdb request because backlog is full: {req:?}");
}
}
pub fn add_advertised_peers(&self, addrs: Vec<PeerAddr>) {
self.lossy_send(Req::AddAdvertisedPeers(addrs));
}
#[inline]
pub fn successful(&self, metric: MetricType, addr: PeerAddr) {
self.lossy_send(Req::Metric {
metric,
value: MetricValue::Successful,
addr,
})
}
#[inline]
pub fn error(&self, metric: MetricType, addr: PeerAddr) {
self.lossy_send(Req::Metric {
metric,
value: MetricValue::Error,
addr,
})
}
pub async fn sample(&self, max_success_age: Option<Duration>) -> Result<Vec<PeerAddr>> {
let (tx, rx) = mpsc::channel(1);
self.request(
Req::Sample {
max_success_age,
tx,
},
rx,
)
.await
}
pub fn write(&self) {
self.lossy_send(Req::Write);
}
}
pub fn format_time_opt(time: Option<DateTime<Utc>>) -> Cow<'static, str> {
if let Some(time) = time {
Cow::Owned(format_time(time))
} else {
Cow::Borrowed("-")
}
}
pub fn format_time(time: DateTime<Utc>) -> String {
time.format("%FT%T").to_string()
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Metric {
pub last_attempt: Option<DateTime<Utc>>,
pub errors_since: usize,
pub last_success: Option<DateTime<Utc>>,
}
impl Metric {
pub fn metric(&mut self, value: MetricValue) {
match value {
MetricValue::Successful => self.successful(),
MetricValue::Error => self.error(),
}
}
pub fn successful(&mut self) {
self.errors_since = 0;
let now = Utc::now();
self.last_success = Some(now);
self.last_attempt = Some(now);
}
pub fn error(&mut self) {
self.errors_since += 1;
self.last_attempt = Some(Utc::now());
}
pub fn format_stats(&self) -> String {
format!(
"last_attempt={:<19} errors_since={} last_success={}",
format_time_opt(self.last_attempt).yellow(),
self.errors_since
.to_string()
.color(if self.errors_since == 0 {
Color::Green
} else {
Color::Red
}),
format_time_opt(self.last_success).yellow(),
)
}
}
#[derive(Debug)]
pub enum MetricType {
Connect,
Handshake,
}
#[derive(Debug)]
pub enum MetricValue {
Successful,
Error,
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct PeerStats {
#[serde(default)]
pub connect: Metric,
#[serde(default)]
pub handshake: Metric,
pub last_advertised: Option<DateTime<Utc>>,
}
impl PeerStats {
pub fn metric(&mut self, metric: MetricType, value: MetricValue) {
match metric {
MetricType::Connect => self.connect.metric(value),
MetricType::Handshake => self.handshake.metric(value),
}
}
pub fn expired(&self, now: DateTime<Utc>) -> bool {
let Some(last_advertised) = self.last_advertised else {
return false;
};
if last_advertised + EXPIRE_UNLESS_ADVERTISED_SINCE > now {
return false;
}
if self.connect.errors_since > EXPIRE_ERROR_THRESHOLD {
return true;
}
if self.handshake.errors_since > EXPIRE_ERROR_THRESHOLD {
return true;
}
false
}
}
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
struct Data {
pub peers: BTreeMap<PeerAddr, PeerStats>,
}
pub struct PeerDb {
data: Data,
path: PathBuf,
new_path: PathBuf,
}
impl PeerDb {
pub fn add_peer(&mut self, addr: PeerAddr) -> (&mut PeerStats, bool) {
trace!("Adding address to peerdb: {addr:?}");
match self.data.peers.entry(addr) {
entry @ Entry::Vacant(_) => (entry.or_default(), true),
entry @ Entry::Occupied(_) => (entry.or_default(), false),
}
}
pub fn add_advertised_peers(&mut self, addrs: &[PeerAddr]) {
let now = Utc::now();
for addr in addrs {
let (peer, _new) = self.add_peer(addr.clone());
peer.last_advertised = Some(now);
}
}
pub fn peers(&self) -> &BTreeMap<PeerAddr, PeerStats> {
&self.data.peers
}
pub fn sample(&self, max_success_age: Option<Duration>) -> Vec<PeerAddr> {
let now = Utc::now();
let delta = max_success_age
.map(|max_success_age| TimeDelta::from_std(max_success_age).unwrap_or(TimeDelta::MAX));
let mut peers = self
.data
.peers
.iter()
.flat_map(|(addr, stats)| {
if let Some(delta) = delta {
let last_success = stats.handshake.last_success?;
if now.signed_duration_since(last_success) > delta {
return None;
}
}
Some(addr)
})
.collect::<Vec<_>>();
fastrand::shuffle(&mut peers);
peers
.into_iter()
.take(PEERDB_SAMPLE_SIZE)
.cloned()
.collect()
}
pub fn expire_old_peers(&mut self, now: DateTime<Utc>) -> bool {
let before = self.data.peers.len();
self.data.peers.retain(|_, peer| !peer.expired(now));
let after = self.data.peers.len();
if after != before {
info!("Removed {} expired peers", before.saturating_sub(after));
true
} else {
false
}
}
pub async fn read(config: &Config) -> Result<Self> {
let mut db = Self {
data: Data::default(),
path: config.peerdb_path()?,
new_path: config.peerdb_new_path()?,
};
let path = &db.path;
debug!("Reading peerdb from file: {path:?}");
let Ok(buf) = fs::read(&path).await else {
debug!("Failed to read peerdb file, using empty");
return Ok(db);
};
let Ok(data) = serde_json::from_slice(&buf) else {
debug!("Failed to parse peerdb file, using empty");
return Ok(db);
};
db.data = data;
Ok(db)
}
pub async fn write(&self) -> Result<()> {
let buf = serde_json::to_string(&self.data).context("Failed to serialize peerdb")?;
let new_path = &self.new_path;
debug!("Writing peerdb file to disk: {new_path:?}");
fs::write(&new_path, &buf)
.await
.with_context(|| anyhow!("Failed to write peerdb file at {new_path:?}"))?;
let path = &self.path;
debug!("Moving peerdb file to final location: {path:?}");
fs::rename(&new_path, &path)
.await
.with_context(|| anyhow!("Failed to rename peerdb {new_path:?} to {path:?}"))?;
Ok(())
}
}
pub async fn spawn(mut peerdb: PeerDb, mut rx: mpsc::Receiver<Req>) -> Result<Infallible> {
let mut interval = time::interval(PEERDB_EXPIRE_INTERVAL);
loop {
tokio::select! {
req = rx.recv() => {
let Some(req) = req else { break };
match req {
Req::AddAdvertisedPeers(addrs) => {
peerdb.add_advertised_peers(&addrs);
peerdb.write().await?;
}
Req::Sample { max_success_age, tx } => {
let sample = peerdb.sample(max_success_age);
tx.send(sample).await.ok();
}
Req::Metric { metric, value, addr } => {
let (peer, _new) = peerdb.add_peer(addr);
peer.metric(metric, value);
}
Req::Write => peerdb.write().await?,
}
}
_ = interval.tick() => {
if peerdb.expire_old_peers(Utc::now()) {
peerdb.write().await?;
}
}
}
}
bail!("PeerDb channel has been closed");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_db() {
let data = r#"
{"peers":{"[2001:db8::]:16169":{}}}
"#;
let data = serde_json::from_str::<Data>(data).unwrap();
assert_eq!(
data,
Data {
peers: [("[2001:db8::]:16169".parse().unwrap(), PeerStats::default())]
.into_iter()
.collect(),
}
);
}
#[test]
fn test_expired_peers() {
fn datetime(s: &str) -> DateTime<Utc> {
s.parse::<DateTime<Utc>>().unwrap()
}
let now = datetime("2025-02-17T01:00:00Z");
assert!(!PeerStats {
connect: Metric::default(),
handshake: Metric::default(),
last_advertised: None,
}
.expired(now));
assert!(PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: None,
},
handshake: Metric::default(),
last_advertised: Some(datetime("2025-01-01T13:37:00Z")),
}
.expired(now));
assert!(PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 0,
last_success: Some(datetime("2025-02-17T00:45:00Z")),
},
handshake: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: Some(datetime("2025-01-14T00:45:00Z")),
},
last_advertised: Some(datetime("2025-01-01T13:37:00Z")),
}
.expired(now));
assert!(!PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: None,
},
handshake: Metric::default(),
last_advertised: Some(datetime("2025-02-14T13:37:00Z")),
}
.expired(now));
assert!(!PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 0,
last_success: Some(datetime("2025-02-17T00:45:00Z")),
},
handshake: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: Some(datetime("2025-01-14T00:45:00Z")),
},
last_advertised: Some(datetime("2025-02-14T13:37:00Z")),
}
.expired(now));
assert!(!PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: None,
},
handshake: Metric::default(),
last_advertised: None,
}
.expired(now));
assert!(!PeerStats {
connect: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 0,
last_success: Some(datetime("2025-02-17T00:45:00Z")),
},
handshake: Metric {
last_attempt: Some(datetime("2025-02-17T00:45:00Z")),
errors_since: 500,
last_success: Some(datetime("2025-01-14T00:45:00Z")),
},
last_advertised: None,
}
.expired(now));
}
}