1use std::{
2 hash::{BuildHasher, Hash, RandomState},
3 io::Read,
4 iter::repeat_with,
5 num::NonZeroUsize,
6 sync::{
7 Arc, Mutex, MutexGuard,
8 atomic::{AtomicUsize, Ordering},
9 },
10};
11
12use crate::{
13 associated_data::AssociatedData,
14 bucket::{Bucket, InsertValues, LookupValues},
15 config::CuckooConfiguration,
16 data_block::{DataBlock, Fingerprint},
17 exporter::{
18 CuckooFilterExporter, Exportable, ExportableBuildHasher, ExportableRandomState,
19 read_hasher_from,
20 },
21};
22
23#[derive(Clone)]
117pub struct CuckooFilter<H: BuildHasher> {
118 configuration: CuckooConfiguration,
119 buckets: Arc<Vec<Mutex<Bucket>>>,
120 build_hasher: H,
121 items: Arc<AtomicUsize>,
122}
123
124impl CuckooFilter<RandomState> {
125 #[must_use]
131 pub fn new_random(configuration: CuckooConfiguration) -> Self {
132 Self::new(configuration, RandomState::new())
133 }
134}
135
136impl CuckooFilter<ExportableRandomState> {
137 #[must_use]
145 pub fn new_random_exportable(configuration: CuckooConfiguration) -> Self {
146 Self::new(configuration, ExportableRandomState::new_random())
147 }
148
149 pub fn import_random_exportable(reader: impl Read) -> Result<Self, crate::ImportError> {
156 Self::import(reader)
157 }
158}
159
160impl<H: ExportableBuildHasher + BuildHasher> CuckooFilter<H> {
161 pub fn import(mut reader: impl Read) -> Result<Self, crate::ImportError> {
167 let (hasher, configuration) = Self::import_config(&mut reader)?;
168 Self::import_state(hasher, configuration, reader)
169 }
170
171 pub fn import_config(
173 mut reader: impl Read,
174 ) -> Result<(H, CuckooConfiguration), crate::ImportError> {
175 let hasher = read_hasher_from::<H>(&mut reader)?;
176 let config = CuckooConfiguration::read_from(&mut reader)?;
177 Ok((hasher, config))
178 }
179
180 pub fn import_state(
187 hasher: H,
188 configuration: CuckooConfiguration,
189 mut reader: impl Read,
190 ) -> Result<Self, crate::ImportError> {
191 let mut buckets = Vec::with_capacity(configuration.bucket_count);
192
193 let mut item_count = 0;
194 for _ in 0..configuration.bucket_count {
195 let bucket = Bucket::take_from(&mut reader, &configuration)?;
196 item_count += bucket.occupied_count(&configuration);
197 buckets.push(Mutex::new(bucket));
198 }
199
200 Ok(Self {
201 configuration,
202 buckets: Arc::new(buckets),
203 build_hasher: hasher,
204 items: Arc::new(AtomicUsize::new(item_count)),
205 })
206 }
207
208 pub fn exporter<'a>(&'a self) -> CuckooFilterExporter<'a, H> {
211 CuckooFilterExporter::new(&self.build_hasher, &self.buckets, &self.configuration)
212 }
213}
214
215impl<H: BuildHasher> CuckooFilter<H> {
216 pub fn new(configuration: CuckooConfiguration, build_hasher: H) -> Self {
222 Self {
223 configuration: configuration.clone(),
224 buckets: repeat_with(|| Bucket::new(&configuration).into())
225 .take(configuration.bucket_count)
226 .collect::<Vec<_>>()
227 .into(),
228 build_hasher,
229 items: Arc::new(AtomicUsize::new(0)),
230 }
231 }
232
233 pub const fn get_bucket_count(&self) -> usize {
239 self.configuration.bucket_count
240 }
241
242 pub fn get_item_count(&self) -> usize {
244 self.items.load(Ordering::Relaxed)
245 }
246
247 pub fn get_configuration(&self) -> CuckooConfiguration {
249 self.configuration.clone()
250 }
251
252 pub fn get_memory_usage(&self) -> usize {
254 size_of::<Self>()
255 + size_of::<AtomicUsize>()
256 + size_of::<Vec<Mutex<Bucket>>>()
257 + size_of::<Mutex<Bucket>>() * self.buckets.len()
258 + self.configuration.bucket_byte_size * self.buckets.len()
259 }
260
261 pub(crate) const fn get_expected_memory_usage(
263 bucket_byte_size: usize,
264 buckets: usize,
265 ) -> usize {
266 size_of::<Self>()
267 + size_of::<AtomicUsize>()
268 + size_of::<Vec<Mutex<Bucket>>>()
269 + size_of::<Mutex<Bucket>>() * buckets
270 + bucket_byte_size * buckets
271 }
272
273 pub fn insert_if_not_present<K: Hash + ?Sized>(&self, key: &K) -> Option<Fingerprint> {
284 self.insert_if_not_present_with_update(
285 key,
286 InsertValues::default(),
287 LookupValues::default(),
288 )
289 }
290
291 pub fn insert_if_not_present_with_update<K: Hash + ?Sized>(
297 &self,
298 key: &K,
299 insert_values: InsertValues,
300 lookup_update: LookupValues,
301 ) -> Option<Fingerprint> {
302 let (fp, i1) = self.get_fingerprint_and_index(key);
303
304 let mut contains =
305 self.lock_bucket(i1 as usize)
306 .contains(&fp, &self.configuration, &lookup_update);
307
308 if contains {
309 return None;
310 }
311
312 let i2 = self.alt_index(&fp, i1);
313 contains = self
314 .lock_bucket(i2 as usize)
315 .contains(&fp, &self.configuration, &lookup_update);
316
317 if contains {
318 return None;
319 }
320
321 let mut cur_data_block = self.new_data_block(&fp, insert_values);
322
323 let inserted = self
324 .lock_bucket(i1 as usize)
325 .insert(&cur_data_block, &self.configuration);
326
327 if inserted {
328 self.items.fetch_add(1, Ordering::Relaxed);
329 return None;
330 }
331
332 let inserted = self
333 .lock_bucket(i2 as usize)
334 .insert(&cur_data_block, &self.configuration);
335
336 if inserted {
337 self.items.fetch_add(1, Ordering::Relaxed);
338 return None;
339 }
340
341 let mut cur_index = if rand::random::<bool>() { i1 } else { i2 };
342 let mut new_item = true;
343 for _ in 0..self.configuration.max_kicks {
344 {
345 let mut bucket = self.lock_bucket(cur_index as usize);
346 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
348 if !bucket.kick_lru(
349 &mut cur_data_block,
350 &self.configuration,
351 lru_config,
352 new_item,
353 ) {
354 return Some(cur_data_block.get_fingerprint(&self.configuration));
355 }
356 new_item = false;
357 } else {
358 bucket.kick_random(&mut cur_data_block, &self.configuration);
359 }
360 cur_index = self.alt_index(
361 &cur_data_block.get_fingerprint(&self.configuration),
362 cur_index,
363 );
364 }
365
366 if self
367 .lock_bucket(cur_index as usize)
368 .insert(&cur_data_block, &self.configuration)
369 {
370 self.items.fetch_add(1, Ordering::Relaxed);
371 return None;
373 }
374 }
375
376 Some(cur_data_block.get_fingerprint(&self.configuration))
378 }
379
380 pub fn insert<K: Hash + ?Sized>(&self, key: &K) -> Option<Fingerprint> {
392 self.insert_with_defaults(key, InsertValues::default())
393 }
394
395 pub fn insert_with_defaults<K: Hash + ?Sized>(
400 &self,
401 key: &K,
402 default: InsertValues,
403 ) -> Option<Fingerprint> {
404 let (fp, i1) = self.get_fingerprint_and_index(key);
405 let mut cur_data_block = self.new_data_block(&fp, default);
406
407 let inserted = self
408 .lock_bucket(i1 as usize)
409 .insert(&cur_data_block, &self.configuration);
410
411 if inserted {
412 self.items.fetch_add(1, Ordering::Relaxed);
413 return None;
414 }
415
416 let i2 = self.alt_index(&fp, i1);
417
418 let inserted = self
419 .lock_bucket(i2 as usize)
420 .insert(&cur_data_block, &self.configuration);
421
422 if inserted {
423 self.items.fetch_add(1, Ordering::Relaxed);
424 return None;
425 }
426
427 let mut cur_index = i1;
428 let mut new_item = true;
429 for _ in 0..self.configuration.max_kicks {
430 {
431 let mut bucket = self.lock_bucket(cur_index as usize);
432 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
434 if !bucket.kick_lru(
435 &mut cur_data_block,
436 &self.configuration,
437 lru_config,
438 new_item,
439 ) {
440 return Some(cur_data_block.get_fingerprint(&self.configuration));
441 }
442 new_item = false;
443 } else {
444 bucket.kick_random(&mut cur_data_block, &self.configuration);
446 }
447 cur_index = self.alt_index(
448 &cur_data_block.get_fingerprint(&self.configuration),
449 cur_index,
450 );
451 }
452
453 if self
454 .lock_bucket(cur_index as usize)
455 .insert(&cur_data_block, &self.configuration)
456 {
457 self.items.fetch_add(1, Ordering::Relaxed);
458 return None;
460 }
461 }
462
463 Some(cur_data_block.get_fingerprint(&self.configuration))
465 }
466
467 pub fn contains_with_update<K: Hash + ?Sized>(&self, key: &K, update: LookupValues) -> bool {
472 let (fp, i1) = self.get_fingerprint_and_index(key);
473
474 let mut contains =
475 self.lock_bucket(i1 as usize)
476 .contains(&fp, &self.configuration, &update);
477
478 if !contains {
479 let i2 = self.alt_index(&fp, i1);
480 contains = self
481 .lock_bucket(i2 as usize)
482 .contains(&fp, &self.configuration, &update);
483 }
484
485 contains
486 }
487
488 pub fn contains<K: Hash + ?Sized>(&self, key: &K) -> bool {
493 self.contains_with_update(key, LookupValues::default())
494 }
495
496 pub fn get_associated_data<K: Hash + ?Sized>(&self, key: &K) -> Option<AssociatedData> {
503 self.get_associated_data_with_update(key, LookupValues::default())
504 }
505
506 pub fn get_associated_data_with_update<K: Hash + ?Sized>(
511 &self,
512 key: &K,
513 update: LookupValues,
514 ) -> Option<AssociatedData> {
515 let (fp, i1) = self.get_fingerprint_and_index(key);
516
517 let mut contains =
518 self.lock_bucket(i1 as usize)
519 .get_associated_data(&fp, &self.configuration, &update);
520
521 if contains.is_none() {
522 let i2 = self.alt_index(&fp, i1);
523 contains = self.lock_bucket(i2 as usize).get_associated_data(
524 &fp,
525 &self.configuration,
526 &update,
527 );
528 }
529
530 contains
531 }
532
533 pub fn remove<K: Hash + ?Sized>(&self, key: &K) -> bool {
537 let (fp, i1) = self.get_fingerprint_and_index(key);
538
539 let mut removed = self
540 .lock_bucket(i1 as usize)
541 .remove(&fp, &self.configuration);
542
543 if !removed {
544 let i2 = self.alt_index(&fp, i1);
545 removed = self
546 .lock_bucket(i2 as usize)
547 .remove(&fp, &self.configuration);
548 }
549
550 if removed {
551 self.items.fetch_sub(1, Ordering::Relaxed);
552 }
553
554 removed
555 }
556
557 pub fn scan_and_update_full(&self) -> usize {
600 #[expect(clippy::unwrap_used)]
601 self.scan_and_update_full_partition(NonZeroUsize::new(1).unwrap(), 0)
602 }
603
604 pub fn scan_and_update_full_partition(
609 &self,
610 total_partitions: NonZeroUsize,
611 partition_index: usize,
612 ) -> usize {
613 if self.configuration.lru_field_config.is_none()
614 && self.configuration.ttl_field_config.is_none()
615 {
616 return 0;
617 }
618
619 let mut removed = 0;
620 let part_size = self.buckets.len().div_ceil(total_partitions.get());
621 if (partition_index * part_size) >= self.buckets.len() {
622 return 0;
623 }
624 for b in self.buckets
625 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
626 .iter()
627 {
628 #[expect(clippy::unwrap_used)]
629 let mut bucket = b.lock().unwrap();
630 if let Some(lru_config) = &self.configuration.lru_field_config {
631 removed += bucket.age_lru_counters(&self.configuration, lru_config)
632 }
633 if let Some(ttl_config) = &self.configuration.ttl_field_config {
634 removed += bucket.age_ttl_counters(&self.configuration, ttl_config);
635 }
636 }
637
638 if removed > 0 {
639 self.items.fetch_sub(removed, Ordering::Relaxed);
640 }
641 removed
642 }
643
644 pub fn scan_and_update_ttl(&self) -> usize {
651 #[expect(clippy::unwrap_used)]
652 self.scan_and_update_ttl_partition(NonZeroUsize::new(1).unwrap(), 0)
653 }
654
655 pub fn scan_and_update_ttl_partition(
660 &self,
661 total_partitions: NonZeroUsize,
662 partition_index: usize,
663 ) -> usize {
664 if self.configuration.ttl_field_config.is_none() {
665 return 0;
666 }
667
668 let mut removed = 0;
669 let part_size = self.buckets.len().div_ceil(total_partitions.get());
670 if (partition_index * part_size) >= self.buckets.len() {
671 return 0;
672 }
673 for b in self.buckets
674 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
675 .iter()
676 {
677 #[expect(clippy::unwrap_used)]
678 let mut bucket = b.lock().unwrap();
679 if let Some(ttl_config) = &self.configuration.ttl_field_config {
680 removed += bucket.age_ttl_counters(&self.configuration, ttl_config);
681 }
682 }
683
684 if removed > 0 {
685 self.items.fetch_sub(removed, Ordering::Relaxed);
686 }
687 removed
688 }
689
690 pub fn scan_and_update_lru(&self) -> usize {
697 #[expect(clippy::unwrap_used)]
698 self.scan_and_update_lru_partition(NonZeroUsize::new(1).unwrap(), 0)
699 }
700
701 pub fn scan_and_update_lru_partition(
706 &self,
707 total_partitions: NonZeroUsize,
708 partition_index: usize,
709 ) -> usize {
710 if self.configuration.lru_field_config.is_none() {
711 return 0;
712 }
713
714 let mut removed = 0;
715 let part_size = self.buckets.len().div_ceil(total_partitions.get());
716 if (partition_index * part_size) >= self.buckets.len() {
717 return removed;
718 }
719 for b in self.buckets
720 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
721 .iter()
722 {
723 #[expect(clippy::unwrap_used)]
724 let mut bucket = b.lock().unwrap();
725 if let Some(lru_config) = &self.configuration.lru_field_config {
726 removed += bucket.age_lru_counters(&self.configuration, lru_config);
727 }
728 }
729
730 if removed > 0 {
731 self.items.fetch_sub(removed, Ordering::Relaxed);
732 }
733 removed
734 }
735
736 pub(crate) fn get_fingerprint<K: Hash + ?Sized>(&self, key: &K) -> Fingerprint {
738 self.get_fingerprint_and_index(key).0
739 }
740
741 fn new_data_block(&self, fp: &Fingerprint, defaults: InsertValues) -> DataBlock<Vec<u8>> {
742 let data = vec![0u8; self.configuration.data_block_size];
743 let mut cur_data_block = DataBlock::from(data);
744 cur_data_block.store_fingerprint(fp, &self.configuration);
745
746 if let Some(ttl_config) = self.configuration.ttl_field_config.as_ref() {
747 cur_data_block.set_ttl(ttl_config, defaults.ttl.unwrap_or(ttl_config.0.ttl.into()));
748 }
749 if let Some(counter_config) = self.configuration.counter_field_config.as_ref() {
750 cur_data_block.update_counter(
751 counter_config,
752 defaults
753 .counter
754 .unwrap_or(counter_config.0.change_on_insert),
755 );
756 }
757 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
758 cur_data_block.init_lru_counter(lru_config);
759 }
760 cur_data_block
761 }
762
763 fn get_fingerprint_and_index<K: Hash + ?Sized>(&self, key: &K) -> (Fingerprint, u32) {
764 let result = self.build_hasher.hash_one(key);
765
766 let fingerprint = (result >> 32) as u32;
769 #[expect(clippy::cast_possible_truncation)]
771 let index = result as u32 & self.configuration.buckets_mask;
772
773 (
774 Fingerprint::new(
775 fingerprint,
776 self.configuration.fingerprint_field_config.value_mask(),
777 ),
778 index,
779 )
780 }
781
782 #[expect(clippy::cast_possible_truncation)]
784 fn alt_index(&self, fingerprint: &Fingerprint, index: u32) -> u32 {
785 let result = self.build_hasher.hash_one(fingerprint);
786
787 (index ^ ((result as u32) & self.configuration.buckets_mask))
788 & self.configuration.buckets_mask
789 }
790
791 #[expect(clippy::unwrap_used)]
792 fn lock_bucket(&self, index: usize) -> MutexGuard<'_, Bucket> {
793 self.buckets[index].lock().unwrap()
796 }
797}
798
799#[cfg(test)]
800#[expect(clippy::unwrap_used)]
801mod tests {
802 use std::{
803 collections::{HashSet, VecDeque},
804 hash::Hasher,
805 ops::Range,
806 };
807
808 use crate::config::{CounterConfig, LruConfig, TtlConfig};
809
810 use super::*;
811
812 fn get_words(range: Range<usize>) -> Vec<String> {
813 std::fs::read_to_string("/usr/share/dict/words")
814 .unwrap()
815 .split("\n")
816 .skip(range.start)
817 .take(range.len())
818 .map(ToString::to_string)
819 .collect()
820 }
821
822 #[test]
823 fn basic_insertion() {
824 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
825
826 filter.insert("basic");
827
828 assert!(filter.contains("basic"));
829 }
830
831 #[test]
832 fn basic_removal() {
833 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
834
835 filter.insert("basic");
836
837 assert!(filter.contains("basic"));
838
839 filter.remove("basic");
840
841 assert!(!filter.contains("basic"));
842 }
843
844 struct PredefinedBucketItem(u64);
845 struct TestHasher(u64);
846 impl BuildHasher for TestHasher {
847 type Hasher = TestHasher;
848
849 fn build_hasher(&self) -> Self::Hasher {
850 TestHasher(0)
851 }
852 }
853 impl Hasher for TestHasher {
854 fn finish(&self) -> u64 {
855 self.0
856 }
857
858 fn write(&mut self, bytes: &[u8]) {
859 if bytes.len() == 8 {
860 self.0 = u64::from_ne_bytes(bytes.try_into().unwrap());
861 } else {
862 self.0 = 1 - (u32::from_ne_bytes(bytes.try_into().unwrap()) as u64 % 2);
864 }
865 }
866 }
867 impl Hash for PredefinedBucketItem {
868 fn hash<H: Hasher>(&self, state: &mut H) {
869 state.write_u64(self.0);
870 }
871 }
872
873 #[test]
874 fn lru_insertion() {
875 let filter = CuckooFilter::new(
876 CuckooConfiguration::builder(1000)
877 .bucket_size(2.try_into().unwrap())
878 .with_lru(LruConfig {
879 counter_bits: 8.try_into().unwrap(),
880 ..Default::default()
881 })
882 .build()
883 .unwrap(),
884 TestHasher(0),
885 );
886
887 let test_item = PredefinedBucketItem(2 << 32);
888 filter.insert(&test_item);
889 filter.contains(&test_item); let test_item_2 = PredefinedBucketItem(4 << 32);
892 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
895 filter.insert(&test_item_3); filter.contains(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
899 filter.insert(&test_item_4); assert!(filter.contains(&test_item));
903 assert!(filter.contains(&test_item_2));
904 assert!(filter.contains(&test_item_3));
905 assert!(filter.contains(&test_item_4));
906
907 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
908 filter.insert(&test_item_5);
910
911 assert!(filter.contains(&test_item_2));
912 assert!(filter.contains(&test_item));
913 assert!(filter.contains(&test_item_3));
914
915 assert!(
916 !filter.contains(&test_item_5) || !filter.contains(&test_item_4),
917 "No inserted items are missing, but filter can't hold them all"
918 );
919
920 filter.insert(&test_item_5);
922 filter.insert(&test_item_4);
923 assert!(filter.contains(&test_item));
924 assert!(filter.contains(&test_item_3));
925 }
926
927 #[test]
928 fn alt_index() {
929 let words = get_words(0..200_000);
930 let filter = CuckooFilter::new_random(
931 CuckooConfiguration::builder(200_000)
932 .fingerprint_bits(32.try_into().unwrap())
933 .build()
934 .unwrap(),
935 );
936
937 for word in words {
938 let (fp, index) = filter.get_fingerprint_and_index(&word);
939 let alt_index = filter.alt_index(&fp, index);
940 assert_eq!(index, filter.alt_index(&fp, alt_index));
941 }
942 }
943
944 #[test]
945 fn random_kicks() {
946 let filter = CuckooFilter::new(
947 CuckooConfiguration::builder(1000)
948 .bucket_size(2.try_into().unwrap())
949 .build()
950 .unwrap(),
951 TestHasher(0),
952 );
953
954 let test_item = PredefinedBucketItem(2 << 32);
955 filter.insert(&test_item);
956
957 let test_item_2 = PredefinedBucketItem(4 << 32);
958 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
961 filter.insert(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
964 filter.insert(&test_item_4); let test_item_unrelated = PredefinedBucketItem((10 << 32) + 10);
968 filter.insert(&test_item_unrelated);
969
970 assert!(filter.contains(&test_item));
972 assert!(filter.contains(&test_item_2));
973 assert!(filter.contains(&test_item_3));
974 assert!(filter.contains(&test_item_4));
975
976 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
977 let kicked = filter.insert(&test_item_5);
979 assert!(kicked.is_some(), "An item had to be kicked");
980 assert!(filter.contains(&test_item_5));
981 assert!(filter.contains(&test_item_unrelated));
982
983 for item in [&test_item, &test_item_2, &test_item_3, &test_item_4]
984 .iter()
985 .filter(|i| !kicked.as_ref().unwrap().matches_key(i, &filter))
986 {
987 assert!(filter.contains(item), "Only one item should be kicked");
988 }
989 }
990
991 #[test]
992 fn overriding_defaults() {
993 let filter = CuckooFilter::new_random(
994 CuckooConfiguration::builder(1000)
995 .with_ttl(TtlConfig {
996 ttl: 30.try_into().unwrap(),
997 ttl_bits: 8.try_into().unwrap(),
998 })
999 .with_counter(CounterConfig::default())
1000 .build()
1001 .unwrap(),
1002 );
1003
1004 filter.insert_with_defaults(
1005 "basic",
1006 InsertValues {
1007 ttl: Some(50),
1008 counter: Some(10),
1009 },
1010 );
1011
1012 assert!(filter.contains("basic"));
1013 assert_eq!(
1014 filter
1015 .get_associated_data("basic")
1016 .unwrap()
1017 .get_stored_ttl_value()
1018 .unwrap(),
1019 50
1020 );
1021 assert_eq!(
1022 filter
1023 .get_associated_data("basic")
1024 .unwrap()
1025 .get_counter()
1026 .unwrap(),
1027 13 );
1029 }
1030
1031 #[test]
1032 fn overriding_updates() {
1033 let filter = CuckooFilter::new_random(
1034 CuckooConfiguration::builder(1000)
1035 .with_ttl(TtlConfig {
1036 ttl: 30.try_into().unwrap(),
1037 ttl_bits: 8.try_into().unwrap(),
1038 })
1039 .with_counter(CounterConfig::default())
1040 .build()
1041 .unwrap(),
1042 );
1043
1044 filter.insert_with_defaults(
1045 "basic",
1046 InsertValues {
1047 ttl: Some(5),
1048 counter: Some(1),
1049 },
1050 );
1051
1052 assert!(filter.contains_with_update(
1053 "basic",
1054 LookupValues {
1055 ttl: Some(50),
1056 counter_diff: Some(10),
1057 },
1058 ));
1059 assert_eq!(
1060 filter
1061 .get_associated_data("basic")
1062 .unwrap()
1063 .get_stored_ttl_value()
1064 .unwrap(),
1065 50
1066 );
1067 assert_eq!(
1068 filter
1069 .get_associated_data("basic")
1070 .unwrap()
1071 .get_counter()
1072 .unwrap(),
1073 13 );
1075 }
1076
1077 #[test]
1078 fn scan_and_update_full() {
1079 let words = get_words(0..100_000);
1080 let filter = CuckooFilter::new_random(
1081 CuckooConfiguration::builder(100_000)
1082 .fingerprint_bits(32.try_into().unwrap())
1083 .with_lru(LruConfig::default())
1084 .with_ttl(TtlConfig {
1085 ttl: 3.try_into().unwrap(),
1086 ttl_bits: 2.try_into().unwrap(),
1087 })
1088 .build()
1089 .unwrap(),
1090 );
1091
1092 assert_eq!(filter.get_item_count(), 0);
1093
1094 let mut stored_words = HashSet::new();
1095
1096 for (index, word) in words.iter().enumerate() {
1097 stored_words.insert(word);
1098 if let Some(evicted_fp) = filter.insert(word) {
1099 words[0..=index]
1100 .iter()
1101 .filter(|w| evicted_fp.matches_key(w, &filter))
1102 .for_each(|evicted_word| {
1103 stored_words.remove(evicted_word);
1104 });
1105 }
1106 }
1107
1108 assert_eq!(filter.get_item_count(), stored_words.len());
1109
1110 for _ in 0..2 {
1111 assert_eq!(filter.scan_and_update_full(), 0);
1112 }
1113
1114 assert_eq!(filter.get_item_count(), stored_words.len());
1115 for word in stored_words.iter() {
1116 assert!(
1117 filter.contains(word),
1118 "Word: {word} expected in the filter, but not found"
1119 );
1120 }
1121
1122 assert_eq!(filter.scan_and_update_full(), stored_words.len());
1124 for word in &words {
1125 assert!(
1126 !filter.contains(word),
1127 "Filter contained {word}, but shouldn't have"
1128 );
1129 }
1130 assert_eq!(filter.get_item_count(), 0);
1131 }
1132
1133 #[test]
1134 fn scan_and_update_lru_deletion() {
1135 let words = get_words(0..100_000);
1136 let filter = CuckooFilter::new_random(
1137 CuckooConfiguration::builder(100_000)
1138 .fingerprint_bits(32.try_into().unwrap())
1139 .with_lru(LruConfig {
1140 counter_bits: 2.try_into().unwrap(),
1141 remove_on_zero: true,
1142 ..Default::default()
1143 })
1144 .build()
1145 .unwrap(),
1146 );
1147
1148 assert_eq!(filter.get_item_count(), 0);
1149
1150 let mut stored_words = HashSet::new();
1151
1152 for (index, word) in words.iter().enumerate() {
1153 stored_words.insert(word);
1154 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1155 words[0..=index]
1156 .iter()
1157 .filter(|w| evicted_fp.matches_key(w, &filter))
1158 .for_each(|evicted_word| {
1159 stored_words.remove(evicted_word);
1160 });
1161 }
1162 }
1163
1164 let mut kept_words = HashSet::new();
1165 for word in stored_words.clone().into_iter() {
1166 kept_words.insert(word);
1167 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1168 words
1169 .iter()
1170 .filter(|w| evicted_fp.matches_key(w, &filter))
1171 .for_each(|evicted_word| {
1172 stored_words.remove(evicted_word);
1173 kept_words.remove(evicted_word);
1174 });
1175 }
1176 }
1177
1178 assert_eq!(filter.get_item_count(), stored_words.len());
1179
1180 for _ in 0..2 {
1181 assert_eq!(filter.get_item_count(), stored_words.len());
1182 assert_eq!(filter.scan_and_update_full(), 0);
1183 }
1184
1185 assert_eq!(filter.get_item_count(), kept_words.len());
1186 for word in kept_words.iter() {
1187 assert!(
1188 filter.contains(word),
1189 "Word: {word} expected in the filter, but not found"
1190 );
1191 }
1192 for word in stored_words.difference(&kept_words) {
1193 assert!(
1194 !filter.contains(word),
1195 "Filter contained {word}, but shouldn't have"
1196 );
1197 }
1198
1199 assert_eq!(filter.scan_and_update_full(), 0);
1201
1202 assert_eq!(filter.scan_and_update_full(), kept_words.len());
1204 for word in &words {
1205 assert!(
1206 !filter.contains(word),
1207 "Filter contained {word}, but shouldn't have"
1208 );
1209 }
1210 assert_eq!(filter.get_item_count(), 0);
1211 }
1212
1213 #[test]
1214 fn scan_and_update_lru_deletion_decrement_strategy() {
1215 let words = get_words(0..100_000);
1216 let filter = CuckooFilter::new_random(
1217 CuckooConfiguration::builder(100_000)
1218 .fingerprint_bits(32.try_into().unwrap())
1219 .with_lru(LruConfig {
1220 counter_bits: 2.try_into().unwrap(),
1221 starting_value: 3,
1222 aging_strategy: crate::config::LruAgingStrategy::Decrement(1),
1223 remove_on_zero: true,
1224 ..Default::default()
1225 })
1226 .build()
1227 .unwrap(),
1228 );
1229
1230 assert_eq!(filter.get_item_count(), 0);
1231
1232 let mut stored_words = HashSet::new();
1233
1234 for (index, word) in words.iter().enumerate() {
1235 stored_words.insert(word);
1236 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1237 words[0..=index]
1238 .iter()
1239 .filter(|w| evicted_fp.matches_key(w, &filter))
1240 .for_each(|evicted_word| {
1241 stored_words.remove(evicted_word);
1242 });
1243 }
1244 }
1245
1246 assert_eq!(filter.get_item_count(), stored_words.len());
1247
1248 for _ in 0..3 {
1249 assert_eq!(filter.get_item_count(), stored_words.len());
1250 assert_eq!(filter.scan_and_update_full(), 0);
1251 }
1252
1253 assert_eq!(filter.get_item_count(), stored_words.len());
1254 for word in stored_words.iter() {
1255 assert!(
1256 filter.contains(word),
1257 "Word: {word} expected in the filter, but not found"
1258 );
1259 }
1260
1261 assert_eq!(filter.scan_and_update_full(), 0);
1263
1264 assert_eq!(filter.scan_and_update_full(), stored_words.len());
1266 for word in &words {
1267 assert!(
1268 !filter.contains(word),
1269 "Filter contained {word}, but shouldn't have"
1270 );
1271 }
1272 assert_eq!(filter.get_item_count(), 0);
1273 }
1274
1275 #[test]
1276 fn export_import() {
1277 let words = get_words(0..100_000);
1278 let filter = CuckooFilter::new_random_exportable(
1279 CuckooConfiguration::builder(100_000)
1280 .fingerprint_bits(32.try_into().unwrap())
1281 .with_lru(LruConfig::default())
1282 .with_ttl(TtlConfig {
1283 ttl: 3.try_into().unwrap(),
1284 ttl_bits: 2.try_into().unwrap(),
1285 })
1286 .build()
1287 .unwrap(),
1288 );
1289
1290 assert_eq!(filter.get_item_count(), 0);
1291
1292 let mut stored_words = HashSet::new();
1293
1294 for (index, word) in words.iter().enumerate() {
1295 stored_words.insert(word);
1296 if let Some(evicted_fp) = filter.insert(word) {
1297 words[0..=index]
1298 .iter()
1299 .filter(|w| evicted_fp.matches_key(w, &filter))
1300 .for_each(|evicted_word| {
1301 stored_words.remove(evicted_word);
1302 });
1303 }
1304 }
1305
1306 assert_eq!(filter.get_item_count(), stored_words.len());
1307
1308 let exported_buf = filter.exporter().snapshot().unwrap();
1309 let mut readable_buf = VecDeque::from(exported_buf);
1310
1311 let imported_filter = CuckooFilter::import_random_exportable(&mut readable_buf).unwrap();
1312
1313 assert_eq!(
1314 imported_filter.get_configuration(),
1315 filter.get_configuration()
1316 );
1317
1318 assert_eq!(imported_filter.get_item_count(), stored_words.len());
1319 for word in stored_words.iter() {
1320 assert!(
1321 imported_filter.contains(word),
1322 "Word: {word} expected in the filter, but not found"
1323 );
1324 }
1325 }
1326}