use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tracing::{debug, trace};
const PEER_TTL: Duration = Duration::from_secs(30 * 60);
const MAX_PEERS_PER_INFO_HASH: usize = 50;
struct PeerEntry {
addr: SocketAddr,
last_seen: Instant,
}
pub struct DhtPeerStorage {
inner: Mutex<HashMap<[u8; 20], Vec<PeerEntry>>>,
}
impl Default for DhtPeerStorage {
fn default() -> Self {
Self::new()
}
}
impl DhtPeerStorage {
pub fn new() -> Self {
Self {
inner: Mutex::new(HashMap::new()),
}
}
pub fn add_peer(&self, info_hash: [u8; 20], addr: SocketAddr) {
self.add_peer_inner(info_hash, addr, Instant::now());
}
pub fn get_peers(&self, info_hash: &[u8; 20]) -> Vec<SocketAddr> {
let mut map = self.inner.lock().expect("DhtPeerStorage mutex poisoned");
let Some(entries) = map.get_mut(info_hash) else {
return Vec::new();
};
let now = Instant::now();
let expired_count = entries
.iter()
.filter(|e| now.duration_since(e.last_seen) >= PEER_TTL)
.count();
if expired_count > 0 && expired_count * 2 >= entries.len() {
entries.retain(|e| now.duration_since(e.last_seen) < PEER_TTL);
trace!(
expired_count,
"opportunistically purged expired peers during get_peers"
);
}
entries
.iter()
.filter(|e| now.duration_since(e.last_seen) < PEER_TTL)
.map(|e| e.addr)
.collect()
}
pub fn cleanup_expired(&self) {
let mut map = self.inner.lock().expect("DhtPeerStorage mutex poisoned");
let now = Instant::now();
let mut total_removed = 0usize;
map.retain(|_, entries| {
let before = entries.len();
entries.retain(|e| now.duration_since(e.last_seen) < PEER_TTL);
let removed = before - entries.len();
total_removed += removed;
!entries.is_empty()
});
if total_removed > 0 {
debug!(total_removed, "DhtPeerStorage cleanup complete");
}
}
pub fn total_peer_count(&self) -> usize {
let map = self.inner.lock().expect("DhtPeerStorage mutex poisoned");
map.values().map(Vec::len).sum()
}
fn add_peer_inner(&self, info_hash: [u8; 20], addr: SocketAddr, last_seen: Instant) {
let mut map = self.inner.lock().expect("DhtPeerStorage mutex poisoned");
let entries = map.entry(info_hash).or_default();
if let Some(entry) = entries.iter_mut().find(|e| e.addr == addr) {
entry.last_seen = last_seen;
trace!("refreshed existing DHT peer last_seen");
return;
}
entries.push(PeerEntry { addr, last_seen });
if entries.len() > MAX_PEERS_PER_INFO_HASH {
let oldest_idx = entries
.iter()
.enumerate()
.min_by_key(|(_, e)| e.last_seen)
.map(|(i, _)| i);
if let Some(idx) = oldest_idx {
entries.remove(idx);
debug!(
limit = MAX_PEERS_PER_INFO_HASH,
"evicted oldest DHT peer to enforce per-info_hash cap"
);
}
}
}
#[cfg(test)]
fn add_peer_with_timestamp(&self, info_hash: [u8; 20], addr: SocketAddr, last_seen: Instant) {
self.add_peer_inner(info_hash, addr, last_seen);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_get_peer() {
let storage = DhtPeerStorage::new();
let info_hash = [0x11u8; 20];
let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
storage.add_peer(info_hash, addr);
let peers = storage.get_peers(&info_hash);
assert_eq!(peers, vec![addr]);
}
#[test]
fn test_get_peers_empty() {
let storage = DhtPeerStorage::new();
let info_hash = [0x22u8; 20];
let peers = storage.get_peers(&info_hash);
assert!(peers.is_empty(), "unknown info_hash should yield no peers");
}
#[test]
fn test_add_peer_updates_existing() {
let storage = DhtPeerStorage::new();
let info_hash = [0x33u8; 20];
let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap();
storage.add_peer(info_hash, addr);
storage.add_peer(info_hash, addr);
let peers = storage.get_peers(&info_hash);
assert_eq!(
peers.len(),
1,
"duplicate add should not create a new entry"
);
assert_eq!(peers, vec![addr]);
assert_eq!(storage.total_peer_count(), 1);
}
#[test]
fn test_peer_expiry() {
let storage = DhtPeerStorage::new();
let info_hash = [0xBBu8; 20];
let addr: SocketAddr = "127.0.0.1:6000".parse().unwrap();
let expired_ts = Instant::now() - Duration::from_secs(60 * 31);
storage.add_peer_with_timestamp(info_hash, addr, expired_ts);
let peers = storage.get_peers(&info_hash);
assert!(peers.is_empty(), "peer older than TTL must not be returned");
}
#[test]
fn test_max_peers_limit() {
let storage = DhtPeerStorage::new();
let info_hash = [0xAAu8; 20];
let first_addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
let old_ts = Instant::now() - Duration::from_secs(60);
storage.add_peer_with_timestamp(info_hash, first_addr, old_ts);
for port in 5001u16..=(5000 + MAX_PEERS_PER_INFO_HASH as u16) {
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
storage.add_peer(info_hash, addr);
}
let peers = storage.get_peers(&info_hash);
assert_eq!(
peers.len(),
MAX_PEERS_PER_INFO_HASH,
"peer count must be capped at MAX_PEERS_PER_INFO_HASH"
);
assert!(
!peers.contains(&first_addr),
"oldest peer should have been evicted"
);
}
#[test]
fn test_cleanup_expired() {
let storage = DhtPeerStorage::new();
let info_hash = [0xCCu8; 20];
let expired_addr: SocketAddr = "127.0.0.1:7000".parse().unwrap();
let fresh_addr: SocketAddr = "127.0.0.1:7001".parse().unwrap();
let expired_ts = Instant::now() - Duration::from_secs(60 * 31);
storage.add_peer_with_timestamp(info_hash, expired_addr, expired_ts);
storage.add_peer(info_hash, fresh_addr);
assert_eq!(
storage.total_peer_count(),
2,
"before cleanup both peers are stored"
);
storage.cleanup_expired();
assert_eq!(
storage.total_peer_count(),
1,
"after cleanup only the fresh peer remains"
);
let peers = storage.get_peers(&info_hash);
assert_eq!(peers, vec![fresh_addr]);
}
#[test]
fn test_cleanup_removes_empty_info_hash() {
let storage = DhtPeerStorage::new();
let info_hash = [0xDDu8; 20];
let addr: SocketAddr = "127.0.0.1:7100".parse().unwrap();
let expired_ts = Instant::now() - Duration::from_secs(60 * 31);
storage.add_peer_with_timestamp(info_hash, addr, expired_ts);
assert_eq!(storage.total_peer_count(), 1);
storage.cleanup_expired();
assert_eq!(storage.total_peer_count(), 0);
assert!(storage.get_peers(&info_hash).is_empty());
}
#[test]
fn test_multiple_info_hashes() {
let storage = DhtPeerStorage::new();
let hash_a = [0x01u8; 20];
let hash_b = [0x02u8; 20];
let addr_a: SocketAddr = "127.0.0.1:8000".parse().unwrap();
let addr_b: SocketAddr = "127.0.0.1:8001".parse().unwrap();
storage.add_peer(hash_a, addr_a);
storage.add_peer(hash_b, addr_b);
let peers_a = storage.get_peers(&hash_a);
let peers_b = storage.get_peers(&hash_b);
assert_eq!(peers_a, vec![addr_a], "hash_a should only contain addr_a");
assert_eq!(peers_b, vec![addr_b], "hash_b should only contain addr_b");
assert_eq!(storage.total_peer_count(), 2);
}
#[test]
fn test_default_impl() {
let storage = DhtPeerStorage::default();
assert_eq!(storage.total_peer_count(), 0);
}
}