1use std::collections::HashMap;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::Mutex;
31use std::time::{Duration, Instant};
32
33#[derive(Debug, Clone)]
37pub enum ResolveResult {
38 Providers(Vec<String>),
40 NotFound,
42 Error(String),
44}
45
46#[derive(Debug, Clone)]
54struct WaiterInfo {}
55
56#[derive(Debug)]
63struct FlightRecord {
64 started_at: Instant,
65 waiters: Vec<WaiterInfo>,
66 result: Option<ResolveResult>,
67}
68
69impl FlightRecord {
70 fn new() -> Self {
71 Self {
72 started_at: Instant::now(),
73 waiters: Vec::new(),
74 result: None,
75 }
76 }
77
78 fn is_timed_out(&self, now: Instant, timeout: Duration) -> bool {
80 now.duration_since(self.started_at) >= timeout
81 }
82}
83
84#[derive(Debug, Clone)]
89pub struct WaiterHandle {
90 pub cid: String,
92 pub position: usize,
94 pub registered_at: Instant,
96}
97
98#[derive(Debug)]
102pub enum AcquireResult {
103 Leader,
105 Waiter(WaiterHandle),
107}
108
109#[derive(Debug, Default)]
113pub struct DedupStats {
114 pub total_leaders: AtomicU64,
116 pub total_waiters: AtomicU64,
118 pub total_resolved: AtomicU64,
120 pub total_timeouts: AtomicU64,
122 pub total_coalesced: AtomicU64,
124}
125
126impl DedupStats {
127 pub fn snapshot(&self) -> DedupStatsSnapshot {
129 DedupStatsSnapshot {
130 total_leaders: self.total_leaders.load(Ordering::Relaxed),
131 total_waiters: self.total_waiters.load(Ordering::Relaxed),
132 total_resolved: self.total_resolved.load(Ordering::Relaxed),
133 total_timeouts: self.total_timeouts.load(Ordering::Relaxed),
134 total_coalesced: self.total_coalesced.load(Ordering::Relaxed),
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct DedupStatsSnapshot {
142 pub total_leaders: u64,
144 pub total_waiters: u64,
146 pub total_resolved: u64,
148 pub total_timeouts: u64,
150 pub total_coalesced: u64,
152}
153
154#[derive(Debug)]
166pub struct RequestDeduplicator {
167 in_flight: Mutex<HashMap<String, FlightRecord>>,
168 pub max_waiters_per_key: usize,
170 pub request_timeout: Duration,
172 pub stats: DedupStats,
174}
175
176impl Default for RequestDeduplicator {
177 fn default() -> Self {
178 Self::new(64, Duration::from_secs(30))
179 }
180}
181
182impl RequestDeduplicator {
183 pub fn new(max_waiters_per_key: usize, request_timeout: Duration) -> Self {
192 Self {
193 in_flight: Mutex::new(HashMap::new()),
194 max_waiters_per_key,
195 request_timeout,
196 stats: DedupStats::default(),
197 }
198 }
199
200 pub fn try_acquire(&self, cid: &str) -> AcquireResult {
205 let mut map = self
206 .in_flight
207 .lock()
208 .expect("RequestDeduplicator lock poisoned");
209
210 match map.get_mut(cid) {
211 Some(record) if record.waiters.len() < self.max_waiters_per_key => {
212 let position = record.waiters.len();
214 let registered_at = Instant::now();
215 record.waiters.push(WaiterInfo {});
216 self.stats.total_waiters.fetch_add(1, Ordering::Relaxed);
217 AcquireResult::Waiter(WaiterHandle {
218 cid: cid.to_owned(),
219 position,
220 registered_at,
221 })
222 }
223 Some(_) => {
224 self.stats.total_leaders.fetch_add(1, Ordering::Relaxed);
227 AcquireResult::Leader
228 }
229 None => {
230 map.insert(cid.to_owned(), FlightRecord::new());
232 self.stats.total_leaders.fetch_add(1, Ordering::Relaxed);
233 AcquireResult::Leader
234 }
235 }
236 }
237
238 pub fn resolve(&self, cid: &str, result: ResolveResult) -> bool {
245 let mut map = self
246 .in_flight
247 .lock()
248 .expect("RequestDeduplicator lock poisoned");
249
250 if let Some(mut record) = map.remove(cid) {
251 record.result = Some(result);
252 let waiter_count = record.waiters.len() as u64;
253 self.stats.total_resolved.fetch_add(1, Ordering::Relaxed);
254 self.stats
255 .total_coalesced
256 .fetch_add(waiter_count, Ordering::Relaxed);
257 true
258 } else {
259 false
260 }
261 }
262
263 pub fn timeout_expired_flights(&self) -> usize {
267 let now = Instant::now();
268 let timeout = self.request_timeout;
269
270 let mut map = self
271 .in_flight
272 .lock()
273 .expect("RequestDeduplicator lock poisoned");
274
275 let before = map.len();
276 map.retain(|_, record| !record.is_timed_out(now, timeout));
277 let pruned = before - map.len();
278
279 if pruned > 0 {
280 self.stats
281 .total_timeouts
282 .fetch_add(pruned as u64, Ordering::Relaxed);
283 }
284 pruned
285 }
286
287 pub fn in_flight_count(&self) -> usize {
289 self.in_flight
290 .lock()
291 .expect("RequestDeduplicator lock poisoned")
292 .len()
293 }
294
295 pub fn waiter_count_for(&self, cid: &str) -> Option<usize> {
298 self.in_flight
299 .lock()
300 .expect("RequestDeduplicator lock poisoned")
301 .get(cid)
302 .map(|r| r.waiters.len())
303 }
304}
305
306#[cfg(test)]
309mod tests {
310 use super::*;
311 use std::time::Duration;
312
313 const CID_A: &str = "QmTestCidA111111111111111111111111111111111111111";
314 const CID_B: &str = "QmTestCidB222222222222222222222222222222222222222";
315
316 fn dedup_default() -> RequestDeduplicator {
317 RequestDeduplicator::new(64, Duration::from_secs(30))
318 }
319
320 #[test]
322 fn first_caller_is_leader() {
323 let dedup = dedup_default();
324 let result = dedup.try_acquire(CID_A);
325 assert!(
326 matches!(result, AcquireResult::Leader),
327 "expected Leader for first caller"
328 );
329 }
330
331 #[test]
333 fn second_caller_same_cid_is_waiter() {
334 let dedup = dedup_default();
335 let _ = dedup.try_acquire(CID_A); let result = dedup.try_acquire(CID_A);
337 assert!(
338 matches!(result, AcquireResult::Waiter(_)),
339 "expected Waiter for second caller with same CID"
340 );
341 }
342
343 #[test]
345 fn different_cid_is_new_leader() {
346 let dedup = dedup_default();
347 let _ = dedup.try_acquire(CID_A);
348 let _ = dedup.try_acquire(CID_A);
349 let result = dedup.try_acquire(CID_B);
350 assert!(
351 matches!(result, AcquireResult::Leader),
352 "expected Leader for a different CID"
353 );
354 }
355
356 #[test]
358 fn waiter_handle_metadata() {
359 let dedup = dedup_default();
360 let _ = dedup.try_acquire(CID_A); let handle = match dedup.try_acquire(CID_A) {
363 AcquireResult::Waiter(h) => h,
364 AcquireResult::Leader => panic!("expected Waiter"),
365 };
366 assert_eq!(handle.cid, CID_A);
367 assert_eq!(handle.position, 0, "first waiter is at position 0");
368 }
369
370 #[test]
372 fn resolve_clears_flight() {
373 let dedup = dedup_default();
374 let _ = dedup.try_acquire(CID_A);
375 assert_eq!(dedup.in_flight_count(), 1);
376
377 let ok = dedup.resolve(CID_A, ResolveResult::NotFound);
378 assert!(ok, "resolve should return true for active flight");
379 assert_eq!(dedup.in_flight_count(), 0, "flight should be removed");
380 }
381
382 #[test]
384 fn resolve_unknown_cid_returns_false() {
385 let dedup = dedup_default();
386 let ok = dedup.resolve("QmNonExistent", ResolveResult::NotFound);
387 assert!(!ok, "resolve on unknown CID should return false");
388 }
389
390 #[test]
392 fn after_resolve_next_caller_is_leader() {
393 let dedup = dedup_default();
394 let _ = dedup.try_acquire(CID_A);
395 dedup.resolve(CID_A, ResolveResult::NotFound);
396
397 let result = dedup.try_acquire(CID_A);
398 assert!(
399 matches!(result, AcquireResult::Leader),
400 "post-resolve caller should become Leader again"
401 );
402 }
403
404 #[test]
406 fn overflow_waiters_become_leaders() {
407 let max = 3usize;
408 let dedup = RequestDeduplicator::new(max, Duration::from_secs(30));
409
410 assert!(matches!(dedup.try_acquire(CID_A), AcquireResult::Leader));
412
413 for _ in 0..max {
415 assert!(matches!(dedup.try_acquire(CID_A), AcquireResult::Waiter(_)));
416 }
417
418 let overflow = dedup.try_acquire(CID_A);
420 assert!(
421 matches!(overflow, AcquireResult::Leader),
422 "overflow caller should be promoted to Leader"
423 );
424 }
425
426 #[test]
428 fn timeout_prunes_expired_flights() {
429 let dedup = RequestDeduplicator::new(64, Duration::from_secs(0));
431 let _ = dedup.try_acquire(CID_A);
432 assert_eq!(dedup.in_flight_count(), 1);
433
434 let pruned = dedup.timeout_expired_flights();
435 assert_eq!(pruned, 1, "one expired flight should have been pruned");
436 assert_eq!(dedup.in_flight_count(), 0);
437 }
438
439 #[test]
441 fn timeout_leaves_fresh_flights() {
442 let dedup = RequestDeduplicator::new(64, Duration::from_secs(9999));
443 let _ = dedup.try_acquire(CID_A);
444 let pruned = dedup.timeout_expired_flights();
445 assert_eq!(pruned, 0, "fresh flight should not be pruned");
446 assert_eq!(dedup.in_flight_count(), 1);
447 }
448
449 #[test]
451 fn stats_leaders_and_waiters() {
452 let dedup = dedup_default();
453 let _ = dedup.try_acquire(CID_A); let _ = dedup.try_acquire(CID_B); let _ = dedup.try_acquire(CID_A); let _ = dedup.try_acquire(CID_A); let snap = dedup.stats.snapshot();
459 assert_eq!(snap.total_leaders, 2, "two distinct leader acquires");
460 assert_eq!(snap.total_waiters, 2, "two coalesced waiters");
461 }
462
463 #[test]
465 fn stats_resolved_and_coalesced() {
466 let dedup = dedup_default();
467 let _ = dedup.try_acquire(CID_A); let _ = dedup.try_acquire(CID_A); let _ = dedup.try_acquire(CID_A); dedup.resolve(
472 CID_A,
473 ResolveResult::Providers(vec!["peer1".to_owned(), "peer2".to_owned()]),
474 );
475
476 let snap = dedup.stats.snapshot();
477 assert_eq!(snap.total_resolved, 1);
478 assert_eq!(snap.total_coalesced, 2, "both waiters coalesced");
479 }
480
481 #[test]
483 fn stats_timeout_counter() {
484 let dedup = RequestDeduplicator::new(64, Duration::from_secs(0));
485 let _ = dedup.try_acquire(CID_A);
486 let _ = dedup.try_acquire(CID_B);
487 let pruned = dedup.timeout_expired_flights();
488 assert_eq!(pruned, 2);
489 let snap = dedup.stats.snapshot();
490 assert_eq!(snap.total_timeouts, 2);
491 }
492
493 #[test]
495 fn multiple_cids_independent_flights() {
496 let dedup = dedup_default();
497 let _ = dedup.try_acquire(CID_A);
498 let _ = dedup.try_acquire(CID_B);
499 assert_eq!(dedup.in_flight_count(), 2);
500
501 dedup.resolve(CID_A, ResolveResult::NotFound);
502 assert_eq!(dedup.in_flight_count(), 1);
503
504 dedup.resolve(CID_B, ResolveResult::Error("timeout".to_owned()));
505 assert_eq!(dedup.in_flight_count(), 0);
506 }
507
508 #[test]
510 fn waiter_count_for_helper() {
511 let dedup = dedup_default();
512 assert_eq!(dedup.waiter_count_for(CID_A), None);
513
514 let _ = dedup.try_acquire(CID_A);
515 assert_eq!(dedup.waiter_count_for(CID_A), Some(0));
516
517 let _ = dedup.try_acquire(CID_A);
518 let _ = dedup.try_acquire(CID_A);
519 assert_eq!(dedup.waiter_count_for(CID_A), Some(2));
520 }
521}