1use std::collections::{HashMap, VecDeque};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone)]
16pub struct CachedResult {
17 pub providers: Vec<String>,
19 pub resolved_at: Instant,
21 pub ttl: Duration,
23}
24
25impl CachedResult {
26 #[inline]
28 pub fn is_expired(&self, now: Instant) -> bool {
29 now.duration_since(self.resolved_at) >= self.ttl
30 }
31}
32
33#[derive(Debug, Clone)]
37pub struct PendingLookup {
38 pub cid: String,
40 pub queued_at: Instant,
42}
43
44#[derive(Debug, Clone)]
51pub struct LookupHandle {
52 pub cid: String,
54 pub queued_at: Instant,
56}
57
58#[derive(Debug, Default)]
62pub struct BatchResolverStats {
63 pub total_queued: AtomicU64,
65 pub total_resolved: AtomicU64,
67 pub total_cache_hits: AtomicU64,
69 pub total_batches_drained: AtomicU64,
71 pub total_evictions: AtomicU64,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct BatchResolverStatsSnapshot {
78 pub total_queued: u64,
80 pub total_resolved: u64,
82 pub total_cache_hits: u64,
84 pub total_batches_drained: u64,
86 pub total_evictions: u64,
88}
89
90impl BatchResolverStats {
91 pub fn snapshot(&self) -> BatchResolverStatsSnapshot {
93 BatchResolverStatsSnapshot {
94 total_queued: self.total_queued.load(Ordering::Relaxed),
95 total_resolved: self.total_resolved.load(Ordering::Relaxed),
96 total_cache_hits: self.total_cache_hits.load(Ordering::Relaxed),
97 total_batches_drained: self.total_batches_drained.load(Ordering::Relaxed),
98 total_evictions: self.total_evictions.load(Ordering::Relaxed),
99 }
100 }
101}
102
103pub struct BatchCidResolver {
112 pending: Mutex<Vec<PendingLookup>>,
114 cache: Mutex<HashMap<String, CachedResult>>,
116 pub cache_ttl: Duration,
118 pub max_batch_size: usize,
120 pub stats: Arc<BatchResolverStats>,
122}
123
124impl BatchCidResolver {
125 pub fn new() -> Self {
127 Self::with_config(Duration::from_secs(300), 32)
128 }
129
130 pub fn with_config(cache_ttl: Duration, max_batch_size: usize) -> Self {
132 Self {
133 pending: Mutex::new(Vec::new()),
134 cache: Mutex::new(HashMap::new()),
135 cache_ttl,
136 max_batch_size,
137 stats: Arc::new(BatchResolverStats::default()),
138 }
139 }
140
141 pub fn queue_lookup(&self, cid: &str) -> LookupHandle {
147 let now = Instant::now();
148 let lookup = PendingLookup {
149 cid: cid.to_owned(),
150 queued_at: now,
151 };
152 {
153 let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner());
154 guard.push(lookup);
155 }
156 self.stats.total_queued.fetch_add(1, Ordering::Relaxed);
157 LookupHandle {
158 cid: cid.to_owned(),
159 queued_at: now,
160 }
161 }
162
163 pub fn drain_batch(&self) -> Vec<PendingLookup> {
168 let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner());
169 if guard.is_empty() {
170 return Vec::new();
171 }
172 let n = guard.len().min(self.max_batch_size);
173 let drained: Vec<PendingLookup> = guard.drain(..n).collect();
175 drop(guard);
176 if !drained.is_empty() {
177 self.stats
178 .total_batches_drained
179 .fetch_add(1, Ordering::Relaxed);
180 }
181 drained
182 }
183
184 pub fn record_result(&self, cid: &str, providers: Vec<String>) {
188 let entry = CachedResult {
189 providers,
190 resolved_at: Instant::now(),
191 ttl: self.cache_ttl,
192 };
193 {
194 let mut guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
195 guard.insert(cid.to_owned(), entry);
196 }
197 self.stats.total_resolved.fetch_add(1, Ordering::Relaxed);
198 }
199
200 pub fn get_cached(&self, cid: &str) -> Option<Vec<String>> {
202 let now = Instant::now();
203 let guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
204 let entry = guard.get(cid)?;
205 if entry.is_expired(now) {
206 return None;
207 }
208 let providers = entry.providers.clone();
209 drop(guard);
210 self.stats.total_cache_hits.fetch_add(1, Ordering::Relaxed);
211 Some(providers)
212 }
213
214 pub fn evict_expired(&self) {
216 let now = Instant::now();
217 let mut guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
218 let before = guard.len();
219 guard.retain(|_, v| !v.is_expired(now));
220 let after = guard.len();
221 let evicted = (before - after) as u64;
222 drop(guard);
223 if evicted > 0 {
224 self.stats
225 .total_evictions
226 .fetch_add(evicted, Ordering::Relaxed);
227 }
228 }
229
230 pub fn pending_count(&self) -> usize {
232 let guard = self.pending.lock().unwrap_or_else(|e| e.into_inner());
233 guard.len()
234 }
235
236 pub fn cache_size(&self) -> usize {
239 let guard = self.cache.lock().unwrap_or_else(|e| e.into_inner());
240 guard.len()
241 }
242}
243
244impl Default for BatchCidResolver {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250pub struct PrefetchScheduler {
258 access_log: Mutex<VecDeque<(String, Instant)>>,
260 co_access: Mutex<HashMap<String, HashMap<String, u32>>>,
262}
263
264impl PrefetchScheduler {
265 pub fn new() -> Self {
267 Self {
268 access_log: Mutex::new(VecDeque::new()),
269 co_access: Mutex::new(HashMap::new()),
270 }
271 }
272
273 pub fn record_access(&self, cid: &str) {
279 let now = Instant::now();
280 let mut log = self.access_log.lock().unwrap_or_else(|e| e.into_inner());
281
282 let window_size = 4.min(log.len());
284 let recent: Vec<String> = log
285 .iter()
286 .rev()
287 .take(window_size)
288 .map(|(c, _)| c.clone())
289 .collect();
290
291 log.push_back((cid.to_owned(), now));
293 while log.len() > 256 {
295 log.pop_front();
296 }
297 drop(log);
298
299 if !recent.is_empty() {
301 let mut co = self.co_access.lock().unwrap_or_else(|e| e.into_inner());
302 for peer in &recent {
303 co.entry(cid.to_owned())
305 .or_default()
306 .entry(peer.clone())
307 .and_modify(|c| *c += 1)
308 .or_insert(1);
309 co.entry(peer.clone())
311 .or_default()
312 .entry(cid.to_owned())
313 .and_modify(|c| *c += 1)
314 .or_insert(1);
315 }
316 }
317 }
318
319 pub fn prefetch_candidates(&self, cid: &str, top_n: usize) -> Vec<String> {
322 if top_n == 0 {
323 return Vec::new();
324 }
325 let co = self.co_access.lock().unwrap_or_else(|e| e.into_inner());
326 let peers = match co.get(cid) {
327 Some(map) => map,
328 None => return Vec::new(),
329 };
330 let mut sorted: Vec<(String, u32)> = peers.iter().map(|(k, &v)| (k.clone(), v)).collect();
331 sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
332 sorted.into_iter().take(top_n).map(|(k, _)| k).collect()
333 }
334
335 pub fn access_count(&self) -> usize {
337 let log = self.access_log.lock().unwrap_or_else(|e| e.into_inner());
338 log.len()
339 }
340
341 pub fn prune_log(&self, max_age: Duration) {
346 let now = Instant::now();
347 let mut log = self.access_log.lock().unwrap_or_else(|e| e.into_inner());
348 log.retain(|(_, ts)| now.duration_since(*ts) < max_age);
349 }
350}
351
352impl Default for PrefetchScheduler {
353 fn default() -> Self {
354 Self::new()
355 }
356}
357
358#[cfg(test)]
361mod tests {
362 use super::*;
363 use std::thread;
364 use std::time::Duration;
365
366 fn short_ttl_resolver() -> BatchCidResolver {
368 BatchCidResolver::with_config(Duration::from_millis(50), 32)
369 }
370
371 #[test]
374 fn test_queue_and_pending_count() {
375 let r = BatchCidResolver::new();
376 assert_eq!(r.pending_count(), 0);
377 r.queue_lookup("QmA");
378 r.queue_lookup("QmB");
379 assert_eq!(r.pending_count(), 2);
380 assert_eq!(r.stats.snapshot().total_queued, 2);
381 }
382
383 #[test]
384 fn test_drain_batch_respects_max_batch_size() {
385 let r = BatchCidResolver::with_config(Duration::from_secs(300), 3);
386 for i in 0..7u32 {
387 r.queue_lookup(&format!("Qm{}", i));
388 }
389 let batch = r.drain_batch();
390 assert_eq!(batch.len(), 3, "first drain should return max_batch_size");
391 assert_eq!(r.pending_count(), 4, "remaining items still queued");
392 }
393
394 #[test]
395 fn test_drain_batch_empty_returns_empty_vec() {
396 let r = BatchCidResolver::new();
397 let batch = r.drain_batch();
398 assert!(batch.is_empty());
399 assert_eq!(r.stats.snapshot().total_batches_drained, 0);
401 }
402
403 #[test]
404 fn test_drain_batch_increments_stat_on_nonempty() {
405 let r = BatchCidResolver::new();
406 r.queue_lookup("QmX");
407 let _ = r.drain_batch();
408 assert_eq!(r.stats.snapshot().total_batches_drained, 1);
409 }
410
411 #[test]
412 fn test_drain_batch_preserves_fifo_order() {
413 let r = BatchCidResolver::with_config(Duration::from_secs(300), 10);
414 let cids = ["QmA", "QmB", "QmC", "QmD"];
415 for c in &cids {
416 r.queue_lookup(c);
417 }
418 let batch = r.drain_batch();
419 let drained_cids: Vec<&str> = batch.iter().map(|p| p.cid.as_str()).collect();
420 assert_eq!(drained_cids, cids);
421 }
422
423 #[test]
424 fn test_cache_hit_returns_providers() {
425 let r = BatchCidResolver::new();
426 let providers = vec!["peer1".to_string(), "peer2".to_string()];
427 r.record_result("QmFoo", providers.clone());
428 let cached = r.get_cached("QmFoo");
429 assert_eq!(cached, Some(providers));
430 assert_eq!(r.stats.snapshot().total_cache_hits, 1);
431 }
432
433 #[test]
434 fn test_cache_miss_returns_none() {
435 let r = BatchCidResolver::new();
436 assert!(r.get_cached("QmMissing").is_none());
437 assert_eq!(r.stats.snapshot().total_cache_hits, 0);
438 }
439
440 #[test]
441 fn test_expired_cache_entries_evicted() {
442 let r = short_ttl_resolver();
443 r.record_result("QmExpire", vec!["peer".to_string()]);
444 assert_eq!(r.cache_size(), 1);
445
446 thread::sleep(Duration::from_millis(60));
448
449 assert!(r.get_cached("QmExpire").is_none());
451
452 r.evict_expired();
454 assert_eq!(r.cache_size(), 0);
455 assert_eq!(r.stats.snapshot().total_evictions, 1);
456 }
457
458 #[test]
459 fn test_evict_expired_only_removes_stale_entries() {
460 let r = short_ttl_resolver();
461 r.record_result("QmOld", vec!["p1".to_string()]);
462 thread::sleep(Duration::from_millis(60));
463 r.record_result("QmNew", vec!["p2".to_string()]);
465 r.evict_expired();
466 assert_eq!(r.cache_size(), 1);
467 assert!(r.get_cached("QmNew").is_some());
468 }
469
470 #[test]
471 fn test_stats_accumulate_correctly() {
472 let r = BatchCidResolver::new();
473 r.queue_lookup("Qm1");
474 r.queue_lookup("Qm2");
475 r.drain_batch();
476 r.record_result("Qm1", vec!["peer".to_string()]);
477 r.get_cached("Qm1");
478 let snap = r.stats.snapshot();
479 assert_eq!(snap.total_queued, 2);
480 assert_eq!(snap.total_resolved, 1);
481 assert_eq!(snap.total_cache_hits, 1);
482 assert_eq!(snap.total_batches_drained, 1);
483 }
484
485 #[test]
488 fn test_prefetch_records_co_access_patterns() {
489 let s = PrefetchScheduler::new();
490 s.record_access("A");
491 s.record_access("B");
492 let candidates = s.prefetch_candidates("A", 5);
494 assert!(candidates.contains(&"B".to_string()));
495 }
496
497 #[test]
498 fn test_prefetch_candidates_sorted_by_frequency() {
499 let s = PrefetchScheduler::new();
500 for _ in 0..6 {
505 s.record_access("NOISE");
506 s.record_access("BASE");
507 s.record_access("C");
508 }
509 s.record_access("NOISE2");
511 s.record_access("BASE");
512 s.record_access("B");
513
514 let candidates = s.prefetch_candidates("BASE", 3);
515 let filtered: Vec<&str> = candidates
517 .iter()
518 .map(|s| s.as_str())
519 .filter(|&c| c == "C" || c == "B")
520 .collect();
521 assert!(
522 !filtered.is_empty(),
523 "expected at least C or B in candidates"
524 );
525 assert_eq!(filtered[0], "C", "C should be the top co-access candidate");
527 }
528
529 #[test]
530 fn test_prefetch_candidates_empty_for_unknown_cid() {
531 let s = PrefetchScheduler::new();
532 s.record_access("X");
533 let candidates = s.prefetch_candidates("UNKNOWN", 5);
534 assert!(candidates.is_empty());
535 }
536
537 #[test]
538 fn test_prefetch_candidates_top_n_respected() {
539 let s = PrefetchScheduler::new();
540 let base = "BASE";
542 for i in 0..10u32 {
543 s.record_access(base);
544 s.record_access(&format!("PEER{}", i));
545 }
546 let candidates = s.prefetch_candidates(base, 3);
547 assert!(candidates.len() <= 3);
548 }
549
550 #[test]
551 fn test_prefetch_access_count() {
552 let s = PrefetchScheduler::new();
553 assert_eq!(s.access_count(), 0);
554 s.record_access("A");
555 s.record_access("B");
556 s.record_access("C");
557 assert_eq!(s.access_count(), 3);
558 }
559
560 #[test]
561 fn test_prune_log_removes_old_entries() {
562 let s = PrefetchScheduler::new();
563 s.record_access("A");
564 s.record_access("B");
565 thread::sleep(Duration::from_millis(30));
566 s.prune_log(Duration::from_millis(20));
568 assert_eq!(s.access_count(), 0);
569 }
570
571 #[test]
572 fn test_prune_log_keeps_recent_entries() {
573 let s = PrefetchScheduler::new();
574 s.record_access("A");
575 s.prune_log(Duration::from_secs(1));
577 assert_eq!(s.access_count(), 1);
578 }
579
580 #[test]
581 fn test_access_log_capped_at_256() {
582 let s = PrefetchScheduler::new();
583 for i in 0..300u32 {
584 s.record_access(&format!("Qm{}", i));
585 }
586 assert_eq!(s.access_count(), 256);
587 }
588
589 #[test]
590 fn test_lookup_handle_fields() {
591 let r = BatchCidResolver::new();
592 let handle = r.queue_lookup("QmHandle");
593 assert_eq!(handle.cid, "QmHandle");
594 assert!(handle.queued_at.elapsed() < Duration::from_secs(1));
596 }
597}