use dig_nat::{PeerId, TraversalKind};
use crate::registry::PeerEntry;
use crate::scoring::{PeerClass, RelayModel, SaturationModel};
use crate::types::Provenance;
#[derive(Debug, Clone, serde::Serialize)]
pub struct PeerSnapshot {
pub peer_id: String,
pub provenance: Provenance,
pub connection_class: Option<String>,
pub connected: bool,
pub banned: bool,
pub throughput_bps: Option<f64>,
pub rtt_ms: Option<f64>,
pub reliability: Option<f64>,
pub throughput_volatility: f64,
pub samples: u64,
pub hard_failures: u64,
pub in_flight: u32,
}
impl PeerSnapshot {
pub fn of(entry: &PeerEntry) -> Self {
let q = &entry.quality;
PeerSnapshot {
peer_id: entry.peer_id.to_hex(),
provenance: entry.provenance,
connection_class: entry.connection_class.map(traversal_kind_name),
connected: entry.connected,
banned: entry.banned,
throughput_bps: q.throughput.value(),
rtt_ms: q.rtt.value(),
reliability: q.reliability.rate(),
throughput_volatility: q.throughput.relative_volatility(),
samples: q.samples,
hard_failures: q.reliability.hard_failures(),
in_flight: q.in_flight,
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SelectorSnapshot {
pub registry_size: usize,
pub measured_peers: usize,
pub connected_peers: usize,
pub saturation_direct: u32,
pub saturation_relayed: u32,
pub saturation_unknown: u32,
pub relayed_penalty: f64,
pub last_selected_len: usize,
pub dispatched_len: usize,
}
impl SelectorSnapshot {
pub(crate) fn build<'a>(
entries: impl Iterator<Item = &'a PeerEntry>,
saturation: &SaturationModel,
relay: &RelayModel,
last_selected_len: usize,
dispatched_len: usize,
) -> Self {
let mut registry_size = 0;
let mut measured_peers = 0;
let mut connected_peers = 0;
for e in entries {
registry_size += 1;
if !e.quality.is_cold() {
measured_peers += 1;
}
if e.connected {
connected_peers += 1;
}
}
SelectorSnapshot {
registry_size,
measured_peers,
connected_peers,
saturation_direct: saturation.saturation_point(PeerClass::DirectPath),
saturation_relayed: saturation.saturation_point(PeerClass::RelayedPath),
saturation_unknown: saturation.saturation_point(PeerClass::Unknown),
relayed_penalty: relay.penalty(),
last_selected_len,
dispatched_len,
}
}
}
fn traversal_kind_name(k: TraversalKind) -> String {
match k {
TraversalKind::Direct => "direct",
TraversalKind::Upnp => "upnp",
TraversalKind::NatPmp => "natpmp",
TraversalKind::Pcp => "pcp",
TraversalKind::HolePunch => "holepunch",
TraversalKind::Relayed => "relayed",
}
.to_string()
}
pub fn peer_id_hex(peer: &PeerId) -> String {
peer.to_hex()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::Registry;
use crate::types::{Candidate, Provenance};
use dig_dht::CandidateAddr;
fn pid(b: u8) -> PeerId {
PeerId::from_bytes([b; 32])
}
#[test]
fn peer_snapshot_reflects_learned_state() {
let mut e = PeerEntry::cold(pid(1), Provenance::Dht, 0);
e.connection_class = Some(TraversalKind::Relayed);
e.quality.observe_throughput(500.0);
e.quality.observe_result(true, false);
e.quality.bump_samples();
let snap = PeerSnapshot::of(&e);
assert_eq!(snap.peer_id, pid(1).to_hex());
assert_eq!(snap.connection_class.as_deref(), Some("relayed"));
assert_eq!(snap.throughput_bps, Some(500.0));
assert_eq!(snap.samples, 1);
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"throughput_bps\":500"));
}
#[test]
fn selector_snapshot_counts_and_learned_values() {
let mut r = Registry::new(100);
r.mark_connected(pid(1), Provenance::Gossip, 0);
r.upsert_candidate(
&Candidate::new(pid(2), vec![CandidateAddr::direct("h", 1)]),
Provenance::Dht,
0,
);
r.get_mut(&pid(1))
.unwrap()
.quality
.observe_throughput(100.0);
r.get_mut(&pid(1)).unwrap().quality.bump_samples();
let sat = SaturationModel::default();
let relay = RelayModel::default();
let snap = SelectorSnapshot::build(r.iter(), &sat, &relay, 0, 0);
assert_eq!(snap.registry_size, 2);
assert_eq!(snap.measured_peers, 1);
assert_eq!(snap.connected_peers, 1);
assert!(snap.saturation_direct >= 1);
assert!(snap.relayed_penalty > 0.0 && snap.relayed_penalty <= 1.0);
}
#[test]
fn peer_id_hex_helper() {
assert_eq!(peer_id_hex(&pid(0xAB)), pid(0xAB).to_hex());
}
}