1use std::collections::HashMap;
30
31const FNV_OFFSET_BASIS_64: u64 = 14_695_981_039_346_656_037;
35const FNV_PRIME_64: u64 = 1_099_511_628_211;
37
38#[inline]
40fn fnv1a_64(data: &[u8]) -> u64 {
41 let mut h = FNV_OFFSET_BASIS_64;
42 for &b in data {
43 h ^= b as u64;
44 h = h.wrapping_mul(FNV_PRIME_64);
45 }
46 h
47}
48
49#[inline]
51fn fnv1a_64_seeded(data: &[u8], seed: u64) -> u64 {
52 let mut h = FNV_OFFSET_BASIS_64 ^ seed;
54 h = h.wrapping_mul(FNV_PRIME_64);
55 for &b in data {
56 h ^= b as u64;
57 h = h.wrapping_mul(FNV_PRIME_64);
58 }
59 h
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct MsgId {
69 pub bytes: [u8; 32],
71}
72
73impl MsgId {
74 #[inline]
76 pub fn from_bytes(bytes: [u8; 32]) -> Self {
77 Self { bytes }
78 }
79}
80
81#[derive(Debug, Clone)]
85pub struct DedupConfig {
86 pub bloom_bits: usize,
89 pub bloom_hash_count: u32,
92 pub cache_capacity: usize,
94 pub window_ms: u64,
97}
98
99impl Default for DedupConfig {
100 fn default() -> Self {
101 Self {
102 bloom_bits: 1 << 17, bloom_hash_count: 7,
104 cache_capacity: 8_192,
105 window_ms: 120_000, }
107 }
108}
109
110#[derive(Debug, Clone)]
114pub struct DedupEntry {
115 pub msg_id: MsgId,
117 pub first_seen: u64,
119 pub count: u32,
121}
122
123#[derive(Debug, Clone, Default)]
127pub struct MsgDedupStats {
128 pub total_seen: u64,
130 pub duplicates: u64,
132 pub unique: u64,
134 pub bloom_false_positives: u64,
137 pub cache_evictions: u64,
139}
140
141pub struct MessageDeduplicator {
147 config: DedupConfig,
148 bloom: Vec<u64>,
150 cache: HashMap<[u8; 32], DedupEntry>,
152 insertion_order: Vec<[u8; 32]>,
154 stats: MsgDedupStats,
156}
157
158impl MessageDeduplicator {
159 pub fn new(config: DedupConfig) -> Self {
163 let bloom_bits = config.bloom_bits.max(64);
165 let words = bloom_bits.div_ceil(64);
166
167 let cache_capacity = config.cache_capacity.max(1);
168 Self {
169 bloom: vec![0u64; words],
170 cache: HashMap::with_capacity(cache_capacity),
171 insertion_order: Vec::with_capacity(cache_capacity),
172 stats: MsgDedupStats::default(),
173 config: DedupConfig {
174 bloom_bits: words * 64,
175 cache_capacity,
176 ..config
177 },
178 }
179 }
180
181 pub fn check_and_insert(&mut self, msg_id: &MsgId, now: u64) -> bool {
191 self.stats.total_seen += 1;
192
193 let bloom_says_seen = self.is_duplicate_bloom(msg_id);
195
196 if bloom_says_seen {
197 if let Some(entry) = self.cache.get_mut(&msg_id.bytes) {
199 entry.count = entry.count.saturating_add(1);
201 self.stats.duplicates += 1;
202 return true;
203 }
204 self.stats.bloom_false_positives += 1;
206 }
207
208 self.insert_bloom(msg_id);
210 self.insert_cache(msg_id, now);
211 self.stats.unique += 1;
212 false
213 }
214
215 pub fn is_duplicate_bloom(&self, msg_id: &MsgId) -> bool {
219 let total_bits = self.bloom.len() * 64;
220 if total_bits == 0 {
221 return false;
222 }
223 for pos in Self::bloom_bit_positions(msg_id, self.config.bloom_hash_count) {
224 let bit_idx = pos % total_bits;
225 let word = bit_idx / 64;
226 let bit = bit_idx % 64;
227 if self.bloom[word] & (1u64 << bit) == 0 {
228 return false;
229 }
230 }
231 true
232 }
233
234 pub fn insert_bloom(&mut self, msg_id: &MsgId) {
236 let total_bits = self.bloom.len() * 64;
237 if total_bits == 0 {
238 return;
239 }
240 for pos in Self::bloom_bit_positions(msg_id, self.config.bloom_hash_count) {
241 let bit_idx = pos % total_bits;
242 let word = bit_idx / 64;
243 let bit = bit_idx % 64;
244 self.bloom[word] |= 1u64 << bit;
245 }
246 }
247
248 pub fn bloom_bit_positions(msg_id: &MsgId, count: u32) -> Vec<usize> {
255 let h1 = fnv1a_64(&msg_id.bytes);
256 let h2 = fnv1a_64_seeded(&msg_id.bytes, 0xDEAD_BEEF_CAFE_BABE_u64);
258 let h2 = h2 | 1;
260
261 (0..count as u64)
262 .map(|i| h1.wrapping_add(i.wrapping_mul(h2)) as usize)
263 .collect()
264 }
265
266 pub fn make_msg_id(data: &[u8]) -> MsgId {
272 let h0 = fnv1a_64(data);
273 let h1 = fnv1a_64_seeded(data, 1);
274 let h2 = fnv1a_64_seeded(data, 2);
275 let h3 = fnv1a_64_seeded(data, 3);
276
277 let mut bytes = [0u8; 32];
278 bytes[0..8].copy_from_slice(&h0.to_le_bytes());
279 bytes[8..16].copy_from_slice(&h1.to_le_bytes());
280 bytes[16..24].copy_from_slice(&h2.to_le_bytes());
281 bytes[24..32].copy_from_slice(&h3.to_le_bytes());
282 MsgId { bytes }
283 }
284
285 pub fn purge_expired(&mut self, now: u64) -> usize {
291 let cutoff = now.saturating_sub(self.config.window_ms);
292 let before = self.cache.len();
293
294 let expired: Vec<[u8; 32]> = self
296 .cache
297 .iter()
298 .filter(|(_, e)| e.first_seen < cutoff)
299 .map(|(k, _)| *k)
300 .collect();
301
302 for key in &expired {
303 self.cache.remove(key);
304 if let Some(pos) = self.insertion_order.iter().position(|k| k == key) {
306 self.insertion_order.remove(pos);
307 }
308 }
309
310 before - self.cache.len()
311 }
312
313 pub fn evict_oldest(&mut self) -> bool {
317 if let Some(oldest) = self.insertion_order.first().copied() {
318 self.insertion_order.remove(0);
319 self.cache.remove(&oldest);
320 self.stats.cache_evictions += 1;
321 true
322 } else {
323 false
324 }
325 }
326
327 pub fn seen_count(&self) -> usize {
329 self.cache.len()
330 }
331
332 pub fn stats(&self) -> &MsgDedupStats {
334 &self.stats
335 }
336
337 pub fn reset(&mut self) {
340 for word in &mut self.bloom {
341 *word = 0;
342 }
343 self.cache.clear();
344 self.insertion_order.clear();
345 self.stats = MsgDedupStats::default();
346 }
347
348 fn insert_cache(&mut self, msg_id: &MsgId, now: u64) {
353 while self.cache.len() >= self.config.cache_capacity {
355 self.evict_oldest();
356 }
357 let entry = DedupEntry {
358 msg_id: msg_id.clone(),
359 first_seen: now,
360 count: 0,
361 };
362 self.cache.insert(msg_id.bytes, entry);
363 self.insertion_order.push(msg_id.bytes);
364 }
365}
366
367#[cfg(test)]
370mod tests {
371 use super::*;
372
373 fn default_config() -> DedupConfig {
374 DedupConfig {
375 bloom_bits: 1 << 14, bloom_hash_count: 7,
377 cache_capacity: 128,
378 window_ms: 60_000,
379 }
380 }
381
382 fn make_dedup() -> MessageDeduplicator {
383 MessageDeduplicator::new(default_config())
384 }
385
386 #[test]
388 fn test_new_message_returns_false() {
389 let mut dedup = make_dedup();
390 let id = MessageDeduplicator::make_msg_id(b"hello");
391 assert!(!dedup.check_and_insert(&id, 1_000));
392 }
393
394 #[test]
396 fn test_duplicate_returns_true() {
397 let mut dedup = make_dedup();
398 let id = MessageDeduplicator::make_msg_id(b"hello");
399 assert!(!dedup.check_and_insert(&id, 1_000));
400 assert!(dedup.check_and_insert(&id, 1_001));
401 }
402
403 #[test]
405 fn test_multiple_duplicates() {
406 let mut dedup = make_dedup();
407 let id = MessageDeduplicator::make_msg_id(b"repeat");
408 assert!(!dedup.check_and_insert(&id, 100));
409 for i in 1..10u64 {
410 assert!(dedup.check_and_insert(&id, 100 + i));
411 }
412 }
413
414 #[test]
416 fn test_bloom_fast_path() {
417 let mut dedup = make_dedup();
418 let id = MessageDeduplicator::make_msg_id(b"bloom_test");
419 assert!(!dedup.is_duplicate_bloom(&id));
420 dedup.insert_bloom(&id);
421 assert!(dedup.is_duplicate_bloom(&id));
422 }
423
424 #[test]
426 fn test_bloom_positions_deterministic() {
427 let id = MessageDeduplicator::make_msg_id(b"positions");
428 let p1 = MessageDeduplicator::bloom_bit_positions(&id, 7);
429 let p2 = MessageDeduplicator::bloom_bit_positions(&id, 7);
430 assert_eq!(p1, p2);
431 }
432
433 #[test]
435 fn test_bloom_positions_count() {
436 let id = MessageDeduplicator::make_msg_id(b"count_test");
437 for k in [1u32, 3, 7, 10, 20] {
438 let positions = MessageDeduplicator::bloom_bit_positions(&id, k);
439 assert_eq!(positions.len(), k as usize);
440 }
441 }
442
443 #[test]
445 fn test_make_msg_id_deterministic() {
446 let id1 = MessageDeduplicator::make_msg_id(b"data");
447 let id2 = MessageDeduplicator::make_msg_id(b"data");
448 assert_eq!(id1, id2);
449 }
450
451 #[test]
453 fn test_identical_data_same_id() {
454 let data = b"same content here";
455 let id1 = MessageDeduplicator::make_msg_id(data);
456 let id2 = MessageDeduplicator::make_msg_id(data);
457 assert_eq!(id1.bytes, id2.bytes);
458 }
459
460 #[test]
462 fn test_different_data_different_id() {
463 let id1 = MessageDeduplicator::make_msg_id(b"message A");
464 let id2 = MessageDeduplicator::make_msg_id(b"message B");
465 assert_ne!(id1.bytes, id2.bytes);
466 }
467
468 #[test]
470 fn test_empty_data_msg_id() {
471 let id = MessageDeduplicator::make_msg_id(b"");
472 assert_ne!(id.bytes, [0u8; 32]);
474 }
475
476 #[test]
478 fn test_stats_accuracy() {
479 let mut dedup = make_dedup();
480 let id_a = MessageDeduplicator::make_msg_id(b"A");
481 let id_b = MessageDeduplicator::make_msg_id(b"B");
482
483 dedup.check_and_insert(&id_a, 1);
484 dedup.check_and_insert(&id_a, 2); dedup.check_and_insert(&id_b, 3);
486
487 let s = dedup.stats();
488 assert_eq!(s.total_seen, 3);
489 assert_eq!(s.unique, 2);
490 assert_eq!(s.duplicates, 1);
491 }
492
493 #[test]
495 fn test_seen_count() {
496 let mut dedup = make_dedup();
497 assert_eq!(dedup.seen_count(), 0);
498 let id = MessageDeduplicator::make_msg_id(b"x");
499 dedup.check_and_insert(&id, 0);
500 assert_eq!(dedup.seen_count(), 1);
501 }
502
503 #[test]
505 fn test_lru_capacity_enforcement() {
506 let config = DedupConfig {
507 bloom_bits: 1 << 16,
508 bloom_hash_count: 7,
509 cache_capacity: 5,
510 window_ms: 60_000,
511 };
512 let mut dedup = MessageDeduplicator::new(config);
513
514 for i in 0u64..10 {
515 let id = MessageDeduplicator::make_msg_id(&i.to_le_bytes());
516 dedup.check_and_insert(&id, i);
517 }
518 assert!(dedup.seen_count() <= 5);
520 }
521
522 #[test]
524 fn test_lru_eviction_stats() {
525 let config = DedupConfig {
526 bloom_bits: 1 << 16,
527 bloom_hash_count: 7,
528 cache_capacity: 3,
529 window_ms: 60_000,
530 };
531 let mut dedup = MessageDeduplicator::new(config);
532
533 for i in 0u64..6 {
534 let id = MessageDeduplicator::make_msg_id(&i.to_le_bytes());
535 dedup.check_and_insert(&id, i);
536 }
537 assert!(dedup.stats().cache_evictions >= 3);
539 }
540
541 #[test]
543 fn test_evict_oldest() {
544 let mut dedup = make_dedup();
545 let id0 = MessageDeduplicator::make_msg_id(b"first");
546 let id1 = MessageDeduplicator::make_msg_id(b"second");
547 dedup.check_and_insert(&id0, 0);
548 dedup.check_and_insert(&id1, 1);
549 assert_eq!(dedup.seen_count(), 2);
550 let evicted = dedup.evict_oldest();
551 assert!(evicted);
552 assert_eq!(dedup.seen_count(), 1);
553 }
554
555 #[test]
557 fn test_evict_oldest_empty() {
558 let mut dedup = make_dedup();
559 assert!(!dedup.evict_oldest());
560 }
561
562 #[test]
564 fn test_purge_expired() {
565 let mut dedup = make_dedup(); let id_old = MessageDeduplicator::make_msg_id(b"old");
567 let id_new = MessageDeduplicator::make_msg_id(b"new");
568
569 dedup.check_and_insert(&id_old, 0);
571 dedup.check_and_insert(&id_new, 100_000);
572
573 let now = 120_001; let purged = dedup.purge_expired(now);
575 assert_eq!(purged, 1);
576 assert_eq!(dedup.seen_count(), 1);
577 }
578
579 #[test]
581 fn test_purge_expired_keeps_recent() {
582 let mut dedup = make_dedup(); let id = MessageDeduplicator::make_msg_id(b"recent");
584 dedup.check_and_insert(&id, 1_000);
585 let purged = dedup.purge_expired(30_000); assert_eq!(purged, 0);
587 assert_eq!(dedup.seen_count(), 1);
588 }
589
590 #[test]
592 fn test_purge_expired_empty() {
593 let mut dedup = make_dedup();
594 assert_eq!(dedup.purge_expired(999_999), 0);
595 }
596
597 #[test]
599 fn test_reset_clears_state() {
600 let mut dedup = make_dedup();
601 let id = MessageDeduplicator::make_msg_id(b"pre-reset");
602 dedup.check_and_insert(&id, 0);
603 dedup.check_and_insert(&id, 1); dedup.reset();
606
607 assert_eq!(dedup.seen_count(), 0);
608 let s = dedup.stats();
609 assert_eq!(s.total_seen, 0);
610 assert_eq!(s.duplicates, 0);
611 assert_eq!(s.unique, 0);
612
613 assert!(!dedup.check_and_insert(&id, 2));
615 }
616
617 #[test]
619 fn test_reset_clears_bloom() {
620 let mut dedup = make_dedup();
621 let id = MessageDeduplicator::make_msg_id(b"bloom_reset");
622 dedup.insert_bloom(&id);
623 assert!(dedup.is_duplicate_bloom(&id));
624 dedup.reset();
625 assert!(!dedup.is_duplicate_bloom(&id));
626 }
627
628 #[test]
630 fn test_bloom_false_positive_tracking() {
631 let config = DedupConfig {
633 bloom_bits: 64,
634 bloom_hash_count: 20,
635 cache_capacity: 1024,
636 window_ms: 60_000,
637 };
638 let mut dedup = MessageDeduplicator::new(config);
639
640 for i in 0u64..50 {
642 let id = MessageDeduplicator::make_msg_id(&i.to_le_bytes());
643 dedup.check_and_insert(&id, i);
644 }
645 let id_new = MessageDeduplicator::make_msg_id(b"definitely_new_unique_key_9999");
648 dedup.check_and_insert(&id_new, 9999);
649
650 let _ = dedup.stats().bloom_false_positives;
653 }
654
655 #[test]
657 fn test_large_volume_unique() {
658 let config = DedupConfig {
659 bloom_bits: 1 << 17,
660 bloom_hash_count: 7,
661 cache_capacity: 2048,
662 window_ms: 60_000,
663 };
664 let mut dedup = MessageDeduplicator::new(config);
665
666 for i in 0u64..1000 {
667 let data = format!("message-{}", i);
668 let id = MessageDeduplicator::make_msg_id(data.as_bytes());
669 assert!(!dedup.check_and_insert(&id, i));
670 }
671 assert_eq!(dedup.stats().total_seen, 1000);
672 assert_eq!(dedup.stats().unique, 1000 - dedup.stats().duplicates);
675 }
676
677 #[test]
679 fn test_large_volume_duplicates() {
680 let mut dedup = make_dedup();
681 let id = MessageDeduplicator::make_msg_id(b"same_msg");
682 assert!(!dedup.check_and_insert(&id, 0));
683 for i in 1u64..1000 {
684 assert!(dedup.check_and_insert(&id, i));
685 }
686 assert_eq!(dedup.stats().duplicates, 999);
687 }
688
689 #[test]
691 fn test_dedup_entry_count() {
692 let mut dedup = make_dedup();
693 let id = MessageDeduplicator::make_msg_id(b"counted");
694 dedup.check_and_insert(&id, 0);
695 dedup.check_and_insert(&id, 1);
696 dedup.check_and_insert(&id, 2);
697 let entry = dedup.cache.get(&id.bytes).cloned();
699 assert!(entry.is_some());
700 let e = entry.unwrap_or_else(|| DedupEntry {
701 msg_id: id.clone(),
702 first_seen: 0,
703 count: 0,
704 });
705 assert_eq!(e.count, 2);
706 }
707
708 #[test]
710 fn test_distinct_messages_independent() {
711 let mut dedup = make_dedup();
712 for i in 0u64..20 {
713 let id = MessageDeduplicator::make_msg_id(&i.to_le_bytes());
714 assert!(!dedup.check_and_insert(&id, i * 100));
715 }
716 for i in 0u64..20 {
718 let id = MessageDeduplicator::make_msg_id(&i.to_le_bytes());
719 assert!(dedup.check_and_insert(&id, i * 100 + 50));
721 }
722 }
723
724 #[test]
726 fn test_msg_id_from_bytes_roundtrip() {
727 let original = [42u8; 32];
728 let id = MsgId::from_bytes(original);
729 assert_eq!(id.bytes, original);
730 }
731
732 #[test]
734 fn test_default_config_valid() {
735 let config = DedupConfig::default();
736 let dedup = MessageDeduplicator::new(config);
737 assert!(!dedup.bloom.is_empty());
738 assert!(dedup.config.cache_capacity > 0);
739 }
740}