1use super::config::BootstrapCacheConfig;
11use super::entry::{CachedPeer, ConnectionOutcome, PeerCapabilities, PeerSource};
12use super::persistence::{CacheData, CachePersistence};
13use super::selection::select_epsilon_greedy;
14use crate::nat_traversal_api::PeerId;
15use std::net::SocketAddr;
16use std::sync::Arc;
17use std::time::{Instant, SystemTime};
18use tokio::sync::{RwLock, broadcast};
19use tracing::{debug, info, warn};
20
21#[derive(Debug, Clone)]
23pub enum CacheEvent {
24 Updated {
26 peer_count: usize,
28 },
29 Saved,
31 Merged {
33 added: usize,
35 },
36 Cleaned {
38 removed: usize,
40 },
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct CacheStats {
46 pub total_peers: usize,
48 pub relay_peers: usize,
50 pub coordinator_peers: usize,
52 pub dual_stack_relay_peers: usize,
54 pub average_quality: f64,
56 pub untested_peers: usize,
58}
59
60#[derive(Debug)]
66pub struct BootstrapCache {
67 config: BootstrapCacheConfig,
68 data: Arc<RwLock<CacheData>>,
69 persistence: Option<CachePersistence>,
71 event_tx: broadcast::Sender<CacheEvent>,
72 last_save: Arc<RwLock<Instant>>,
73 last_cleanup: Arc<RwLock<Instant>>,
74}
75
76impl BootstrapCache {
77 pub async fn open(config: BootstrapCacheConfig) -> std::io::Result<Self> {
84 let persistence = if config.persist {
85 Some(CachePersistence::new(
86 &config.cache_dir,
87 config.enable_file_locking,
88 )?)
89 } else {
90 None
91 };
92 let data = match &persistence {
93 Some(p) => p.load()?,
94 None => CacheData::new(super::persistence::generate_instance_id()),
95 };
96 let (event_tx, _) = broadcast::channel(256);
97 let now = Instant::now();
98
99 info!(
100 "Opened bootstrap cache with {} peers (persist: {})",
101 data.peers.len(),
102 config.persist
103 );
104
105 Ok(Self {
106 config,
107 data: Arc::new(RwLock::new(data)),
108 persistence,
109 event_tx,
110 last_save: Arc::new(RwLock::new(now)),
111 last_cleanup: Arc::new(RwLock::new(now)),
112 })
113 }
114
115 pub fn subscribe(&self) -> broadcast::Receiver<CacheEvent> {
117 self.event_tx.subscribe()
118 }
119
120 pub async fn peer_count(&self) -> usize {
122 self.data.read().await.peers.len()
123 }
124
125 pub async fn get_peer(&self, peer_id: &PeerId) -> Option<CachedPeer> {
127 let mut data = self.data.write().await;
128 let peer = data.peers.get_mut(&peer_id.0)?;
129 peer.capabilities
130 .refresh_direct_capabilities(self.config.reachability_ttl, SystemTime::now());
131 peer.calculate_quality(&self.config.weights);
132 Some(peer.clone())
133 }
134
135 fn refresh_cached_peer(&self, peer: &mut CachedPeer, now: SystemTime) {
136 peer.capabilities
137 .refresh_direct_capabilities(self.config.reachability_ttl, now);
138 peer.calculate_quality(&self.config.weights);
139 }
140
141 pub async fn select_peers(&self, count: usize) -> Vec<CachedPeer> {
146 let mut data = self.data.write().await;
147 let now = SystemTime::now();
148 for peer in data.peers.values_mut() {
149 self.refresh_cached_peer(peer, now);
150 }
151 let peers: Vec<CachedPeer> = data.peers.values().cloned().collect();
152 drop(data);
153
154 select_epsilon_greedy(&peers, count, self.config.epsilon)
155 .into_iter()
156 .cloned()
157 .collect()
158 }
159
160 pub async fn select_relay_peers(&self, count: usize) -> Vec<CachedPeer> {
164 let mut data = self.data.write().await;
165 let now = SystemTime::now();
166 for peer in data.peers.values_mut() {
167 self.refresh_cached_peer(peer, now);
168 }
169 let peers: Vec<CachedPeer> = data.peers.values().cloned().collect();
170 drop(data);
171
172 super::selection::select_with_capabilities(&peers, count, true, false)
173 .into_iter()
174 .cloned()
175 .collect()
176 }
177
178 pub async fn select_coordinators(&self, count: usize) -> Vec<CachedPeer> {
182 let mut data = self.data.write().await;
183 let now = SystemTime::now();
184 for peer in data.peers.values_mut() {
185 self.refresh_cached_peer(peer, now);
186 }
187 let peers: Vec<CachedPeer> = data.peers.values().cloned().collect();
188 drop(data);
189
190 super::selection::select_with_capabilities(&peers, count, false, true)
191 .into_iter()
192 .cloned()
193 .collect()
194 }
195
196 pub async fn select_relays_for_target(
206 &self,
207 count: usize,
208 target: &std::net::SocketAddr,
209 prefer_dual_stack: bool,
210 ) -> Vec<CachedPeer> {
211 use super::selection::select_relays_for_target;
212
213 let mut data = self.data.write().await;
214 let now = SystemTime::now();
215 for peer in data.peers.values_mut() {
216 self.refresh_cached_peer(peer, now);
217 }
218 let peers: Vec<CachedPeer> = data.peers.values().cloned().collect();
219 drop(data);
220
221 select_relays_for_target(&peers, count, *target, prefer_dual_stack)
222 .into_iter()
223 .cloned()
224 .collect()
225 }
226
227 pub async fn select_dual_stack_relays(&self, count: usize) -> Vec<CachedPeer> {
231 use super::selection::select_dual_stack_relays;
232
233 let mut data = self.data.write().await;
234 let now = SystemTime::now();
235 for peer in data.peers.values_mut() {
236 self.refresh_cached_peer(peer, now);
237 }
238 let peers: Vec<CachedPeer> = data.peers.values().cloned().collect();
239 drop(data);
240
241 select_dual_stack_relays(&peers, count)
242 .into_iter()
243 .cloned()
244 .collect()
245 }
246
247 pub async fn upsert(&self, peer: CachedPeer) {
251 let mut data = self.data.write().await;
252
253 if data.peers.len() >= self.config.max_peers && !data.peers.contains_key(&peer.peer_id.0) {
255 self.evict_lowest_quality(&mut data);
256 }
257
258 data.peers.insert(peer.peer_id.0, peer);
259
260 let count = data.peers.len();
261 drop(data);
262
263 let _ = self
264 .event_tx
265 .send(CacheEvent::Updated { peer_count: count });
266 }
267
268 pub async fn add_seed(&self, peer_id: PeerId, addresses: Vec<SocketAddr>) {
270 let peer = CachedPeer::new(peer_id, addresses, PeerSource::Seed);
271 self.upsert(peer).await;
272 }
273
274 pub async fn add_from_connection(
276 &self,
277 peer_id: PeerId,
278 addresses: Vec<SocketAddr>,
279 caps: Option<PeerCapabilities>,
280 ) {
281 let mut peer = CachedPeer::new(peer_id, addresses, PeerSource::Connection);
282 if let Some(caps) = caps {
283 peer.capabilities = caps;
284 }
285 self.upsert(peer).await;
286 }
287
288 pub async fn record_outcome(&self, peer_id: &PeerId, outcome: ConnectionOutcome) {
290 let mut data = self.data.write().await;
291
292 if let Some(peer) = data.peers.get_mut(&peer_id.0) {
293 if outcome.success {
294 peer.record_success(
295 outcome.rtt_ms.unwrap_or(100),
296 outcome.capabilities_discovered,
297 );
298 } else {
299 peer.record_failure();
300 }
301
302 peer.calculate_quality(&self.config.weights);
304 }
305 }
306
307 pub async fn record_success(&self, peer_id: &PeerId, rtt_ms: u32) {
309 self.record_outcome(
310 peer_id,
311 ConnectionOutcome {
312 success: true,
313 rtt_ms: Some(rtt_ms),
314 capabilities_discovered: None,
315 },
316 )
317 .await;
318 }
319
320 pub async fn record_failure(&self, peer_id: &PeerId) {
322 self.record_outcome(
323 peer_id,
324 ConnectionOutcome {
325 success: false,
326 rtt_ms: None,
327 capabilities_discovered: None,
328 },
329 )
330 .await;
331 }
332
333 pub async fn update_capabilities(&self, peer_id: &PeerId, caps: PeerCapabilities) {
335 let mut data = self.data.write().await;
336
337 if let Some(peer) = data.peers.get_mut(&peer_id.0) {
338 peer.capabilities = caps;
339 peer.calculate_quality(&self.config.weights);
340 }
341 }
342
343 pub async fn observe_direct_reachability(&self, peer_id: PeerId, address: SocketAddr) {
349 let mut data = self.data.write().await;
350 let now = SystemTime::now();
351
352 let peer = data
353 .peers
354 .entry(peer_id.0)
355 .or_insert_with(|| CachedPeer::new(peer_id, vec![address], PeerSource::Connection));
356
357 if !peer.addresses.contains(&address) {
358 peer.addresses.push(address);
359 }
360
361 peer.last_seen = now;
362 peer.last_attempt = Some(now);
363 peer.stats.success_count = peer.stats.success_count.saturating_add(1);
364 peer.capabilities.record_direct_observation(address, now);
365 self.refresh_cached_peer(peer, now);
366
367 let count = data.peers.len();
368 drop(data);
369
370 let _ = self
371 .event_tx
372 .send(CacheEvent::Updated { peer_count: count });
373 }
374
375 pub async fn get(&self, peer_id: &PeerId) -> Option<CachedPeer> {
377 let mut data = self.data.write().await;
378 let peer = data.peers.get_mut(&peer_id.0)?;
379 self.refresh_cached_peer(peer, SystemTime::now());
380 Some(peer.clone())
381 }
382
383 pub async fn update_token(&self, peer_id: PeerId, token: Vec<u8>) {
385 let mut data = self.data.write().await;
386 if let Some(peer) = data.peers.get_mut(&peer_id.0) {
387 peer.token = Some(token);
388 }
389 }
390
391 pub async fn get_all_tokens(&self) -> std::collections::HashMap<PeerId, Vec<u8>> {
393 self.data
394 .read()
395 .await
396 .peers
397 .values()
398 .filter_map(|p| p.token.clone().map(|t| (p.peer_id, t)))
399 .collect()
400 }
401
402 pub async fn contains(&self, peer_id: &PeerId) -> bool {
404 self.data.read().await.peers.contains_key(&peer_id.0)
405 }
406
407 pub async fn remove(&self, peer_id: &PeerId) -> Option<CachedPeer> {
409 self.data.write().await.peers.remove(&peer_id.0)
410 }
411
412 pub async fn save(&self) -> std::io::Result<()> {
414 let Some(persistence) = &self.persistence else {
415 return Ok(());
416 };
417 let mut data = self.data.write().await;
418
419 if data.peers.len() < self.config.min_peers_to_save {
420 debug!(
421 "Skipping save: only {} peers (min: {})",
422 data.peers.len(),
423 self.config.min_peers_to_save
424 );
425 return Ok(());
426 }
427
428 persistence.save(&mut data)?;
429
430 drop(data);
431 *self.last_save.write().await = Instant::now();
432 let _ = self.event_tx.send(CacheEvent::Saved);
433
434 Ok(())
435 }
436
437 pub async fn cleanup_stale(&self) -> usize {
442 let mut data = self.data.write().await;
443 let initial_count = data.peers.len();
444
445 data.peers
446 .retain(|_, peer| !peer.is_stale(self.config.stale_threshold));
447
448 let removed = initial_count - data.peers.len();
449
450 if removed > 0 {
451 info!("Cleaned up {} stale peers", removed);
452 let _ = self.event_tx.send(CacheEvent::Cleaned { removed });
453 }
454
455 drop(data);
456 *self.last_cleanup.write().await = Instant::now();
457
458 removed
459 }
460
461 pub async fn recalculate_quality(&self) {
463 let mut data = self.data.write().await;
464
465 for peer in data.peers.values_mut() {
466 peer.calculate_quality(&self.config.weights);
467 }
468
469 let count = data.peers.len();
470 let _ = self
471 .event_tx
472 .send(CacheEvent::Updated { peer_count: count });
473 }
474
475 pub async fn stats(&self) -> CacheStats {
477 let mut data = self.data.write().await;
478 let now = SystemTime::now();
479 for peer in data.peers.values_mut() {
480 self.refresh_cached_peer(peer, now);
481 }
482
483 let relay_count = data
484 .peers
485 .values()
486 .filter(|p| p.capabilities.supports_relay)
487 .count();
488 let coord_count = data
489 .peers
490 .values()
491 .filter(|p| p.capabilities.supports_coordination)
492 .count();
493 let dual_stack_count = data
494 .peers
495 .values()
496 .filter(|p| p.capabilities.supports_relay && p.capabilities.supports_dual_stack())
497 .count();
498 let untested = data
499 .peers
500 .values()
501 .filter(|p| p.stats.success_count + p.stats.failure_count == 0)
502 .count();
503 let avg_quality = if data.peers.is_empty() {
504 0.0
505 } else {
506 data.peers.values().map(|p| p.quality_score).sum::<f64>() / data.peers.len() as f64
507 };
508
509 CacheStats {
510 total_peers: data.peers.len(),
511 relay_peers: relay_count,
512 coordinator_peers: coord_count,
513 dual_stack_relay_peers: dual_stack_count,
514 average_quality: avg_quality,
515 untested_peers: untested,
516 }
517 }
518
519 pub fn start_maintenance(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
532 let cache = self;
533
534 tokio::spawn(async move {
535 let mut save_interval = tokio::time::interval(cache.config.save_interval);
536 let mut cleanup_interval = tokio::time::interval(cache.config.cleanup_interval);
537 let mut quality_interval = tokio::time::interval(cache.config.quality_update_interval);
538
539 loop {
540 tokio::select! {
541 _ = save_interval.tick() => {
542 if let Err(e) = cache.save().await {
543 warn!("Failed to save cache: {}", e);
544 }
545 }
546 _ = cleanup_interval.tick() => {
547 cache.cleanup_stale().await;
548 }
549 _ = quality_interval.tick() => {
550 cache.recalculate_quality().await;
551 }
552 }
553 }
554 })
555 }
556
557 pub async fn all_peers(&self) -> Vec<CachedPeer> {
559 let mut data = self.data.write().await;
560 let now = SystemTime::now();
561 for peer in data.peers.values_mut() {
562 self.refresh_cached_peer(peer, now);
563 }
564 data.peers.values().cloned().collect()
565 }
566
567 pub fn config(&self) -> &BootstrapCacheConfig {
569 &self.config
570 }
571
572 fn evict_lowest_quality(&self, data: &mut CacheData) {
573 let evict_count = (self.config.max_peers / 20).max(1); let mut sorted: Vec<_> = data.peers.iter().collect();
576 sorted.sort_by(|a, b| {
577 a.1.quality_score
578 .partial_cmp(&b.1.quality_score)
579 .unwrap_or(std::cmp::Ordering::Equal)
580 });
581
582 let to_remove: Vec<[u8; 32]> = sorted
583 .into_iter()
584 .take(evict_count)
585 .map(|(id, _)| *id)
586 .collect();
587
588 for id in to_remove {
589 data.peers.remove(&id);
590 }
591
592 debug!("Evicted {} lowest quality peers", evict_count);
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use tempfile::TempDir;
600
601 async fn create_test_cache(temp_dir: &TempDir) -> BootstrapCache {
602 let config = BootstrapCacheConfig::builder()
603 .cache_dir(temp_dir.path())
604 .max_peers(100)
605 .epsilon(0.0) .min_peers_to_save(1)
607 .build();
608
609 BootstrapCache::open(config).await.unwrap()
610 }
611
612 #[tokio::test]
618 async fn in_memory_cache_never_touches_disk() {
619 let temp_dir = TempDir::new().unwrap();
620 let cache_dir = temp_dir.path().join("does-not-exist");
621 let config = BootstrapCacheConfig::builder()
622 .cache_dir(&cache_dir)
623 .min_peers_to_save(1)
624 .persist(false)
625 .build();
626
627 let cache = BootstrapCache::open(config).await.unwrap();
628 cache
629 .add_seed(PeerId([7u8; 32]), vec!["127.0.0.1:9000".parse().unwrap()])
630 .await;
631 cache.save().await.unwrap();
632
633 assert!(
634 !cache_dir.exists(),
635 "in-memory cache must not create its cache directory"
636 );
637 assert_eq!(cache.peer_count().await, 1);
639 }
640
641 #[tokio::test]
642 async fn test_cache_creation() {
643 let temp_dir = TempDir::new().unwrap();
644 let cache = create_test_cache(&temp_dir).await;
645 assert_eq!(cache.peer_count().await, 0);
646 }
647
648 #[tokio::test]
649 async fn test_add_and_get() {
650 let temp_dir = TempDir::new().unwrap();
651 let cache = create_test_cache(&temp_dir).await;
652
653 let peer_id = PeerId([1u8; 32]);
654 cache
655 .add_seed(peer_id, vec!["127.0.0.1:9000".parse().unwrap()])
656 .await;
657
658 assert_eq!(cache.peer_count().await, 1);
659 assert!(cache.contains(&peer_id).await);
660
661 let peer = cache.get(&peer_id).await.unwrap();
662 assert_eq!(peer.addresses.len(), 1);
663 }
664
665 #[tokio::test]
666 async fn test_select_peers() {
667 let temp_dir = TempDir::new().unwrap();
668 let cache = create_test_cache(&temp_dir).await;
669
670 for i in 0..10usize {
672 let peer_id = PeerId([i as u8; 32]);
673 let mut peer = CachedPeer::new(
674 peer_id,
675 vec![format!("127.0.0.1:{}", 9000 + i).parse().unwrap()],
676 PeerSource::Seed,
677 );
678 peer.quality_score = i as f64 / 10.0;
679 cache.upsert(peer).await;
680 }
681
682 let selected = cache.select_peers(5).await;
684 assert_eq!(selected.len(), 5);
685 assert!(selected[0].quality_score >= selected[4].quality_score);
686 }
687
688 #[tokio::test]
689 async fn test_persistence() {
690 let temp_dir = TempDir::new().unwrap();
691
692 {
694 let cache = create_test_cache(&temp_dir).await;
695 cache
696 .add_seed(PeerId([1; 32]), vec!["127.0.0.1:9000".parse().unwrap()])
697 .await;
698 cache.save().await.unwrap();
699 }
700
701 {
703 let cache = create_test_cache(&temp_dir).await;
704 assert_eq!(cache.peer_count().await, 1);
705 assert!(cache.contains(&PeerId([1; 32])).await);
706 }
707 }
708
709 #[tokio::test]
710 async fn test_persisted_explicit_assist_hints_survive_reopen() {
711 let temp_dir = TempDir::new().unwrap();
712 let peer_id = PeerId([9; 32]);
713 let peer_addr: SocketAddr = "198.51.100.9:9000".parse().unwrap();
714
715 {
716 let cache = create_test_cache(&temp_dir).await;
717 let mut peer = CachedPeer::new(peer_id, vec![peer_addr], PeerSource::Merge);
718 peer.capabilities.record_assist_hints(true, true);
719 cache.upsert(peer).await;
720 cache.save().await.unwrap();
721 }
722
723 {
724 let cache = create_test_cache(&temp_dir).await;
725 let peer = cache.get(&peer_id).await.expect("peer should reload");
726 assert!(peer.capabilities.hinted_supports_relay);
727 assert!(peer.capabilities.hinted_supports_coordination);
728 assert!(peer.capabilities.supports_relay);
729 assert!(peer.capabilities.supports_coordination);
730 assert!(peer.addresses.contains(&peer_addr));
731 }
732 }
733
734 #[tokio::test]
735 async fn test_quality_scoring() {
736 let temp_dir = TempDir::new().unwrap();
737 let cache = create_test_cache(&temp_dir).await;
738
739 let peer_id = PeerId([1; 32]);
740 cache
741 .add_seed(peer_id, vec!["127.0.0.1:9000".parse().unwrap()])
742 .await;
743
744 let peer = cache.get(&peer_id).await.unwrap();
746 let initial_quality = peer.quality_score;
747
748 for _ in 0..5 {
750 cache.record_success(&peer_id, 50).await;
751 }
752
753 let peer = cache.get(&peer_id).await.unwrap();
754 assert!(peer.quality_score > initial_quality);
755 assert!(peer.success_rate() > 0.9);
756 }
757
758 #[tokio::test]
759 async fn test_eviction() {
760 let temp_dir = TempDir::new().unwrap();
761 let config = BootstrapCacheConfig::builder()
762 .cache_dir(temp_dir.path())
763 .max_peers(10)
764 .build();
765
766 let cache = BootstrapCache::open(config).await.unwrap();
767
768 for i in 0..15u8 {
770 let peer_id = PeerId([i; 32]);
771 let mut peer = CachedPeer::new(
772 peer_id,
773 vec![format!("127.0.0.1:{}", 9000 + i as u16).parse().unwrap()],
774 PeerSource::Seed,
775 );
776 peer.quality_score = i as f64 / 15.0;
777 cache.upsert(peer).await;
778 }
779
780 assert!(cache.peer_count().await <= 10);
782 }
783
784 #[tokio::test]
785 async fn test_stats() {
786 let temp_dir = TempDir::new().unwrap();
787 let cache = create_test_cache(&temp_dir).await;
788
789 let mut peer1 = CachedPeer::new(
791 PeerId([1; 32]),
792 vec!["203.0.113.1:9001".parse().unwrap()],
793 PeerSource::Seed,
794 );
795 peer1
796 .capabilities
797 .record_direct_observation("203.0.113.1:9001".parse().unwrap(), SystemTime::now());
798 cache.upsert(peer1).await;
799
800 let mut peer2 = CachedPeer::new(
801 PeerId([2; 32]),
802 vec!["198.51.100.2:9002".parse().unwrap()],
803 PeerSource::Seed,
804 );
805 peer2
806 .capabilities
807 .record_direct_observation("198.51.100.2:9002".parse().unwrap(), SystemTime::now());
808 cache.upsert(peer2).await;
809
810 cache
811 .add_seed(PeerId([3; 32]), vec!["127.0.0.1:9003".parse().unwrap()])
812 .await;
813
814 let stats = cache.stats().await;
815 assert_eq!(stats.total_peers, 3);
816 assert_eq!(stats.relay_peers, 2);
817 assert_eq!(stats.coordinator_peers, 2);
818 assert_eq!(stats.untested_peers, 3);
819 }
820
821 #[tokio::test]
822 async fn test_select_relay_peers() {
823 let temp_dir = TempDir::new().unwrap();
824 let cache = create_test_cache(&temp_dir).await;
825
826 for i in 0..10u8 {
828 let addr: SocketAddr = format!("127.0.0.1:{}", 9000 + i as u16).parse().unwrap();
829 let mut peer = CachedPeer::new(PeerId([i; 32]), vec![addr], PeerSource::Seed);
830 if i % 2 == 0 {
831 peer.capabilities
832 .record_direct_observation(addr, SystemTime::now());
833 }
834 peer.quality_score = i as f64 / 10.0;
835 cache.upsert(peer).await;
836 }
837
838 let relays = cache.select_relay_peers(10).await;
841 assert_eq!(relays.len(), 10); let relay_capable = relays
845 .iter()
846 .take(5)
847 .filter(|p| p.capabilities.direct_reachability_scope.is_some())
848 .count();
849 assert_eq!(
850 relay_capable, 5,
851 "Scoped direct-evidence peers should be first"
852 );
853 }
854
855 #[tokio::test]
856 async fn test_observe_direct_reachability_preserves_local_scope_without_global_promotion() {
857 let temp_dir = TempDir::new().unwrap();
858 let cache = create_test_cache(&temp_dir).await;
859 let peer_id = PeerId([9; 32]);
860 let addr: SocketAddr = "192.168.1.50:9000".parse().unwrap();
861
862 cache.observe_direct_reachability(peer_id, addr).await;
863
864 let peer = cache.get(&peer_id).await.expect("peer inserted");
865 assert!(!peer.capabilities.supports_relay);
866 assert!(!peer.capabilities.supports_coordination);
867 assert_eq!(
868 peer.capabilities.direct_reachability_scope,
869 Some(crate::reachability::ReachabilityScope::LocalNetwork)
870 );
871 assert!(peer.addresses.contains(&addr));
872 assert!(
873 peer.capabilities
874 .reachable_addresses
875 .iter()
876 .any(|entry| entry.address == addr)
877 );
878 assert!(peer.success_rate() > 0.0);
879 }
880}