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 removed
731 }
732
733 pub(crate) fn get_fingerprint<K: Hash + ?Sized>(&self, key: &K) -> Fingerprint {
735 self.get_fingerprint_and_index(key).0
736 }
737
738 fn new_data_block(&self, fp: &Fingerprint, defaults: InsertValues) -> DataBlock<Vec<u8>> {
739 let data = vec![0u8; self.configuration.data_block_size];
740 let mut cur_data_block = DataBlock::from(data);
741 cur_data_block.store_fingerprint(fp, &self.configuration);
742
743 if let Some(ttl_config) = self.configuration.ttl_field_config.as_ref() {
744 cur_data_block.set_ttl(ttl_config, defaults.ttl.unwrap_or(ttl_config.0.ttl.into()));
745 }
746 if let Some(counter_config) = self.configuration.counter_field_config.as_ref() {
747 cur_data_block.update_counter(
748 counter_config,
749 defaults
750 .counter
751 .unwrap_or(counter_config.0.change_on_insert),
752 );
753 }
754 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
755 cur_data_block.init_lru_counter(lru_config);
756 }
757 cur_data_block
758 }
759
760 fn get_fingerprint_and_index<K: Hash + ?Sized>(&self, key: &K) -> (Fingerprint, u32) {
761 let result = self.build_hasher.hash_one(key);
762
763 let fingerprint = (result >> 32) as u32;
766 #[expect(clippy::cast_possible_truncation)]
768 let index = result as u32 & self.configuration.buckets_mask;
769
770 (
771 Fingerprint::new(
772 fingerprint,
773 self.configuration.fingerprint_field_config.value_mask(),
774 ),
775 index,
776 )
777 }
778
779 #[expect(clippy::cast_possible_truncation)]
781 fn alt_index(&self, fingerprint: &Fingerprint, index: u32) -> u32 {
782 let result = self.build_hasher.hash_one(fingerprint);
783
784 (index ^ ((result as u32) & self.configuration.buckets_mask))
785 & self.configuration.buckets_mask
786 }
787
788 #[expect(clippy::unwrap_used)]
789 fn lock_bucket(&self, index: usize) -> MutexGuard<'_, Bucket> {
790 self.buckets[index].lock().unwrap()
793 }
794}
795
796#[cfg(test)]
797#[expect(clippy::unwrap_used)]
798mod tests {
799 use std::{
800 collections::{HashSet, VecDeque},
801 hash::Hasher,
802 ops::Range,
803 };
804
805 use crate::config::{CounterConfig, LruConfig, TtlConfig};
806
807 use super::*;
808
809 fn get_words(range: Range<usize>) -> Vec<String> {
810 std::fs::read_to_string("/usr/share/dict/words")
811 .unwrap()
812 .split("\n")
813 .skip(range.start)
814 .take(range.len())
815 .map(ToString::to_string)
816 .collect()
817 }
818
819 #[test]
820 fn basic_insertion() {
821 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
822
823 filter.insert("basic");
824
825 assert!(filter.contains("basic"));
826 }
827
828 #[test]
829 fn basic_removal() {
830 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
831
832 filter.insert("basic");
833
834 assert!(filter.contains("basic"));
835
836 filter.remove("basic");
837
838 assert!(!filter.contains("basic"));
839 }
840
841 struct PredefinedBucketItem(u64);
842 struct TestHasher(u64);
843 impl BuildHasher for TestHasher {
844 type Hasher = TestHasher;
845
846 fn build_hasher(&self) -> Self::Hasher {
847 TestHasher(0)
848 }
849 }
850 impl Hasher for TestHasher {
851 fn finish(&self) -> u64 {
852 self.0
853 }
854
855 fn write(&mut self, bytes: &[u8]) {
856 if bytes.len() == 8 {
857 self.0 = u64::from_ne_bytes(bytes.try_into().unwrap());
858 } else {
859 self.0 = 1 - (u32::from_ne_bytes(bytes.try_into().unwrap()) as u64 % 2);
861 }
862 }
863 }
864 impl Hash for PredefinedBucketItem {
865 fn hash<H: Hasher>(&self, state: &mut H) {
866 state.write_u64(self.0);
867 }
868 }
869
870 #[test]
871 fn lru_insertion() {
872 let filter = CuckooFilter::new(
873 CuckooConfiguration::builder(1000)
874 .bucket_size(2.try_into().unwrap())
875 .with_lru(LruConfig {
876 counter_bits: 8.try_into().unwrap(),
877 ..Default::default()
878 })
879 .build()
880 .unwrap(),
881 TestHasher(0),
882 );
883
884 let test_item = PredefinedBucketItem(2 << 32);
885 filter.insert(&test_item);
886 filter.contains(&test_item); let test_item_2 = PredefinedBucketItem(4 << 32);
889 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
892 filter.insert(&test_item_3); filter.contains(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
896 filter.insert(&test_item_4); assert!(filter.contains(&test_item));
900 assert!(filter.contains(&test_item_2));
901 assert!(filter.contains(&test_item_3));
902 assert!(filter.contains(&test_item_4));
903
904 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
905 filter.insert(&test_item_5);
907
908 assert!(filter.contains(&test_item_2));
909 assert!(filter.contains(&test_item));
910 assert!(filter.contains(&test_item_3));
911
912 assert!(
913 !filter.contains(&test_item_5) || !filter.contains(&test_item_4),
914 "No inserted items are missing, but filter can't hold them all"
915 );
916
917 filter.insert(&test_item_5);
919 filter.insert(&test_item_4);
920 assert!(filter.contains(&test_item));
921 assert!(filter.contains(&test_item_3));
922 }
923
924 #[test]
925 fn alt_index() {
926 let words = get_words(0..200_000);
927 let filter = CuckooFilter::new_random(
928 CuckooConfiguration::builder(200_000)
929 .fingerprint_bits(32.try_into().unwrap())
930 .build()
931 .unwrap(),
932 );
933
934 for word in words {
935 let (fp, index) = filter.get_fingerprint_and_index(&word);
936 let alt_index = filter.alt_index(&fp, index);
937 assert_eq!(index, filter.alt_index(&fp, alt_index));
938 }
939 }
940
941 #[test]
942 fn random_kicks() {
943 let filter = CuckooFilter::new(
944 CuckooConfiguration::builder(1000)
945 .bucket_size(2.try_into().unwrap())
946 .build()
947 .unwrap(),
948 TestHasher(0),
949 );
950
951 let test_item = PredefinedBucketItem(2 << 32);
952 filter.insert(&test_item);
953
954 let test_item_2 = PredefinedBucketItem(4 << 32);
955 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
958 filter.insert(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
961 filter.insert(&test_item_4); let test_item_unrelated = PredefinedBucketItem((10 << 32) + 10);
965 filter.insert(&test_item_unrelated);
966
967 assert!(filter.contains(&test_item));
969 assert!(filter.contains(&test_item_2));
970 assert!(filter.contains(&test_item_3));
971 assert!(filter.contains(&test_item_4));
972
973 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
974 let kicked = filter.insert(&test_item_5);
976 assert!(kicked.is_some(), "An item had to be kicked");
977 assert!(filter.contains(&test_item_5));
978 assert!(filter.contains(&test_item_unrelated));
979
980 for item in [&test_item, &test_item_2, &test_item_3, &test_item_4]
981 .iter()
982 .filter(|i| !kicked.as_ref().unwrap().matches_key(i, &filter))
983 {
984 assert!(filter.contains(item), "Only one item should be kicked");
985 }
986 }
987
988 #[test]
989 fn overriding_defaults() {
990 let filter = CuckooFilter::new_random(
991 CuckooConfiguration::builder(1000)
992 .with_ttl(TtlConfig {
993 ttl: 30.try_into().unwrap(),
994 ttl_bits: 8.try_into().unwrap(),
995 })
996 .with_counter(CounterConfig::default())
997 .build()
998 .unwrap(),
999 );
1000
1001 filter.insert_with_defaults(
1002 "basic",
1003 InsertValues {
1004 ttl: Some(50),
1005 counter: Some(10),
1006 },
1007 );
1008
1009 assert!(filter.contains("basic"));
1010 assert_eq!(
1011 filter
1012 .get_associated_data("basic")
1013 .unwrap()
1014 .get_stored_ttl_value()
1015 .unwrap(),
1016 50
1017 );
1018 assert_eq!(
1019 filter
1020 .get_associated_data("basic")
1021 .unwrap()
1022 .get_counter()
1023 .unwrap(),
1024 13 );
1026 }
1027
1028 #[test]
1029 fn overriding_updates() {
1030 let filter = CuckooFilter::new_random(
1031 CuckooConfiguration::builder(1000)
1032 .with_ttl(TtlConfig {
1033 ttl: 30.try_into().unwrap(),
1034 ttl_bits: 8.try_into().unwrap(),
1035 })
1036 .with_counter(CounterConfig::default())
1037 .build()
1038 .unwrap(),
1039 );
1040
1041 filter.insert_with_defaults(
1042 "basic",
1043 InsertValues {
1044 ttl: Some(5),
1045 counter: Some(1),
1046 },
1047 );
1048
1049 assert!(filter.contains_with_update(
1050 "basic",
1051 LookupValues {
1052 ttl: Some(50),
1053 counter_diff: Some(10),
1054 },
1055 ));
1056 assert_eq!(
1057 filter
1058 .get_associated_data("basic")
1059 .unwrap()
1060 .get_stored_ttl_value()
1061 .unwrap(),
1062 50
1063 );
1064 assert_eq!(
1065 filter
1066 .get_associated_data("basic")
1067 .unwrap()
1068 .get_counter()
1069 .unwrap(),
1070 13 );
1072 }
1073
1074 #[test]
1075 fn scan_and_update_full() {
1076 let words = get_words(0..100_000);
1077 let filter = CuckooFilter::new_random(
1078 CuckooConfiguration::builder(100_000)
1079 .fingerprint_bits(32.try_into().unwrap())
1080 .with_lru(LruConfig::default())
1081 .with_ttl(TtlConfig {
1082 ttl: 3.try_into().unwrap(),
1083 ttl_bits: 2.try_into().unwrap(),
1084 })
1085 .build()
1086 .unwrap(),
1087 );
1088
1089 assert_eq!(filter.get_item_count(), 0);
1090
1091 let mut stored_words = HashSet::new();
1092
1093 for (index, word) in words.iter().enumerate() {
1094 stored_words.insert(word);
1095 if let Some(evicted_fp) = filter.insert(word) {
1096 words[0..=index]
1097 .iter()
1098 .filter(|w| evicted_fp.matches_key(w, &filter))
1099 .for_each(|evicted_word| {
1100 stored_words.remove(evicted_word);
1101 });
1102 }
1103 }
1104
1105 assert_eq!(filter.get_item_count(), stored_words.len());
1106
1107 for _ in 0..2 {
1108 assert_eq!(filter.scan_and_update_full(), 0);
1109 }
1110
1111 assert_eq!(filter.get_item_count(), stored_words.len());
1112 for word in stored_words.iter() {
1113 assert!(
1114 filter.contains(word),
1115 "Word: {word} expected in the filter, but not found"
1116 );
1117 }
1118
1119 assert_eq!(filter.scan_and_update_full(), stored_words.len());
1121 for word in &words {
1122 assert!(
1123 !filter.contains(word),
1124 "Filter contained {word}, but shouldn't have"
1125 );
1126 }
1127 assert_eq!(filter.get_item_count(), 0);
1128 }
1129
1130 #[test]
1131 fn scan_and_update_lru_deletion() {
1132 let words = get_words(0..100_000);
1133 let filter = CuckooFilter::new_random(
1134 CuckooConfiguration::builder(100_000)
1135 .fingerprint_bits(32.try_into().unwrap())
1136 .with_lru(LruConfig {
1137 counter_bits: 2.try_into().unwrap(),
1138 remove_on_zero: true,
1139 ..Default::default()
1140 })
1141 .build()
1142 .unwrap(),
1143 );
1144
1145 assert_eq!(filter.get_item_count(), 0);
1146
1147 let mut stored_words = HashSet::new();
1148
1149 for (index, word) in words.iter().enumerate() {
1150 stored_words.insert(word);
1151 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1152 words[0..=index]
1153 .iter()
1154 .filter(|w| evicted_fp.matches_key(w, &filter))
1155 .for_each(|evicted_word| {
1156 stored_words.remove(evicted_word);
1157 });
1158 }
1159 }
1160
1161 let mut kept_words = HashSet::new();
1162 for word in stored_words.clone().into_iter() {
1163 kept_words.insert(word);
1164 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1165 words
1166 .iter()
1167 .filter(|w| evicted_fp.matches_key(w, &filter))
1168 .for_each(|evicted_word| {
1169 stored_words.remove(evicted_word);
1170 kept_words.remove(evicted_word);
1171 });
1172 }
1173 }
1174
1175 assert_eq!(filter.get_item_count(), stored_words.len());
1176
1177 for _ in 0..2 {
1178 assert_eq!(filter.get_item_count(), stored_words.len());
1179 assert_eq!(filter.scan_and_update_full(), 0);
1180 }
1181
1182 assert_eq!(filter.get_item_count(), kept_words.len());
1183 for word in kept_words.iter() {
1184 assert!(
1185 filter.contains(word),
1186 "Word: {word} expected in the filter, but not found"
1187 );
1188 }
1189 for word in stored_words.difference(&kept_words) {
1190 assert!(
1191 !filter.contains(word),
1192 "Filter contained {word}, but shouldn't have"
1193 );
1194 }
1195
1196 assert_eq!(filter.scan_and_update_full(), 0);
1198
1199 assert_eq!(filter.scan_and_update_full(), kept_words.len());
1201 for word in &words {
1202 assert!(
1203 !filter.contains(word),
1204 "Filter contained {word}, but shouldn't have"
1205 );
1206 }
1207 assert_eq!(filter.get_item_count(), 0);
1208 }
1209
1210 #[test]
1211 fn scan_and_update_lru_deletion_decrement_strategy() {
1212 let words = get_words(0..100_000);
1213 let filter = CuckooFilter::new_random(
1214 CuckooConfiguration::builder(100_000)
1215 .fingerprint_bits(32.try_into().unwrap())
1216 .with_lru(LruConfig {
1217 counter_bits: 2.try_into().unwrap(),
1218 starting_value: 3,
1219 aging_strategy: crate::config::LruAgingStrategy::Decrement(1),
1220 remove_on_zero: true,
1221 ..Default::default()
1222 })
1223 .build()
1224 .unwrap(),
1225 );
1226
1227 assert_eq!(filter.get_item_count(), 0);
1228
1229 let mut stored_words = HashSet::new();
1230
1231 for (index, word) in words.iter().enumerate() {
1232 stored_words.insert(word);
1233 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1234 words[0..=index]
1235 .iter()
1236 .filter(|w| evicted_fp.matches_key(w, &filter))
1237 .for_each(|evicted_word| {
1238 stored_words.remove(evicted_word);
1239 });
1240 }
1241 }
1242
1243 assert_eq!(filter.get_item_count(), stored_words.len());
1244
1245 for _ in 0..3 {
1246 assert_eq!(filter.get_item_count(), stored_words.len());
1247 assert_eq!(filter.scan_and_update_full(), 0);
1248 }
1249
1250 assert_eq!(filter.get_item_count(), stored_words.len());
1251 for word in stored_words.iter() {
1252 assert!(
1253 filter.contains(word),
1254 "Word: {word} expected in the filter, but not found"
1255 );
1256 }
1257
1258 assert_eq!(filter.scan_and_update_full(), 0);
1260
1261 assert_eq!(filter.scan_and_update_full(), stored_words.len());
1263 for word in &words {
1264 assert!(
1265 !filter.contains(word),
1266 "Filter contained {word}, but shouldn't have"
1267 );
1268 }
1269 assert_eq!(filter.get_item_count(), 0);
1270 }
1271
1272 #[test]
1273 fn export_import() {
1274 let words = get_words(0..100_000);
1275 let filter = CuckooFilter::new_random_exportable(
1276 CuckooConfiguration::builder(100_000)
1277 .fingerprint_bits(32.try_into().unwrap())
1278 .with_lru(LruConfig::default())
1279 .with_ttl(TtlConfig {
1280 ttl: 3.try_into().unwrap(),
1281 ttl_bits: 2.try_into().unwrap(),
1282 })
1283 .build()
1284 .unwrap(),
1285 );
1286
1287 assert_eq!(filter.get_item_count(), 0);
1288
1289 let mut stored_words = HashSet::new();
1290
1291 for (index, word) in words.iter().enumerate() {
1292 stored_words.insert(word);
1293 if let Some(evicted_fp) = filter.insert(word) {
1294 words[0..=index]
1295 .iter()
1296 .filter(|w| evicted_fp.matches_key(w, &filter))
1297 .for_each(|evicted_word| {
1298 stored_words.remove(evicted_word);
1299 });
1300 }
1301 }
1302
1303 assert_eq!(filter.get_item_count(), stored_words.len());
1304
1305 let exported_buf = filter.exporter().snapshot().unwrap();
1306 let mut readable_buf = VecDeque::from(exported_buf);
1307
1308 let imported_filter = CuckooFilter::import_random_exportable(&mut readable_buf).unwrap();
1309
1310 assert_eq!(
1311 imported_filter.get_configuration(),
1312 filter.get_configuration()
1313 );
1314
1315 assert_eq!(imported_filter.get_item_count(), stored_words.len());
1316 for word in stored_words.iter() {
1317 assert!(
1318 imported_filter.contains(word),
1319 "Word: {word} expected in the filter, but not found"
1320 );
1321 }
1322 }
1323}