1use cid::Cid;
10use dashmap::DashMap;
11use ipfrs_core::error::{Error, Result};
12use libp2p::PeerId;
13use parking_lot::RwLock;
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::sync::mpsc;
18use tracing::{debug, info};
19
20const DEFAULT_PROVIDER_TTL: Duration = Duration::from_secs(24 * 60 * 60);
22
23const DEFAULT_QUERY_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
25
26#[derive(Debug, Clone)]
28pub struct DhtConfig {
29 pub provider_ttl: Duration,
31 pub query_cache_ttl: Duration,
33 pub enable_provider_refresh: bool,
35 pub provider_refresh_interval: Duration,
37 pub max_cached_queries: usize,
39}
40
41impl Default for DhtConfig {
42 fn default() -> Self {
43 Self {
44 provider_ttl: DEFAULT_PROVIDER_TTL,
45 query_cache_ttl: DEFAULT_QUERY_CACHE_TTL,
46 enable_provider_refresh: true,
47 provider_refresh_interval: Duration::from_secs(12 * 60 * 60), max_cached_queries: 10_000,
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55struct CachedQuery {
56 peers: Vec<PeerId>,
58 cached_at: Instant,
60 hit_count: usize,
62}
63
64#[derive(Debug, Clone)]
66struct ProviderRecord {
67 cid: Cid,
69 last_announced: Instant,
71}
72
73pub struct DhtManager {
75 config: DhtConfig,
76 query_cache: Arc<DashMap<String, CachedQuery>>,
78 peer_cache: Arc<DashMap<PeerId, Instant>>,
80 provider_records: Arc<RwLock<HashMap<String, ProviderRecord>>>,
82 stats: Arc<RwLock<DhtStats>>,
84 refresh_handle: Option<tokio::task::JoinHandle<()>>,
86 cmd_tx: Option<mpsc::Sender<DhtCommand>>,
88}
89
90#[derive(Debug, Clone, Default, serde::Serialize)]
92pub struct DhtStats {
93 pub total_queries: u64,
95 pub cache_hits: u64,
97 pub cache_misses: u64,
99 pub provider_refreshes: u64,
101 pub active_providers: usize,
103 pub cached_queries: usize,
105 pub cached_peers: usize,
107 pub successful_queries: u64,
109 pub failed_queries: u64,
111}
112
113#[derive(Debug, Clone, serde::Serialize)]
115pub struct DhtHealth {
116 pub health_score: f64,
118 pub query_success_rate: f64,
120 pub cache_hit_rate: f64,
122 pub peer_count: usize,
124 pub cached_query_count: usize,
126 pub provider_count: usize,
128 pub status: DhtHealthStatus,
130}
131
132#[derive(Debug, Clone, PartialEq, serde::Serialize)]
134pub enum DhtHealthStatus {
135 Healthy,
137 Degraded,
139 Unhealthy,
141 Unknown,
143}
144
145pub(crate) enum DhtCommand {
147 TrackProvider { cid: Cid },
149 StopTracking { cid: String },
151 #[allow(dead_code)]
153 RefreshProviders { response_tx: mpsc::Sender<Vec<Cid>> },
154 Shutdown,
156}
157
158impl DhtManager {
159 pub fn new(config: DhtConfig) -> Self {
161 let manager = Self {
162 config,
163 query_cache: Arc::new(DashMap::new()),
164 peer_cache: Arc::new(DashMap::new()),
165 provider_records: Arc::new(RwLock::new(HashMap::new())),
166 stats: Arc::new(RwLock::new(DhtStats::default())),
167 refresh_handle: None,
168 cmd_tx: None,
169 };
170
171 info!(
172 "DHT Manager initialized (provider_ttl={:?}, query_cache_ttl={:?})",
173 manager.config.provider_ttl, manager.config.query_cache_ttl
174 );
175
176 manager
177 }
178
179 pub fn start_provider_refresh(&mut self) {
181 if !self.config.enable_provider_refresh {
182 info!("Provider refresh disabled");
183 return;
184 }
185
186 let (cmd_tx, mut cmd_rx) = mpsc::channel::<DhtCommand>(100);
187 let provider_records = Arc::clone(&self.provider_records);
188 let stats = Arc::clone(&self.stats);
189 let refresh_interval = self.config.provider_refresh_interval;
190
191 let handle = tokio::spawn(async move {
192 info!(
193 "Starting provider refresh task (interval={:?})",
194 refresh_interval
195 );
196
197 let mut interval = tokio::time::interval(refresh_interval);
198
199 loop {
200 tokio::select! {
201 _ = interval.tick() => {
202 let now = Instant::now();
204 let mut refresh_needed = Vec::new();
205
206 {
207 let records = provider_records.read();
208 for (key, record) in records.iter() {
209 if now.duration_since(record.last_announced) >= refresh_interval {
210 refresh_needed.push((key.clone(), record.cid));
211 }
212 }
213 }
214
215 if !refresh_needed.is_empty() {
216 info!("Refreshing {} provider records", refresh_needed.len());
217 stats.write().provider_refreshes += refresh_needed.len() as u64;
218
219 let mut records = provider_records.write();
221 for (key, _cid) in refresh_needed {
222 if let Some(record) = records.get_mut(&key) {
223 record.last_announced = now;
224 }
225 }
226 }
227 }
228 Some(cmd) = cmd_rx.recv() => {
229 match cmd {
230 DhtCommand::TrackProvider { cid } => {
231 let key = cid.to_string();
232 let mut records = provider_records.write();
233 records.insert(key.clone(), ProviderRecord {
234 cid,
235 last_announced: Instant::now(),
236 });
237 debug!("Tracking provider record: {}", key);
238 stats.write().active_providers = records.len();
239 }
240 DhtCommand::StopTracking { cid } => {
241 let mut records = provider_records.write();
242 records.remove(&cid);
243 debug!("Stopped tracking provider: {}", cid);
244 stats.write().active_providers = records.len();
245 }
246 DhtCommand::RefreshProviders { response_tx } => {
247 let cids: Vec<Cid> = {
248 let records = provider_records.read();
249 records.values().map(|r| r.cid).collect()
250 };
251 let _ = response_tx.send(cids).await;
252 }
253 DhtCommand::Shutdown => {
254 info!("Shutting down provider refresh task");
255 break;
256 }
257 }
258 }
259 }
260 }
261 });
262
263 self.refresh_handle = Some(handle);
264 self.cmd_tx = Some(cmd_tx);
265 }
266
267 pub async fn track_provider(&self, cid: Cid) -> Result<()> {
269 if let Some(cmd_tx) = &self.cmd_tx {
270 cmd_tx
271 .send(DhtCommand::TrackProvider { cid })
272 .await
273 .map_err(|e| Error::Network(format!("Failed to track provider: {}", e)))?;
274 }
275 Ok(())
276 }
277
278 pub async fn stop_tracking(&self, cid: &Cid) -> Result<()> {
280 if let Some(cmd_tx) = &self.cmd_tx {
281 cmd_tx
282 .send(DhtCommand::StopTracking {
283 cid: cid.to_string(),
284 })
285 .await
286 .map_err(|e| Error::Network(format!("Failed to stop tracking: {}", e)))?;
287 }
288 Ok(())
289 }
290
291 pub fn cache_query_result(&self, cid: &Cid, peers: Vec<PeerId>) {
293 let key = cid.to_string();
294
295 if self.query_cache.len() >= self.config.max_cached_queries {
297 let now = Instant::now();
299 let mut to_remove = Vec::new();
300
301 for entry in self.query_cache.iter() {
302 if now.duration_since(entry.value().cached_at) > self.config.query_cache_ttl * 2 {
303 to_remove.push(entry.key().clone());
304 }
305 }
306
307 for key in to_remove {
308 self.query_cache.remove(&key);
309 }
310 }
311
312 self.query_cache.insert(
313 key.clone(),
314 CachedQuery {
315 peers,
316 cached_at: Instant::now(),
317 hit_count: 0,
318 },
319 );
320
321 debug!("Cached query result for {}", key);
322 self.stats.write().cached_queries = self.query_cache.len();
323 }
324
325 pub fn get_cached_query(&self, cid: &Cid) -> Option<Vec<PeerId>> {
327 let key = cid.to_string();
328 let mut stats = self.stats.write();
329 stats.total_queries += 1;
330
331 if let Some(mut cached) = self.query_cache.get_mut(&key) {
332 let age = Instant::now().duration_since(cached.cached_at);
333
334 if age < self.config.query_cache_ttl {
335 cached.hit_count += 1;
336 stats.cache_hits += 1;
337 debug!(
338 "Cache hit for {} (age={:?}, hits={})",
339 key, age, cached.hit_count
340 );
341 return Some(cached.peers.clone());
342 } else {
343 debug!("Cache entry expired for {} (age={:?})", key, age);
344 drop(cached);
345 self.query_cache.remove(&key);
346 }
347 }
348
349 stats.cache_misses += 1;
350 None
351 }
352
353 pub fn cache_peer(&self, peer_id: PeerId) {
355 self.peer_cache.insert(peer_id, Instant::now());
356 self.stats.write().cached_peers = self.peer_cache.len();
357 }
358
359 pub fn is_peer_cached(&self, peer_id: &PeerId) -> bool {
361 self.peer_cache.contains_key(peer_id)
362 }
363
364 pub fn get_cached_peers(&self) -> Vec<PeerId> {
366 self.peer_cache.iter().map(|entry| *entry.key()).collect()
367 }
368
369 pub fn cleanup_cache(&self) {
371 let now = Instant::now();
372 let mut removed_queries = 0;
373 let mut removed_peers = 0;
374
375 let to_remove: Vec<String> = self
377 .query_cache
378 .iter()
379 .filter(|entry| {
380 now.duration_since(entry.value().cached_at) > self.config.query_cache_ttl
381 })
382 .map(|entry| entry.key().clone())
383 .collect();
384
385 for key in to_remove {
386 self.query_cache.remove(&key);
387 removed_queries += 1;
388 }
389
390 let peer_ttl = Duration::from_secs(3600);
392 let to_remove: Vec<PeerId> = self
393 .peer_cache
394 .iter()
395 .filter(|entry| now.duration_since(*entry.value()) > peer_ttl)
396 .map(|entry| *entry.key())
397 .collect();
398
399 for peer_id in to_remove {
400 self.peer_cache.remove(&peer_id);
401 removed_peers += 1;
402 }
403
404 if removed_queries > 0 || removed_peers > 0 {
405 debug!(
406 "Cache cleanup: removed {} queries, {} peers",
407 removed_queries, removed_peers
408 );
409 }
410
411 let mut stats = self.stats.write();
412 stats.cached_queries = self.query_cache.len();
413 stats.cached_peers = self.peer_cache.len();
414 }
415
416 pub fn get_stats(&self) -> DhtStats {
418 self.stats.read().clone()
419 }
420
421 pub fn record_query_success(&self) {
423 self.stats.write().successful_queries += 1;
424 }
425
426 pub fn record_query_failure(&self) {
428 self.stats.write().failed_queries += 1;
429 }
430
431 pub fn get_health(&self) -> DhtHealth {
433 let stats = self.stats.read();
434
435 let total_tracked_queries = stats.successful_queries + stats.failed_queries;
437 let query_success_rate = if total_tracked_queries > 0 {
438 stats.successful_queries as f64 / total_tracked_queries as f64
439 } else {
440 1.0 };
442
443 let total_cache_queries = stats.cache_hits + stats.cache_misses;
445 let cache_hit_rate = if total_cache_queries > 0 {
446 stats.cache_hits as f64 / total_cache_queries as f64
447 } else {
448 0.0
449 };
450
451 let health_score = if total_tracked_queries > 10 {
453 let query_weight = 0.6;
455 let cache_weight = 0.2;
456 let peer_weight = 0.2;
457
458 let peer_score = if stats.cached_peers > 0 { 1.0 } else { 0.0 };
459
460 query_success_rate * query_weight
461 + cache_hit_rate * cache_weight
462 + peer_score * peer_weight
463 } else {
464 1.0 };
466
467 let status = if total_tracked_queries < 10 {
469 DhtHealthStatus::Unknown
470 } else if health_score >= 0.8 {
471 DhtHealthStatus::Healthy
472 } else if health_score >= 0.5 {
473 DhtHealthStatus::Degraded
474 } else {
475 DhtHealthStatus::Unhealthy
476 };
477
478 DhtHealth {
479 health_score,
480 query_success_rate,
481 cache_hit_rate,
482 peer_count: stats.cached_peers,
483 cached_query_count: stats.cached_queries,
484 provider_count: stats.active_providers,
485 status,
486 }
487 }
488
489 pub fn is_healthy(&self) -> bool {
491 let health = self.get_health();
492 matches!(
493 health.status,
494 DhtHealthStatus::Healthy | DhtHealthStatus::Unknown
495 )
496 }
497
498 pub async fn shutdown(&mut self) {
500 if let Some(tx) = self.cmd_tx.take() {
501 let _ = tx.send(DhtCommand::Shutdown).await;
502 }
503
504 if let Some(handle) = self.refresh_handle.take() {
505 handle.abort();
506 }
507
508 info!("DHT Manager shut down");
509 }
510}
511
512impl Drop for DhtManager {
513 fn drop(&mut self) {
514 if let Some(handle) = self.refresh_handle.take() {
515 handle.abort();
516 }
517 }
518}
519
520#[derive(Debug, Default)]
522pub struct ReannounceStats {
523 pub total_provided: usize,
525 pub due_count: usize,
527 pub avg_age_secs: f64,
529}
530
531pub struct ProviderReannouncer {
537 provided_cids: std::collections::HashMap<String, Instant>,
539 reannounce_interval: Duration,
541 max_per_cycle: usize,
543}
544
545impl ProviderReannouncer {
546 pub fn new(reannounce_interval: Duration) -> Self {
548 Self {
549 provided_cids: std::collections::HashMap::new(),
550 reannounce_interval,
551 max_per_cycle: 500,
552 }
553 }
554
555 pub fn with_max_per_cycle(reannounce_interval: Duration, max_per_cycle: usize) -> Self {
557 Self {
558 provided_cids: std::collections::HashMap::new(),
559 reannounce_interval,
560 max_per_cycle,
561 }
562 }
563
564 pub fn record_provide(&mut self, cid: &str) {
567 self.provided_cids.insert(cid.to_string(), Instant::now());
568 }
569
570 pub fn due_for_reannouncement(&self) -> Vec<String> {
575 let now = Instant::now();
576 let mut due: Vec<String> = self
577 .provided_cids
578 .iter()
579 .filter(|(_, last)| now.duration_since(**last) >= self.reannounce_interval)
580 .map(|(cid, _)| cid.clone())
581 .collect();
582
583 due.sort_unstable();
585 due.truncate(self.max_per_cycle);
586 due
587 }
588
589 pub fn mark_reannounced(&mut self, cids: &[String]) {
591 let now = Instant::now();
592 for cid in cids {
593 if let Some(entry) = self.provided_cids.get_mut(cid) {
594 *entry = now;
595 }
596 }
597 }
598
599 pub fn remove(&mut self, cid: &str) {
601 self.provided_cids.remove(cid);
602 }
603
604 pub fn count(&self) -> usize {
606 self.provided_cids.len()
607 }
608
609 pub fn stats(&self) -> ReannounceStats {
611 let now = Instant::now();
612 let total_provided = self.provided_cids.len();
613
614 if total_provided == 0 {
615 return ReannounceStats {
616 total_provided: 0,
617 due_count: 0,
618 avg_age_secs: 0.0,
619 };
620 }
621
622 let mut due_count = 0usize;
623 let mut age_sum_secs = 0.0f64;
624
625 for last in self.provided_cids.values() {
626 let age = now.duration_since(*last).as_secs_f64();
627 age_sum_secs += age;
628 if now.duration_since(*last) >= self.reannounce_interval {
629 due_count += 1;
630 }
631 }
632
633 ReannounceStats {
634 total_provided,
635 due_count,
636 avg_age_secs: age_sum_secs / total_provided as f64,
637 }
638 }
639}
640
641impl DhtManager {
642 pub fn record_provide(&self, cid: &str) {
647 let _ = cid; }
657
658 pub fn get_due_for_reannouncement(&self) -> Vec<String> {
664 Vec::new()
665 }
666
667 pub fn mark_reannounced(&self, _cids: &[String]) {
671 }
673}
674
675#[cfg(test)]
676mod reannounce_tests {
677 use super::*;
678
679 #[test]
680 fn test_record_and_due_for_reannouncement() {
681 let mut r = ProviderReannouncer::new(Duration::ZERO);
683 r.record_provide("cid-aaa");
684 r.record_provide("cid-bbb");
685
686 let due = r.due_for_reannouncement();
687 assert_eq!(due.len(), 2);
688 assert!(due.contains(&"cid-aaa".to_string()));
689 assert!(due.contains(&"cid-bbb".to_string()));
690 }
691
692 #[test]
693 fn test_not_due_with_large_interval() {
694 let mut r = ProviderReannouncer::new(Duration::from_secs(86_400));
696 r.record_provide("cid-fresh");
697
698 let due = r.due_for_reannouncement();
699 assert!(due.is_empty(), "should not be due with a 24-hour interval");
700 }
701
702 #[test]
703 fn test_mark_reannounced_resets_timer() {
704 let mut r = ProviderReannouncer::new(Duration::ZERO);
705 r.record_provide("cid-x");
706
707 assert!(!r.due_for_reannouncement().is_empty());
709
710 r.reannounce_interval = Duration::from_secs(86_400);
712 let cids = vec!["cid-x".to_string()];
713 r.mark_reannounced(&cids);
714
715 assert!(r.due_for_reannouncement().is_empty());
717 }
718
719 #[test]
720 fn test_remove_cid() {
721 let mut r = ProviderReannouncer::new(Duration::ZERO);
722 r.record_provide("cid-del");
723 assert_eq!(r.count(), 1);
724
725 r.remove("cid-del");
726 assert_eq!(r.count(), 0);
727
728 let due = r.due_for_reannouncement();
729 assert!(due.is_empty());
730 }
731
732 #[test]
733 fn test_stats() {
734 let mut r = ProviderReannouncer::new(Duration::ZERO);
735 let s = r.stats();
737 assert_eq!(s.total_provided, 0);
738 assert_eq!(s.due_count, 0);
739 assert_eq!(s.avg_age_secs, 0.0);
740
741 r.record_provide("cid-1");
742 r.record_provide("cid-2");
743
744 let s = r.stats();
745 assert_eq!(s.total_provided, 2);
746 assert_eq!(s.due_count, 2); assert!(s.avg_age_secs >= 0.0);
748 }
749
750 #[test]
751 fn test_zero_interval_everything_due() {
752 let mut r = ProviderReannouncer::new(Duration::ZERO);
753 for i in 0..10 {
754 r.record_provide(&format!("cid-{}", i));
755 }
756 let due = r.due_for_reannouncement();
757 assert_eq!(due.len(), 10, "all CIDs must be due with zero interval");
758 }
759
760 #[test]
761 fn test_max_per_cycle_cap() {
762 let mut r = ProviderReannouncer::with_max_per_cycle(Duration::ZERO, 3);
763 for i in 0..10 {
764 r.record_provide(&format!("cid-{:03}", i));
765 }
766 let due = r.due_for_reannouncement();
767 assert_eq!(due.len(), 3, "max_per_cycle cap must be respected");
768 }
769
770 #[test]
771 fn test_record_provide_idempotent() {
772 let mut r = ProviderReannouncer::new(Duration::from_secs(3600));
773 r.record_provide("cid-idem");
774 r.record_provide("cid-idem"); assert_eq!(r.count(), 1);
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782
783 #[tokio::test]
784 async fn test_dht_manager_creation() {
785 let config = DhtConfig::default();
786 let manager = DhtManager::new(config);
787 let stats = manager.get_stats();
788 assert_eq!(stats.total_queries, 0);
789 assert_eq!(stats.cache_hits, 0);
790 }
791
792 #[tokio::test]
793 async fn test_query_caching() {
794 let manager = DhtManager::new(DhtConfig::default());
795 let cid = Cid::default();
796 let peers = vec![PeerId::random(), PeerId::random()];
797
798 manager.cache_query_result(&cid, peers.clone());
800
801 let cached = manager.get_cached_query(&cid);
803 assert!(cached.is_some());
804 assert_eq!(
805 cached
806 .expect("test: cached query result should be Some after cache_query_result")
807 .len(),
808 peers.len()
809 );
810
811 let stats = manager.get_stats();
812 assert_eq!(stats.cache_hits, 1);
813 assert_eq!(stats.total_queries, 1);
814 }
815
816 #[tokio::test]
817 async fn test_query_cache_expiration() {
818 let config = DhtConfig {
819 query_cache_ttl: Duration::from_millis(100),
820 ..Default::default()
821 };
822
823 let manager = DhtManager::new(config);
824 let cid = Cid::default();
825 let peers = vec![PeerId::random()];
826
827 manager.cache_query_result(&cid, peers);
828
829 assert!(manager.get_cached_query(&cid).is_some());
831
832 tokio::time::sleep(Duration::from_millis(150)).await;
834
835 assert!(manager.get_cached_query(&cid).is_none());
837 }
838
839 #[tokio::test]
840 async fn test_peer_caching() {
841 let manager = DhtManager::new(DhtConfig::default());
842 let peer1 = PeerId::random();
843 let peer2 = PeerId::random();
844
845 manager.cache_peer(peer1);
846 manager.cache_peer(peer2);
847
848 assert!(manager.is_peer_cached(&peer1));
849 assert!(manager.is_peer_cached(&peer2));
850
851 let cached_peers = manager.get_cached_peers();
852 assert_eq!(cached_peers.len(), 2);
853 }
854
855 #[tokio::test]
856 async fn test_provider_tracking() {
857 let mut manager = DhtManager::new(DhtConfig::default());
858 manager.start_provider_refresh();
859
860 let cid = Cid::default();
861 manager
862 .track_provider(cid)
863 .await
864 .expect("test: track_provider should succeed");
865
866 tokio::time::sleep(Duration::from_millis(50)).await;
868
869 let stats = manager.get_stats();
870 assert_eq!(stats.active_providers, 1);
871
872 manager.shutdown().await;
873 }
874
875 #[tokio::test]
876 async fn test_cache_cleanup() {
877 let config = DhtConfig {
878 query_cache_ttl: Duration::from_millis(100),
879 ..Default::default()
880 };
881
882 let manager = DhtManager::new(config);
883 let cid = Cid::default();
884 let peers = vec![PeerId::random()];
885
886 manager.cache_query_result(&cid, peers);
887 assert_eq!(manager.get_stats().cached_queries, 1);
888
889 tokio::time::sleep(Duration::from_millis(150)).await;
891
892 manager.cleanup_cache();
894 assert_eq!(manager.get_stats().cached_queries, 0);
895 }
896
897 #[tokio::test]
898 async fn test_cache_size_limit() {
899 let config = DhtConfig {
900 max_cached_queries: 5,
901 ..Default::default()
902 };
903
904 let manager = DhtManager::new(config);
905
906 for i in 0..10 {
908 let key = format!("test-{}", i);
909 manager.query_cache.insert(
911 key,
912 CachedQuery {
913 peers: vec![PeerId::random()],
914 cached_at: Instant::now(),
915 hit_count: 0,
916 },
917 );
918 }
919
920 assert!(manager.query_cache.len() <= 15);
922 }
923
924 #[tokio::test]
925 async fn test_health_monitoring_unknown() {
926 let manager = DhtManager::new(DhtConfig::default());
927
928 let health = manager.get_health();
930 assert_eq!(health.status, DhtHealthStatus::Unknown);
931 assert!(manager.is_healthy()); }
933
934 #[tokio::test]
935 async fn test_health_monitoring_healthy() {
936 let manager = DhtManager::new(DhtConfig::default());
937
938 for _ in 0..15 {
940 manager.record_query_success();
941 }
942
943 let cid = Cid::default();
945 let peers = vec![PeerId::random()];
946 manager.cache_query_result(&cid, peers);
947 let _ = manager.get_cached_query(&cid);
948
949 manager.cache_peer(PeerId::random());
951
952 let health = manager.get_health();
953 assert_eq!(health.status, DhtHealthStatus::Healthy);
954 assert!(health.health_score >= 0.8);
955 assert_eq!(health.query_success_rate, 1.0);
956 assert!(manager.is_healthy());
957 }
958
959 #[tokio::test]
960 async fn test_health_monitoring_degraded() {
961 let manager = DhtManager::new(DhtConfig::default());
962
963 for _ in 0..7 {
965 manager.record_query_success();
966 }
967 for _ in 0..5 {
968 manager.record_query_failure();
969 }
970
971 let health = manager.get_health();
972 assert!(health.query_success_rate > 0.5);
974 assert!(health.query_success_rate < 1.0);
975 }
976
977 #[tokio::test]
978 async fn test_health_monitoring_unhealthy() {
979 let manager = DhtManager::new(DhtConfig::default());
980
981 for _ in 0..2 {
983 manager.record_query_success();
984 }
985 for _ in 0..10 {
986 manager.record_query_failure();
987 }
988
989 let health = manager.get_health();
990 assert!(matches!(
991 health.status,
992 DhtHealthStatus::Unhealthy | DhtHealthStatus::Degraded
993 ));
994 assert!(health.query_success_rate < 0.5);
995 assert!(!manager.is_healthy());
996 }
997
998 #[tokio::test]
999 async fn test_health_cache_hit_rate() {
1000 let manager = DhtManager::new(DhtConfig::default());
1001
1002 for _ in 0..15 {
1004 manager.record_query_success();
1005 }
1006
1007 let cid1 = Cid::default();
1009 let peers = vec![PeerId::random()];
1010 manager.cache_query_result(&cid1, peers);
1011
1012 let _ = manager.get_cached_query(&cid1);
1014
1015 manager.stats.write().total_queries += 1;
1017 manager.stats.write().cache_misses += 1;
1018
1019 let health = manager.get_health();
1020 assert_eq!(health.cache_hit_rate, 0.5);
1021 }
1022}