use cid::Cid;
use dashmap::DashMap;
use libp2p::PeerId;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct ContentRoutingConfig {
pub max_providers_per_cid: usize,
pub provider_record_ttl: Duration,
pub cache_entry_ttl: Duration,
pub max_cache_size: usize,
pub dht_query_parallelism: usize,
pub dht_query_timeout: Duration,
pub enable_cache_aware_routing: bool,
}
impl Default for ContentRoutingConfig {
fn default() -> Self {
Self {
max_providers_per_cid: 20,
provider_record_ttl: Duration::from_secs(24 * 3600), cache_entry_ttl: Duration::from_secs(3600), max_cache_size: 10_000,
dht_query_parallelism: 3,
dht_query_timeout: Duration::from_secs(30),
enable_cache_aware_routing: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ProviderRecord {
pub peer_id: PeerId,
pub added_at: Instant,
pub ttl: Duration,
pub score: f64,
pub successful_retrievals: u64,
pub failed_retrievals: u64,
}
impl ProviderRecord {
fn new(peer_id: PeerId, ttl: Duration) -> Self {
Self {
peer_id,
added_at: Instant::now(),
ttl,
score: 1.0,
successful_retrievals: 0,
failed_retrievals: 0,
}
}
fn is_expired(&self) -> bool {
self.added_at.elapsed() > self.ttl
}
fn update_score(&mut self) {
let total = self.successful_retrievals + self.failed_retrievals;
if total > 0 {
self.score = self.successful_retrievals as f64 / total as f64;
}
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
#[allow(dead_code)]
cid: Cid,
providers: Vec<PeerId>,
cached_at: Instant,
hits: u64,
}
impl CacheEntry {
fn is_expired(&self, ttl: Duration) -> bool {
self.cached_at.elapsed() > ttl
}
}
#[derive(Debug, Default, Clone)]
pub struct ContentRoutingStats {
pub dht_queries: u64,
pub dht_queries_success: u64,
pub dht_queries_failed: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub content_advertisements: u64,
pub provider_records_added: u64,
pub provider_records_removed: u64,
}
pub struct ContentRouter {
config: ContentRoutingConfig,
providers: Arc<DashMap<Cid, Vec<ProviderRecord>>>,
cache: Arc<RwLock<HashMap<Cid, CacheEntry>>>,
stats: Arc<RwLock<ContentRoutingStats>>,
provided_content: Arc<DashMap<Cid, Instant>>,
}
impl ContentRouter {
pub fn new() -> Self {
Self::with_config(ContentRoutingConfig::default())
}
pub fn with_config(config: ContentRoutingConfig) -> Self {
Self {
config,
providers: Arc::new(DashMap::new()),
cache: Arc::new(RwLock::new(HashMap::new())),
stats: Arc::new(RwLock::new(ContentRoutingStats::default())),
provided_content: Arc::new(DashMap::new()),
}
}
pub async fn advertise_content(&self, cid: Cid, peer_id: PeerId) {
self.provided_content.insert(cid, Instant::now());
self.add_provider(cid, peer_id, self.config.provider_record_ttl)
.await;
let mut stats = self.stats.write().await;
stats.content_advertisements += 1;
}
async fn add_provider(&self, cid: Cid, peer_id: PeerId, ttl: Duration) {
let record = ProviderRecord::new(peer_id, ttl);
self.providers
.entry(cid)
.and_modify(|providers| {
providers.retain(|p| !p.is_expired());
if let Some(existing) = providers.iter_mut().find(|p| p.peer_id == peer_id) {
existing.added_at = Instant::now();
existing.ttl = ttl;
} else if providers.len() < self.config.max_providers_per_cid {
providers.push(record.clone());
} else {
if let Some(worst) = providers.iter_mut().min_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(std::cmp::Ordering::Equal)
}) {
*worst = record.clone();
}
}
})
.or_insert_with(|| vec![record]);
self.cache.write().await.remove(&cid);
let mut stats = self.stats.write().await;
stats.provider_records_added += 1;
}
pub async fn find_providers(&self, cid: &Cid) -> Vec<PeerId> {
if self.config.enable_cache_aware_routing {
let mut cache = self.cache.write().await;
if let Some(entry) = cache.get_mut(cid) {
if !entry.is_expired(self.config.cache_entry_ttl) {
entry.hits += 1;
let mut stats = self.stats.write().await;
stats.cache_hits += 1;
return entry.providers.clone();
} else {
cache.remove(cid);
}
}
let mut stats = self.stats.write().await;
stats.cache_misses += 1;
}
let providers = self.query_dht_providers(cid).await;
if self.config.enable_cache_aware_routing && !providers.is_empty() {
let mut cache = self.cache.write().await;
if cache.len() >= self.config.max_cache_size {
if let Some(lru_cid) = cache
.iter()
.min_by_key(|(_, entry)| entry.hits)
.map(|(cid, _)| *cid)
{
cache.remove(&lru_cid);
}
}
cache.insert(
*cid,
CacheEntry {
cid: *cid,
providers: providers.clone(),
cached_at: Instant::now(),
hits: 1,
},
);
}
providers
}
async fn query_dht_providers(&self, cid: &Cid) -> Vec<PeerId> {
let mut stats = self.stats.write().await;
stats.dht_queries += 1;
if let Some(providers) = self.providers.get(cid) {
let valid_providers: Vec<_> = providers.iter().filter(|p| !p.is_expired()).collect();
if !valid_providers.is_empty() {
stats.dht_queries_success += 1;
let mut sorted_providers = valid_providers.clone();
sorted_providers.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
return sorted_providers.iter().map(|p| p.peer_id).collect();
}
}
stats.dht_queries_failed += 1;
Vec::new()
}
pub async fn record_success(&self, cid: &Cid, peer_id: &PeerId) {
if let Some(mut providers) = self.providers.get_mut(cid) {
if let Some(provider) = providers.iter_mut().find(|p| p.peer_id == *peer_id) {
provider.successful_retrievals += 1;
provider.update_score();
}
}
}
pub async fn record_failure(&self, cid: &Cid, peer_id: &PeerId) {
if let Some(mut providers) = self.providers.get_mut(cid) {
if let Some(provider) = providers.iter_mut().find(|p| p.peer_id == *peer_id) {
provider.failed_retrievals += 1;
provider.update_score();
}
}
}
pub async fn cleanup_expired(&self) {
let mut removed_count = 0;
let cids_to_remove: Vec<Cid> = self
.providers
.iter()
.filter_map(|entry| {
let cid = *entry.key();
let mut providers = entry.value().clone();
providers.retain(|p| !p.is_expired());
if providers.is_empty() {
Some(cid)
} else {
removed_count += entry.value().len() - providers.len();
None
}
})
.collect();
for cid in cids_to_remove {
removed_count += self.providers.get(&cid).map(|p| p.len()).unwrap_or(0);
self.providers.remove(&cid);
}
if self.config.enable_cache_aware_routing {
let mut cache = self.cache.write().await;
cache.retain(|_, entry| !entry.is_expired(self.config.cache_entry_ttl));
}
let mut stats = self.stats.write().await;
stats.provider_records_removed += removed_count as u64;
}
pub async fn stats(&self) -> ContentRoutingStats {
self.stats.read().await.clone()
}
pub async fn get_cache_aware_route(
&self,
cid: &Cid,
_local_cache: &HashSet<Cid>,
) -> Option<PeerId> {
let providers = self.find_providers(cid).await;
if providers.is_empty() {
return None;
}
providers.first().copied()
}
pub fn provider_count(&self, cid: &Cid) -> usize {
self.providers
.get(cid)
.map(|providers| providers.iter().filter(|p| !p.is_expired()).count())
.unwrap_or(0)
}
pub fn list_provided_content(&self) -> Vec<Cid> {
self.provided_content
.iter()
.map(|entry| *entry.key())
.collect()
}
}
impl Default for ContentRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_cid(data: &[u8]) -> Cid {
use ipfrs_core::CidBuilder;
CidBuilder::new()
.build(data)
.expect("test: build CID from data")
}
#[tokio::test]
async fn test_advertise_and_find_content() {
let router = ContentRouter::new();
let cid = create_test_cid(b"test content");
let peer_id = PeerId::random();
router.advertise_content(cid, peer_id).await;
let providers = router.find_providers(&cid).await;
assert_eq!(providers.len(), 1);
assert_eq!(providers[0], peer_id);
}
#[tokio::test]
async fn test_multiple_providers() {
let router = ContentRouter::new();
let cid = create_test_cid(b"shared content");
let peer1 = PeerId::random();
let peer2 = PeerId::random();
let peer3 = PeerId::random();
router.advertise_content(cid, peer1).await;
router.advertise_content(cid, peer2).await;
router.advertise_content(cid, peer3).await;
let providers = router.find_providers(&cid).await;
assert_eq!(providers.len(), 3);
assert!(providers.contains(&peer1));
assert!(providers.contains(&peer2));
assert!(providers.contains(&peer3));
}
#[tokio::test]
async fn test_provider_scoring() {
let router = ContentRouter::new();
let cid = create_test_cid(b"scored content");
let peer = PeerId::random();
router.advertise_content(cid, peer).await;
router.record_success(&cid, &peer).await;
router.record_success(&cid, &peer).await;
router.record_success(&cid, &peer).await;
router.record_failure(&cid, &peer).await;
if let Some(providers) = router.providers.get(&cid) {
let provider = providers
.iter()
.find(|p| p.peer_id == peer)
.expect("test: find provider matching peer");
assert_eq!(provider.successful_retrievals, 3);
assert_eq!(provider.failed_retrievals, 1);
assert!((provider.score - 0.75).abs() < 0.01); };
}
#[tokio::test]
async fn test_cache_functionality() {
let config = ContentRoutingConfig {
enable_cache_aware_routing: true,
..Default::default()
};
let router = ContentRouter::with_config(config);
let cid = create_test_cid(b"cached content");
let peer = PeerId::random();
router.advertise_content(cid, peer).await;
let providers1 = router.find_providers(&cid).await;
let stats1 = router.stats().await;
assert_eq!(stats1.cache_misses, 1);
assert_eq!(stats1.cache_hits, 0);
let providers2 = router.find_providers(&cid).await;
let stats2 = router.stats().await;
assert_eq!(stats2.cache_misses, 1);
assert_eq!(stats2.cache_hits, 1);
assert_eq!(providers1, providers2);
}
#[tokio::test]
async fn test_provider_expiration() {
let config = ContentRoutingConfig {
provider_record_ttl: Duration::from_millis(100),
cache_entry_ttl: Duration::from_millis(50), enable_cache_aware_routing: false, ..Default::default()
};
let router = ContentRouter::with_config(config);
let cid = create_test_cid(b"expiring content");
let peer = PeerId::random();
router.advertise_content(cid, peer).await;
let providers = router.find_providers(&cid).await;
assert_eq!(providers.len(), 1);
tokio::time::sleep(Duration::from_millis(150)).await;
router.cleanup_expired().await;
let providers = router.find_providers(&cid).await;
assert_eq!(providers.len(), 0);
}
#[tokio::test]
async fn test_max_providers_limit() {
let config = ContentRoutingConfig {
max_providers_per_cid: 3,
..Default::default()
};
let router = ContentRouter::with_config(config);
let cid = create_test_cid(b"popular content");
for _ in 0..5 {
let peer = PeerId::random();
router.advertise_content(cid, peer).await;
}
let providers = router.find_providers(&cid).await;
assert_eq!(providers.len(), 3);
}
#[tokio::test]
async fn test_stats_tracking() {
let router = ContentRouter::new();
let cid = create_test_cid(b"stats content");
let peer = PeerId::random();
router.advertise_content(cid, peer).await;
router.find_providers(&cid).await;
let stats = router.stats().await;
assert_eq!(stats.content_advertisements, 1);
assert_eq!(stats.provider_records_added, 1);
assert!(stats.dht_queries > 0);
}
#[tokio::test]
async fn test_list_provided_content() {
let router = ContentRouter::new();
let peer = PeerId::random();
let cid1 = create_test_cid(b"content 1");
let cid2 = create_test_cid(b"content 2");
router.advertise_content(cid1, peer).await;
router.advertise_content(cid2, peer).await;
let provided = router.list_provided_content();
assert_eq!(provided.len(), 2);
assert!(provided.contains(&cid1));
assert!(provided.contains(&cid2));
}
#[tokio::test]
async fn test_provider_count() {
let router = ContentRouter::new();
let cid = create_test_cid(b"counted content");
assert_eq!(router.provider_count(&cid), 0);
let peer1 = PeerId::random();
router.advertise_content(cid, peer1).await;
assert_eq!(router.provider_count(&cid), 1);
let peer2 = PeerId::random();
router.advertise_content(cid, peer2).await;
assert_eq!(router.provider_count(&cid), 2);
}
}