#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
use fastbloom::BloomFilter;
use rand_core::Rng;
pub struct RotatingBloomFilter {
current: BloomFilter,
next: BloomFilter,
hashes: usize,
min_retention: usize,
}
impl RotatingBloomFilter {
pub fn new(false_pos: f64, min_retention: usize, csprng: &mut impl Rng) -> Self {
let mut seed_bytes = [0u8; 16];
csprng.fill_bytes(&mut seed_bytes);
let seed = u128::from_le_bytes(seed_bytes);
Self {
current: BloomFilter::with_false_pos(false_pos)
.seed(&seed)
.expected_items(min_retention * 2),
next: BloomFilter::with_false_pos(false_pos)
.seed(&seed)
.expected_items(min_retention * 2),
hashes: 0,
min_retention,
}
}
#[inline]
pub fn insert(&mut self, value: &(impl std::hash::Hash + ?Sized)) -> (bool, bool) {
if self.hashes >= self.min_retention {
std::mem::swap(&mut self.current, &mut self.next);
self.next.clear();
self.hashes = 0;
};
let hashed_value = self.current.source_hash(value);
let presence = (
self.current.insert_hash(hashed_value),
self.next.insert_hash(hashed_value),
);
self.hashes += 1;
presence
}
#[inline]
pub fn contains(&self, value: &(impl std::hash::Hash + ?Sized)) -> bool {
let hashed_value = self.current.source_hash(value);
self.current.contains_hash(hashed_value) || self.next.contains_hash(hashed_value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
fn seeded_filter(min_retention: usize) -> RotatingBloomFilter {
let mut rng = StdRng::seed_from_u64(42);
RotatingBloomFilter::new(1e-10, min_retention, &mut rng)
}
fn insert_dummy(filter: &mut RotatingBloomFilter, count: usize, offset: usize) {
for i in offset..offset + count {
filter.insert(&format!("dummy-{i}"));
}
}
proptest! {
#[test]
fn absent_on_initial_insert(value: String) {
let mut filter = seeded_filter(100);
let (in_current, in_next) = filter.insert(&value);
prop_assert!(!in_current);
prop_assert!(!in_next);
}
#[test]
fn present_on_reinsert(value: String) {
let mut filter = seeded_filter(100);
filter.insert(&value);
let (in_current, in_next) = filter.insert(&value);
prop_assert!(in_current);
prop_assert!(in_next);
}
#[test]
fn present_within_min_retention(min_retention in 10usize..200) {
let mut filter = seeded_filter(min_retention);
filter.insert("target");
insert_dummy(&mut filter, min_retention - 1, 0);
prop_assert!(filter.contains("target"));
}
#[test]
fn present_after_one_rotation(min_retention in 10usize..200) {
let mut filter = seeded_filter(min_retention);
filter.insert("target");
insert_dummy(&mut filter, min_retention, 0);
prop_assert!(filter.contains("target"));
}
#[test]
fn evicted_after_two_rotations(min_retention in 10usize..200) {
let mut filter = seeded_filter(min_retention);
filter.insert("target");
insert_dummy(&mut filter, min_retention * 2, 0);
prop_assert!(!filter.contains("target"));
}
#[test]
fn counter_resets_after_rotation(min_retention in 10usize..200) {
let mut filter = seeded_filter(min_retention);
insert_dummy(&mut filter, min_retention, 0);
filter.insert("post-rotation");
insert_dummy(&mut filter, min_retention - 1, min_retention);
prop_assert!(filter.contains("post-rotation"));
insert_dummy(&mut filter, 1, min_retention * 2);
prop_assert!(filter.contains("post-rotation"));
}
}
}