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, ExportableBuildHasher, ExportableRandomState, import_config,
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 = import_config(&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 for _ in 0..self.configuration.max_kicks {
343 {
344 let mut bucket = self.lock_bucket(cur_index as usize);
345 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
347 if !bucket.kick_lru(&mut cur_data_block, &self.configuration, lru_config) {
348 return Some(cur_data_block.get_fingerprint(&self.configuration));
349 }
350 } else {
351 bucket.kick_random(&mut cur_data_block, &self.configuration);
352 }
353 cur_index = self.alt_index(
354 &cur_data_block.get_fingerprint(&self.configuration),
355 cur_index,
356 );
357 }
358
359 if self
360 .lock_bucket(cur_index as usize)
361 .insert(&cur_data_block, &self.configuration)
362 {
363 self.items.fetch_add(1, Ordering::Relaxed);
364 return None;
366 }
367 }
368
369 Some(cur_data_block.get_fingerprint(&self.configuration))
371 }
372
373 pub fn insert<K: Hash + ?Sized>(&self, key: &K) -> Option<Fingerprint> {
385 self.insert_with_defaults(key, InsertValues::default())
386 }
387
388 pub fn insert_with_defaults<K: Hash + ?Sized>(
393 &self,
394 key: &K,
395 default: InsertValues,
396 ) -> Option<Fingerprint> {
397 let (fp, i1) = self.get_fingerprint_and_index(key);
398 let mut cur_data_block = self.new_data_block(&fp, default);
399
400 let inserted = self
401 .lock_bucket(i1 as usize)
402 .insert(&cur_data_block, &self.configuration);
403
404 if inserted {
405 self.items.fetch_add(1, Ordering::Relaxed);
406 return None;
407 }
408
409 let i2 = self.alt_index(&fp, i1);
410
411 let inserted = self
412 .lock_bucket(i2 as usize)
413 .insert(&cur_data_block, &self.configuration);
414
415 if inserted {
416 self.items.fetch_add(1, Ordering::Relaxed);
417 return None;
418 }
419
420 let mut cur_index = i1;
421 for _ in 0..self.configuration.max_kicks {
422 {
423 let mut bucket = self.lock_bucket(cur_index as usize);
424 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
426 if !bucket.kick_lru(&mut cur_data_block, &self.configuration, lru_config) {
427 return Some(cur_data_block.get_fingerprint(&self.configuration));
428 }
429 } else {
430 bucket.kick_random(&mut cur_data_block, &self.configuration);
432 }
433 cur_index = self.alt_index(
434 &cur_data_block.get_fingerprint(&self.configuration),
435 cur_index,
436 );
437 }
438
439 if self
440 .lock_bucket(cur_index as usize)
441 .insert(&cur_data_block, &self.configuration)
442 {
443 self.items.fetch_add(1, Ordering::Relaxed);
444 return None;
446 }
447 }
448
449 Some(cur_data_block.get_fingerprint(&self.configuration))
451 }
452
453 pub fn contains_with_update<K: Hash + ?Sized>(&self, key: &K, update: LookupValues) -> bool {
458 let (fp, i1) = self.get_fingerprint_and_index(key);
459
460 let mut contains =
461 self.lock_bucket(i1 as usize)
462 .contains(&fp, &self.configuration, &update);
463
464 if !contains {
465 let i2 = self.alt_index(&fp, i1);
466 contains = self
467 .lock_bucket(i2 as usize)
468 .contains(&fp, &self.configuration, &update);
469 }
470
471 contains
472 }
473
474 pub fn contains<K: Hash + ?Sized>(&self, key: &K) -> bool {
479 self.contains_with_update(key, LookupValues::default())
480 }
481
482 pub fn get_associated_data<K: Hash + ?Sized>(&self, key: &K) -> Option<AssociatedData> {
489 self.get_associated_data_with_update(key, LookupValues::default())
490 }
491
492 pub fn get_associated_data_with_update<K: Hash + ?Sized>(
497 &self,
498 key: &K,
499 update: LookupValues,
500 ) -> Option<AssociatedData> {
501 let (fp, i1) = self.get_fingerprint_and_index(key);
502
503 let mut contains =
504 self.lock_bucket(i1 as usize)
505 .get_associated_data(&fp, &self.configuration, &update);
506
507 if contains.is_none() {
508 let i2 = self.alt_index(&fp, i1);
509 contains = self.lock_bucket(i2 as usize).get_associated_data(
510 &fp,
511 &self.configuration,
512 &update,
513 );
514 }
515
516 contains
517 }
518
519 pub fn remove<K: Hash + ?Sized>(&self, key: &K) -> bool {
523 let (fp, i1) = self.get_fingerprint_and_index(key);
524
525 let mut removed = self
526 .lock_bucket(i1 as usize)
527 .remove(&fp, &self.configuration);
528
529 if !removed {
530 let i2 = self.alt_index(&fp, i1);
531 removed = self
532 .lock_bucket(i2 as usize)
533 .remove(&fp, &self.configuration);
534 }
535
536 if removed {
537 self.items.fetch_sub(1, Ordering::Relaxed);
538 }
539
540 removed
541 }
542
543 pub fn scan_and_update_full(&self) -> usize {
586 #[expect(clippy::unwrap_used)]
587 self.scan_and_update_full_partition(NonZeroUsize::new(1).unwrap(), 0)
588 }
589
590 pub fn scan_and_update_full_partition(
595 &self,
596 total_partitions: NonZeroUsize,
597 partition_index: usize,
598 ) -> usize {
599 if self.configuration.lru_field_config.is_none()
600 && self.configuration.ttl_field_config.is_none()
601 {
602 return 0;
603 }
604
605 let mut removed = 0;
606 let part_size = self.buckets.len().div_ceil(total_partitions.get());
607 if (partition_index * part_size) >= self.buckets.len() {
608 return 0;
609 }
610 for b in self.buckets
611 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
612 .iter()
613 {
614 #[expect(clippy::unwrap_used)]
615 let mut bucket = b.lock().unwrap();
616 if let Some(lru_config) = &self.configuration.lru_field_config {
617 removed += bucket.age_lru_counters(&self.configuration, lru_config)
618 }
619 if let Some(ttl_config) = &self.configuration.ttl_field_config {
620 removed += bucket.age_ttl_counters(&self.configuration, ttl_config);
621 }
622 }
623
624 if removed > 0 {
625 self.items.fetch_sub(removed, Ordering::Relaxed);
626 }
627 removed
628 }
629
630 pub fn scan_and_update_ttl(&self) -> usize {
637 #[expect(clippy::unwrap_used)]
638 self.scan_and_update_ttl_partition(NonZeroUsize::new(1).unwrap(), 0)
639 }
640
641 pub fn scan_and_update_ttl_partition(
646 &self,
647 total_partitions: NonZeroUsize,
648 partition_index: usize,
649 ) -> usize {
650 if self.configuration.ttl_field_config.is_none() {
651 return 0;
652 }
653
654 let mut removed = 0;
655 let part_size = self.buckets.len().div_ceil(total_partitions.get());
656 if (partition_index * part_size) >= self.buckets.len() {
657 return 0;
658 }
659 for b in self.buckets
660 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
661 .iter()
662 {
663 #[expect(clippy::unwrap_used)]
664 let mut bucket = b.lock().unwrap();
665 if let Some(ttl_config) = &self.configuration.ttl_field_config {
666 removed += bucket.age_ttl_counters(&self.configuration, ttl_config);
667 }
668 }
669
670 if removed > 0 {
671 self.items.fetch_sub(removed, Ordering::Relaxed);
672 }
673 removed
674 }
675
676 pub fn scan_and_update_lru(&self) -> usize {
683 #[expect(clippy::unwrap_used)]
684 self.scan_and_update_lru_partition(NonZeroUsize::new(1).unwrap(), 0)
685 }
686
687 pub fn scan_and_update_lru_partition(
692 &self,
693 total_partitions: NonZeroUsize,
694 partition_index: usize,
695 ) -> usize {
696 if self.configuration.lru_field_config.is_none() {
697 return 0;
698 }
699
700 let mut removed = 0;
701 let part_size = self.buckets.len().div_ceil(total_partitions.get());
702 if (partition_index * part_size) >= self.buckets.len() {
703 return removed;
704 }
705 for b in self.buckets
706 [partition_index * part_size..self.buckets.len().min((partition_index + 1) * part_size)]
707 .iter()
708 {
709 #[expect(clippy::unwrap_used)]
710 let mut bucket = b.lock().unwrap();
711 if let Some(lru_config) = &self.configuration.lru_field_config {
712 removed += bucket.age_lru_counters(&self.configuration, lru_config);
713 }
714 }
715
716 removed
717 }
718
719 pub(crate) fn get_fingerprint<K: Hash + ?Sized>(&self, key: &K) -> Fingerprint {
721 self.get_fingerprint_and_index(key).0
722 }
723
724 fn new_data_block(&self, fp: &Fingerprint, defaults: InsertValues) -> DataBlock<Vec<u8>> {
725 let data = vec![0u8; self.configuration.data_block_size];
726 let mut cur_data_block = DataBlock::from(data);
727 cur_data_block.store_fingerprint(fp, &self.configuration);
728
729 if let Some(ttl_config) = self.configuration.ttl_field_config.as_ref() {
730 cur_data_block.set_ttl(ttl_config, defaults.ttl.unwrap_or(ttl_config.0.ttl.into()));
731 }
732 if let Some(counter_config) = self.configuration.counter_field_config.as_ref() {
733 cur_data_block.update_counter(
734 counter_config,
735 defaults
736 .counter
737 .unwrap_or(counter_config.0.change_on_insert),
738 );
739 }
740 if let Some(lru_config) = self.configuration.lru_field_config.as_ref() {
741 cur_data_block.inc_lru_counter(lru_config);
742 }
743 cur_data_block
744 }
745
746 fn get_fingerprint_and_index<K: Hash + ?Sized>(&self, key: &K) -> (Fingerprint, u32) {
747 let result = self.build_hasher.hash_one(key);
748
749 let fingerprint = (result >> 32) as u32;
752 #[expect(clippy::cast_possible_truncation)]
754 let index = result as u32 & self.configuration.buckets_mask;
755
756 (
757 Fingerprint::new(
758 fingerprint,
759 self.configuration.fingerprint_field_config.value_mask(),
760 ),
761 index,
762 )
763 }
764
765 #[expect(clippy::cast_possible_truncation)]
767 fn alt_index(&self, fingerprint: &Fingerprint, index: u32) -> u32 {
768 let result = self.build_hasher.hash_one(fingerprint);
769
770 (index ^ ((result as u32) & self.configuration.buckets_mask))
771 & self.configuration.buckets_mask
772 }
773
774 #[expect(clippy::unwrap_used)]
775 fn lock_bucket(&self, index: usize) -> MutexGuard<'_, Bucket> {
776 self.buckets[index].lock().unwrap()
779 }
780}
781
782#[cfg(test)]
783#[expect(clippy::unwrap_used)]
784mod tests {
785 use std::{
786 collections::{HashSet, VecDeque},
787 hash::Hasher,
788 ops::Range,
789 };
790
791 use crate::config::{CounterConfig, LruConfig, TtlConfig};
792
793 use super::*;
794
795 fn get_words(range: Range<usize>) -> Vec<String> {
796 std::fs::read_to_string("/usr/share/dict/words")
797 .unwrap()
798 .split("\n")
799 .skip(range.start)
800 .take(range.len())
801 .map(ToString::to_string)
802 .collect()
803 }
804
805 #[test]
806 fn basic_insertion() {
807 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
808
809 filter.insert("basic");
810
811 assert!(filter.contains("basic"));
812 }
813
814 #[test]
815 fn basic_removal() {
816 let filter = CuckooFilter::new_random(CuckooConfiguration::builder(1000).build().unwrap());
817
818 filter.insert("basic");
819
820 assert!(filter.contains("basic"));
821
822 filter.remove("basic");
823
824 assert!(!filter.contains("basic"));
825 }
826
827 struct PredefinedBucketItem(u64);
828 struct TestHasher(u64);
829 impl BuildHasher for TestHasher {
830 type Hasher = TestHasher;
831
832 fn build_hasher(&self) -> Self::Hasher {
833 TestHasher(0)
834 }
835 }
836 impl Hasher for TestHasher {
837 fn finish(&self) -> u64 {
838 self.0
839 }
840
841 fn write(&mut self, bytes: &[u8]) {
842 if bytes.len() == 8 {
843 self.0 = u64::from_ne_bytes(bytes.try_into().unwrap());
844 } else {
845 self.0 = 1 - (u32::from_ne_bytes(bytes.try_into().unwrap()) as u64 % 2);
847 }
848 }
849 }
850 impl Hash for PredefinedBucketItem {
851 fn hash<H: Hasher>(&self, state: &mut H) {
852 state.write_u64(self.0);
853 }
854 }
855
856 #[test]
857 fn lru_insertion() {
858 let filter = CuckooFilter::new(
859 CuckooConfiguration::builder(1000)
860 .bucket_size(2.try_into().unwrap())
861 .with_lru(LruConfig {
862 counter_bits: 8.try_into().unwrap(),
863 remove_on_zero: false,
864 })
865 .build()
866 .unwrap(),
867 TestHasher(0),
868 );
869
870 let test_item = PredefinedBucketItem(2 << 32);
871 filter.insert(&test_item);
872 filter.contains(&test_item); let test_item_2 = PredefinedBucketItem(4 << 32);
875 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
878 filter.insert(&test_item_3); filter.contains(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
882 filter.insert(&test_item_4); assert!(filter.contains(&test_item));
886 assert!(filter.contains(&test_item_2));
887 assert!(filter.contains(&test_item_3));
888 assert!(filter.contains(&test_item_4));
889
890 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
891 filter.insert(&test_item_5);
893
894 assert!(filter.contains(&test_item_2));
895 assert!(filter.contains(&test_item));
896 assert!(filter.contains(&test_item_3));
897
898 assert!(
899 !filter.contains(&test_item_5) || !filter.contains(&test_item_4),
900 "No inserted items are missing, but filter can't hold them all"
901 );
902
903 filter.insert(&test_item_5);
905 filter.insert(&test_item_4);
906 assert!(filter.contains(&test_item));
907 assert!(filter.contains(&test_item_3));
908 }
909
910 #[test]
911 fn alt_index() {
912 let words = get_words(0..200_000);
913 let filter = CuckooFilter::new_random(
914 CuckooConfiguration::builder(200_000)
915 .fingerprint_bits(32.try_into().unwrap())
916 .build()
917 .unwrap(),
918 );
919
920 for word in words {
921 let (fp, index) = filter.get_fingerprint_and_index(&word);
922 let alt_index = filter.alt_index(&fp, index);
923 assert_eq!(index, filter.alt_index(&fp, alt_index));
924 }
925 }
926
927 #[test]
928 fn random_kicks() {
929 let filter = CuckooFilter::new(
930 CuckooConfiguration::builder(1000)
931 .bucket_size(2.try_into().unwrap())
932 .build()
933 .unwrap(),
934 TestHasher(0),
935 );
936
937 let test_item = PredefinedBucketItem(2 << 32);
938 filter.insert(&test_item);
939
940 let test_item_2 = PredefinedBucketItem(4 << 32);
941 filter.insert(&test_item_2); let test_item_3 = PredefinedBucketItem((3 << 32) + 2);
944 filter.insert(&test_item_3); let test_item_4 = PredefinedBucketItem((5 << 32) + 2);
947 filter.insert(&test_item_4); let test_item_unrelated = PredefinedBucketItem((10 << 32) + 10);
951 filter.insert(&test_item_unrelated);
952
953 assert!(filter.contains(&test_item));
955 assert!(filter.contains(&test_item_2));
956 assert!(filter.contains(&test_item_3));
957 assert!(filter.contains(&test_item_4));
958
959 let test_item_5 = PredefinedBucketItem((1 << 32) + 2);
960 let kicked = filter.insert(&test_item_5);
962 assert!(kicked.is_some(), "An item had to be kicked");
963 assert!(filter.contains(&test_item_5));
964 assert!(filter.contains(&test_item_unrelated));
965
966 for item in [&test_item, &test_item_2, &test_item_3, &test_item_4]
967 .iter()
968 .filter(|i| !kicked.as_ref().unwrap().matches_key(i, &filter))
969 {
970 assert!(filter.contains(item), "Only one item should be kicked");
971 }
972 }
973
974 #[test]
975 fn overriding_defaults() {
976 let filter = CuckooFilter::new_random(
977 CuckooConfiguration::builder(1000)
978 .with_ttl(TtlConfig {
979 ttl: 30.try_into().unwrap(),
980 ttl_bits: 8.try_into().unwrap(),
981 })
982 .with_counter(CounterConfig::default())
983 .build()
984 .unwrap(),
985 );
986
987 filter.insert_with_defaults(
988 "basic",
989 InsertValues {
990 ttl: Some(50),
991 counter: Some(10),
992 },
993 );
994
995 assert!(filter.contains("basic"));
996 assert_eq!(
997 filter
998 .get_associated_data("basic")
999 .unwrap()
1000 .get_stored_ttl_value()
1001 .unwrap(),
1002 50
1003 );
1004 assert_eq!(
1005 filter
1006 .get_associated_data("basic")
1007 .unwrap()
1008 .get_counter()
1009 .unwrap(),
1010 13 );
1012 }
1013
1014 #[test]
1015 fn overriding_updates() {
1016 let filter = CuckooFilter::new_random(
1017 CuckooConfiguration::builder(1000)
1018 .with_ttl(TtlConfig {
1019 ttl: 30.try_into().unwrap(),
1020 ttl_bits: 8.try_into().unwrap(),
1021 })
1022 .with_counter(CounterConfig::default())
1023 .build()
1024 .unwrap(),
1025 );
1026
1027 filter.insert_with_defaults(
1028 "basic",
1029 InsertValues {
1030 ttl: Some(5),
1031 counter: Some(1),
1032 },
1033 );
1034
1035 assert!(filter.contains_with_update(
1036 "basic",
1037 LookupValues {
1038 ttl: Some(50),
1039 counter_diff: Some(10),
1040 },
1041 ));
1042 assert_eq!(
1043 filter
1044 .get_associated_data("basic")
1045 .unwrap()
1046 .get_stored_ttl_value()
1047 .unwrap(),
1048 50
1049 );
1050 assert_eq!(
1051 filter
1052 .get_associated_data("basic")
1053 .unwrap()
1054 .get_counter()
1055 .unwrap(),
1056 13 );
1058 }
1059
1060 #[test]
1061 fn scan_and_update_full() {
1062 let words = get_words(0..100_000);
1063 let filter = CuckooFilter::new_random(
1064 CuckooConfiguration::builder(100_000)
1065 .fingerprint_bits(32.try_into().unwrap())
1066 .with_lru(LruConfig::default())
1067 .with_ttl(TtlConfig {
1068 ttl: 3.try_into().unwrap(),
1069 ttl_bits: 2.try_into().unwrap(),
1070 })
1071 .build()
1072 .unwrap(),
1073 );
1074
1075 assert_eq!(filter.get_item_count(), 0);
1076
1077 let mut stored_words = HashSet::new();
1078
1079 for (index, word) in words.iter().enumerate() {
1080 stored_words.insert(word);
1081 if let Some(evicted_fp) = filter.insert(word) {
1082 words[0..=index]
1083 .iter()
1084 .filter(|w| evicted_fp.matches_key(w, &filter))
1085 .for_each(|evicted_word| {
1086 stored_words.remove(evicted_word);
1087 });
1088 }
1089 }
1090
1091 assert_eq!(filter.get_item_count(), stored_words.len());
1092
1093 for _ in 0..2 {
1094 assert_eq!(filter.scan_and_update_full(), 0);
1095 }
1096
1097 assert_eq!(filter.get_item_count(), stored_words.len());
1098 for word in stored_words.iter() {
1099 assert!(
1100 filter.contains(word),
1101 "Word: {word} expected in the filter, but not found"
1102 );
1103 }
1104
1105 assert_eq!(filter.scan_and_update_full(), stored_words.len());
1107 for word in &words {
1108 assert!(
1109 !filter.contains(word),
1110 "Filter contained {word}, but shouldn't have"
1111 );
1112 }
1113 assert_eq!(filter.get_item_count(), 0);
1114 }
1115
1116 #[test]
1117 fn scan_and_update_lru_deletion() {
1118 let words = get_words(0..100_000);
1119 let filter = CuckooFilter::new_random(
1120 CuckooConfiguration::builder(100_000)
1121 .fingerprint_bits(32.try_into().unwrap())
1122 .with_lru(LruConfig {
1123 counter_bits: 2.try_into().unwrap(),
1124 remove_on_zero: true,
1125 })
1126 .build()
1127 .unwrap(),
1128 );
1129
1130 assert_eq!(filter.get_item_count(), 0);
1131
1132 let mut stored_words = HashSet::new();
1133
1134 for (index, word) in words.iter().enumerate() {
1135 stored_words.insert(word);
1136 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1137 words[0..=index]
1138 .iter()
1139 .filter(|w| evicted_fp.matches_key(w, &filter))
1140 .for_each(|evicted_word| {
1141 stored_words.remove(evicted_word);
1142 });
1143 }
1144 }
1145
1146 let mut kept_words = HashSet::new();
1147 for word in stored_words.clone().into_iter() {
1148 kept_words.insert(word);
1149 if let Some(evicted_fp) = filter.insert_if_not_present(word) {
1150 words
1151 .iter()
1152 .filter(|w| evicted_fp.matches_key(w, &filter))
1153 .for_each(|evicted_word| {
1154 stored_words.remove(evicted_word);
1155 kept_words.remove(evicted_word);
1156 });
1157 }
1158 }
1159
1160 assert_eq!(filter.get_item_count(), stored_words.len());
1161
1162 for _ in 0..2 {
1163 assert_eq!(filter.get_item_count(), stored_words.len());
1164 assert_eq!(filter.scan_and_update_full(), 0);
1165 }
1166
1167 assert_eq!(filter.get_item_count(), kept_words.len());
1168 for word in kept_words.iter() {
1169 assert!(
1170 filter.contains(word),
1171 "Word: {word} expected in the filter, but not found"
1172 );
1173 }
1174 for word in stored_words.difference(&kept_words) {
1175 assert!(
1176 !filter.contains(word),
1177 "Filter contained {word}, but shouldn't have"
1178 );
1179 }
1180
1181 assert_eq!(filter.scan_and_update_full(), 0);
1183
1184 assert_eq!(filter.scan_and_update_full(), kept_words.len());
1186 for word in &words {
1187 assert!(
1188 !filter.contains(word),
1189 "Filter contained {word}, but shouldn't have"
1190 );
1191 }
1192 assert_eq!(filter.get_item_count(), 0);
1193 }
1194
1195 #[test]
1196 fn export_import() {
1197 let words = get_words(0..100_000);
1198 let filter = CuckooFilter::new_random_exportable(
1199 CuckooConfiguration::builder(100_000)
1200 .fingerprint_bits(32.try_into().unwrap())
1201 .with_lru(LruConfig::default())
1202 .with_ttl(TtlConfig {
1203 ttl: 3.try_into().unwrap(),
1204 ttl_bits: 2.try_into().unwrap(),
1205 })
1206 .build()
1207 .unwrap(),
1208 );
1209
1210 assert_eq!(filter.get_item_count(), 0);
1211
1212 let mut stored_words = HashSet::new();
1213
1214 for (index, word) in words.iter().enumerate() {
1215 stored_words.insert(word);
1216 if let Some(evicted_fp) = filter.insert(word) {
1217 words[0..=index]
1218 .iter()
1219 .filter(|w| evicted_fp.matches_key(w, &filter))
1220 .for_each(|evicted_word| {
1221 stored_words.remove(evicted_word);
1222 });
1223 }
1224 }
1225
1226 assert_eq!(filter.get_item_count(), stored_words.len());
1227
1228 let exported_buf = filter.exporter().snapshot().unwrap();
1229 let mut readable_buf = VecDeque::from(exported_buf);
1230
1231 let imported_filter = CuckooFilter::import_random_exportable(&mut readable_buf).unwrap();
1232
1233 assert_eq!(
1234 imported_filter.get_configuration(),
1235 filter.get_configuration()
1236 );
1237
1238 assert_eq!(imported_filter.get_item_count(), stored_words.len());
1239 for word in stored_words.iter() {
1240 assert!(
1241 imported_filter.contains(word),
1242 "Word: {word} expected in the filter, but not found"
1243 );
1244 }
1245 }
1246}