use crate::core::filter::{BloomFilter, SharedBloomFilter};
use crate::error::{BloomCraftError, Result};
use crate::filters::scalable::{
GrowthStrategy, InternalHasher, CAPACITY_WARNING_THRESHOLD, DEFAULT_FILL_THRESHOLD, MAX_FILTERS,
MIN_FPR,
};
use crate::filters::standard::StandardBloomFilter;
use crate::hash::{BloomHasher, StdHasher};
use std::fmt;
use std::hash::Hash;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
const CACHE_LINE_SIZE: usize = 64;
const CACHE_ALIGNED_USIZE_SIZE: usize = std::mem::size_of::<CacheAligned<AtomicUsize>>();
const CACHE_ALIGNED_BOOL_SIZE: usize = std::mem::size_of::<CacheAligned<AtomicBool>>();
const PAD_AFTER_USIZE: usize = CACHE_LINE_SIZE - CACHE_ALIGNED_USIZE_SIZE;
const PAD_AFTER_BOOL: usize = CACHE_LINE_SIZE - CACHE_ALIGNED_BOOL_SIZE;
#[repr(align(64))]
struct CacheAligned<T> {
value: T,
}
impl<T> CacheAligned<T> {
fn new(value: T) -> Self {
Self { value }
}
}
impl<T> std::ops::Deref for CacheAligned<T> {
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
struct ShardedFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
shards: Vec<StandardBloomFilter<T, H>>,
shard_count: usize,
design_fpr: f64,
}
impl<T, H> ShardedFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default,
{
fn new(capacity: usize, fpr: f64, shard_count: usize, hasher: H) -> Result<Self> {
let per_shard_capacity = (capacity + shard_count - 1) / shard_count;
let shards = (0..shard_count)
.map(|_| StandardBloomFilter::with_hasher(per_shard_capacity, fpr, hasher.clone()))
.collect::<Result<Vec<_>>>()?;
Ok(Self {
shards,
shard_count,
design_fpr: fpr,
})
}
#[inline]
fn insert(&self, item: &T) {
let shard_idx = InternalHasher::hash_one(item) as usize % self.shard_count;
self.shards[shard_idx].insert(item);
}
#[inline]
fn insert_into_shard(&self, shard_idx: usize, item: &T) {
self.shards[shard_idx].insert(item);
}
#[inline]
fn contains(&self, item: &T) -> bool {
let shard_idx = InternalHasher::hash_one(item) as usize % self.shard_count;
self.shards[shard_idx].contains(item)
}
fn fill_rate(&self) -> f64 {
if self.shards.is_empty() {
return 0.0;
}
let total_bits: usize = self.shards.iter().map(|s| s.bit_count()).sum();
let set_bits: usize = self.shards.iter().map(|s| s.count_set_bits()).sum();
if total_bits == 0 {
return 0.0;
}
set_bits as f64 / total_bits as f64
}
fn expected_items(&self) -> usize {
self.shards.iter().map(|s| s.expected_items()).sum()
}
fn memory_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ self.shards.iter().map(|s| s.memory_usage()).sum::<usize>()
}
fn bit_statistics(&self) -> (usize, usize, f64) {
let total_bits: usize = self.shards.iter().map(|s| s.bit_count()).sum();
let set_bits: usize = self.shards.iter().map(|s| s.count_set_bits()).sum();
let utilization = if total_bits > 0 {
set_bits as f64 / total_bits as f64 * 100.0
} else {
0.0
};
(total_bits, set_bits, utilization)
}
fn bit_count(&self) -> usize {
self.shards.iter().map(|s| s.bit_count()).sum()
}
fn count_set_bits(&self) -> usize {
self.shards.iter().map(|s| s.count_set_bits()).sum()
}
fn hash_count(&self) -> usize {
self.shards.first().map(|s| s.hash_count()).unwrap_or(0)
}
pub fn estimate_fpr(&self) -> f64 {
if self.shards.is_empty() {
return self.design_fpr;
}
let fpr_sum: f64 = self.shards.iter().map(|s| s.estimate_fpr()).sum();
fpr_sum / self.shard_count as f64
}
}
pub struct AtomicScalableBloomFilter<T, H = StdHasher>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
inner: Arc<AtomicScalableInner<T, H>>,
}
struct AtomicScalableInner<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
filters: RwLock<Vec<Arc<ShardedFilter<T, H>>>>,
filter_nonempty: [AtomicBool; MAX_FILTERS],
current_filter: CacheAligned<AtomicUsize>,
_pad1: [u8; PAD_AFTER_USIZE],
total_items: CacheAligned<AtomicUsize>,
_pad2: [u8; PAD_AFTER_USIZE],
growth_in_progress: CacheAligned<AtomicBool>,
_pad3: [u8; PAD_AFTER_BOOL],
check_interval: CacheAligned<AtomicUsize>,
config: ConcurrentConfig<H>,
}
struct ConcurrentConfig<H>
where
H: BloomHasher + Clone + Default,
{
initial_capacity: usize,
target_fpr: f64,
error_ratio: std::sync::atomic::AtomicU64,
fill_threshold: f64,
growth_strategy: GrowthStrategy,
hasher: H,
shard_count: usize,
}
impl<H: BloomHasher + Clone + Default> ConcurrentConfig<H> {
#[inline]
fn error_ratio(&self) -> f64 {
f64::from_bits(self.error_ratio.load(Ordering::Relaxed))
}
#[inline]
fn store_error_ratio(&self, v: f64) {
self.error_ratio.store(v.to_bits(), Ordering::Relaxed);
}
#[inline]
fn fill_threshold(&self) -> f64 {
self.fill_threshold
}
}
fn optimal_shard_count() -> usize {
std::thread::available_parallelism()
.map(|n| n.get().min(16))
.unwrap_or(8)
}
impl<T> AtomicScalableBloomFilter<T, StdHasher>
where
T: Hash + Send + Sync,
{
#[must_use]
pub fn new(initial_capacity: usize, target_fpr: f64) -> Result<Self> {
Ok(Self::with_hasher(initial_capacity, target_fpr, StdHasher::new())?)
}
#[must_use]
pub fn with_strategy(
initial_capacity: usize,
target_fpr: f64,
error_ratio: f64,
growth_strategy: GrowthStrategy,
) -> Result<Self> {
Self::with_strategy_and_hasher(
initial_capacity,
target_fpr,
error_ratio,
growth_strategy,
StdHasher::new(),
)
}
pub fn with_preallocated(
initial_capacity: usize,
target_fpr: f64,
estimated_total_items: usize,
) -> Result<Self> {
if estimated_total_items == 0 {
return Err(BloomCraftError::invalid_parameters(
"estimated_total_items must be greater than 0",
));
}
let filter = Self::new(initial_capacity, target_fpr)?;
if estimated_total_items <= initial_capacity {
return Ok(filter);
}
let estimated_filters = {
let mut count = 1usize;
let mut capacity_sum = initial_capacity;
while capacity_sum < estimated_total_items && count < MAX_FILTERS {
let term = initial_capacity
.saturating_mul(2usize.checked_pow(count as u32).unwrap_or(usize::MAX));
capacity_sum = capacity_sum.saturating_add(term);
count += 1;
}
count.min(MAX_FILTERS)
};
let mut new_filters: Vec<Arc<ShardedFilter<T, StdHasher>>> =
Vec::with_capacity(estimated_filters.saturating_sub(1));
for i in 1..estimated_filters {
let capacity = filter.calculate_next_capacity(i)?;
let fpr = filter.calculate_next_fpr(i);
let f = Arc::new(ShardedFilter::new(
capacity,
fpr,
filter.inner.config.shard_count,
filter.inner.config.hasher.clone(),
)?);
new_filters.push(f);
}
{
let mut filters = filter.inner.filters.write().unwrap();
for f in new_filters {
if filters.len() < MAX_FILTERS {
filters.push(f);
}
}
}
Ok(filter)
}
}
impl<T, H> AtomicScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
#[must_use]
pub fn with_hasher(initial_capacity: usize, target_fpr: f64, hasher: H) -> Result<Self> {
Self::with_strategy_and_hasher(
initial_capacity,
target_fpr,
0.5,
GrowthStrategy::Geometric(2.0),
hasher,
)
}
#[must_use]
pub fn with_strategy_and_hasher(
initial_capacity: usize,
target_fpr: f64,
error_ratio: f64,
growth_strategy: GrowthStrategy,
hasher: H,
) -> Result<Self> {
growth_strategy.validate()?;
if initial_capacity == 0 {
return Err(BloomCraftError::invalid_item_count(initial_capacity));
}
if target_fpr <= 0.0 || target_fpr >= 1.0 {
return Err(BloomCraftError::fp_rate_out_of_bounds(target_fpr));
}
if error_ratio <= 0.0 || error_ratio >= 1.0 {
return Err(BloomCraftError::invalid_parameters(format!(
"error_ratio must be in (0.0, 1.0), got {}",
error_ratio
)));
}
let shard_count = optimal_shard_count();
let initial_filter = Arc::new(ShardedFilter::new(
initial_capacity,
target_fpr,
shard_count,
hasher.clone(),
)?);
let initial_check_interval =
(initial_capacity as f64 * DEFAULT_FILL_THRESHOLD) as usize;
let config = ConcurrentConfig {
initial_capacity,
target_fpr,
error_ratio: std::sync::atomic::AtomicU64::new(error_ratio.to_bits()),
fill_threshold: DEFAULT_FILL_THRESHOLD,
growth_strategy,
hasher,
shard_count,
};
let inner = Arc::new(AtomicScalableInner {
filters: RwLock::new(vec![initial_filter]),
filter_nonempty: std::array::from_fn(|_| AtomicBool::new(false)),
current_filter: CacheAligned::new(AtomicUsize::new(0)),
_pad1: [0; PAD_AFTER_USIZE],
total_items: CacheAligned::new(AtomicUsize::new(0)),
_pad2: [0; PAD_AFTER_USIZE],
growth_in_progress: CacheAligned::new(AtomicBool::new(false)),
_pad3: [0; PAD_AFTER_BOOL],
check_interval: CacheAligned::new(AtomicUsize::new(initial_check_interval)),
config,
});
Ok(Self { inner })
}
pub fn insert(&self, item: &T) {
loop {
let current_idx = self.inner.current_filter.load(Ordering::Acquire);
let filter = {
let filters = self.inner.filters.read().unwrap();
filters.get(current_idx).map(Arc::clone)
};
if let Some(sharded_filter) = filter {
sharded_filter.insert(item);
self.inner.filter_nonempty[current_idx].store(true, Ordering::Relaxed);
break;
}
std::thread::yield_now();
}
self.inner.total_items.fetch_add(1, Ordering::Relaxed);
let total = self.inner.total_items.load(Ordering::Relaxed);
let next_threshold = self.inner.check_interval.load(Ordering::Acquire);
if total >= next_threshold {
self.try_grow();
}
}
#[must_use]
#[inline]
pub fn contains(&self, item: &T) -> bool {
let filters = self.inner.filters.read().unwrap();
for (idx, filter) in filters.iter().enumerate().rev() {
if !self.inner.filter_nonempty[idx].load(Ordering::Relaxed) {
continue;
}
if filter.contains(item) {
return true;
}
}
false
}
pub fn insert_batch(&self, items: &[T]) -> Result<()> {
if items.is_empty() {
return Ok(());
}
let current = self.inner.total_items.load(Ordering::Relaxed);
current
.checked_add(items.len())
.ok_or_else(|| {
BloomCraftError::invalid_parameters(format!(
"Batch insert of {} items would overflow counter (current: {})",
items.len(),
current
))
})?;
let shard_count = self.inner.config.shard_count;
let mut shard_buckets: Vec<Vec<&T>> = vec![Vec::new(); shard_count];
for item in items {
let shard_idx = InternalHasher::hash_one(item) as usize % shard_count;
shard_buckets[shard_idx].push(item);
}
for (shard_idx, bucket) in shard_buckets.iter().enumerate() {
if bucket.is_empty() {
continue;
}
let current_idx = self.inner.current_filter.load(Ordering::Acquire);
let filter = {
let filters = self.inner.filters.read().unwrap();
filters.get(current_idx).map(Arc::clone)
};
if let Some(sharded) = filter {
for item in bucket {
sharded.insert_into_shard(shard_idx, item);
}
self.inner.filter_nonempty[current_idx].store(true, Ordering::Relaxed);
}
let total = self.inner
.total_items
.fetch_add(bucket.len(), Ordering::Relaxed)
+ bucket.len();
if total >= self.inner.check_interval.load(Ordering::Acquire) {
self.try_grow();
}
}
Ok(())
}
#[must_use]
pub fn contains_batch(&self, items: &[T]) -> Vec<bool> {
items.iter().map(|item| self.contains(item)).collect()
}
pub fn clear_checked(&self) -> Result<()> {
let replacement = Arc::new(ShardedFilter::new(
self.inner.config.initial_capacity,
self.inner.config.target_fpr,
self.inner.config.shard_count,
self.inner.config.hasher.clone(),
)?);
let mut filters = self.inner.filters.write().unwrap();
filters.clear();
filters.push(replacement);
self.inner.current_filter.store(0, Ordering::Release);
self.inner.total_items.store(0, Ordering::Release);
for flag in &self.inner.filter_nonempty {
flag.store(false, Ordering::Relaxed);
}
if let GrowthStrategy::Adaptive { initial_ratio, .. } =
self.inner.config.growth_strategy
{
self.inner.config.store_error_ratio(initial_ratio);
}
let initial_check_interval = (self.inner.config.initial_capacity as f64
* self.inner.config.fill_threshold) as usize;
self.inner
.check_interval
.store(initial_check_interval.max(1), Ordering::Release);
drop(filters);
Ok(())
}
pub fn clear(&self) {
self.clear_checked()
.expect("AtomicScalableBloomFilter::clear() failed to recreate initial filter")
}
fn try_grow(&self) {
if self.inner.growth_in_progress.load(Ordering::Relaxed) {
return;
}
if self
.inner
.growth_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
.is_err()
{
return;
}
let result = self.perform_growth();
self.inner
.growth_in_progress
.store(false, Ordering::Release);
if let Err(_e) = result {
#[cfg(debug_assertions)]
eprintln!("[AtomicScalableBloomFilter] Growth failed: {}", _e);
}
}
fn perform_growth(&self) -> Result<()> {
let (current_idx, have_preallocated) = {
let filters = self.inner.filters.read().unwrap();
let idx = self.inner.current_filter.load(Ordering::Acquire);
let next_index = idx + 1;
let have_preallocated = next_index < filters.len();
if !have_preallocated && filters.len() >= MAX_FILTERS {
self.inner
.check_interval
.store(usize::MAX, Ordering::Release);
return Err(BloomCraftError::capacity_exceeded(
MAX_FILTERS,
filters.len(),
));
}
(idx, have_preallocated)
};
if have_preallocated {
let filters = self.inner.filters.write().unwrap();
if self.inner.current_filter.load(Ordering::Relaxed) != current_idx {
return Ok(());
}
let next_index = current_idx + 1;
let usable = (filters[next_index].expected_items() as f64
* self.inner.config.fill_threshold()) as usize;
let cur_total = self.inner.total_items.load(Ordering::Relaxed);
self.inner.check_interval.store(
cur_total.saturating_add(usable.max(1)),
Ordering::Release,
);
self.inner
.current_filter
.store(next_index, Ordering::Release);
#[cfg(debug_assertions)]
eprintln!(
"[AtomicScalableBloomFilter] Advanced to pre-allocated filter {} (no allocation)",
next_index
);
return Ok(());
}
let filter_index = self.inner.filters.read().unwrap().len();
let capacity = self.calculate_next_capacity(filter_index)?;
let fpr = self.calculate_next_fpr(filter_index);
let new_filter = Arc::new(ShardedFilter::new(
capacity,
fpr,
self.inner.config.shard_count,
self.inner.config.hasher.clone(),
)?);
let next_threshold;
{
let mut filters = self.inner.filters.write().unwrap();
if self.inner.current_filter.load(Ordering::Relaxed) != current_idx {
return Ok(());
}
filters.push(new_filter);
self.inner
.current_filter
.store(filter_index, Ordering::Release);
if let GrowthStrategy::Adaptive {
min_ratio, max_ratio, ..
} = self.inner.config.growth_strategy
{
let just_filled_idx = filters.len().saturating_sub(2);
if let Some(filled) = filters.get(just_filled_idx) {
let actual_fill = filled.fill_rate();
let current = self.inner.config.error_ratio();
let updated = if actual_fill > self.inner.config.fill_threshold() * 1.2 {
(current * 0.9).max(min_ratio)
} else if actual_fill < self.inner.config.fill_threshold() * 0.8 {
(current * 1.1).min(max_ratio)
} else {
current
};
self.inner.config.store_error_ratio(updated);
}
}
let new_filter_usable = filters
.last()
.map(|f| {
(f.expected_items() as f64 * self.inner.config.fill_threshold()) as usize
})
.unwrap_or(self.inner.config.initial_capacity);
let cur_total = self.inner.total_items.load(Ordering::Relaxed);
next_threshold = cur_total.saturating_add(new_filter_usable.max(1));
self.inner
.check_interval
.store(next_threshold, Ordering::Release);
}
#[cfg(debug_assertions)]
eprintln!(
"[AtomicScalableBloomFilter] Grew to {} filters (capacity: {}, FPR: {:.6}, interval: {})",
filter_index + 1,
capacity,
fpr,
next_threshold
);
Ok(())
}
fn calculate_next_capacity(&self, filter_index: usize) -> Result<usize> {
const MAX_CAPACITY: f64 = usize::MAX as f64;
let capacity = match self.inner.config.growth_strategy {
GrowthStrategy::Constant => self.inner.config.initial_capacity,
GrowthStrategy::Geometric(scale) => {
if filter_index == 0 {
self.inner.config.initial_capacity
} else {
let scale_log = scale.ln();
let max_safe_exp = (MAX_CAPACITY.ln()
- (self.inner.config.initial_capacity as f64).ln())
/ scale_log;
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Filter index {} would cause capacity overflow (max safe: {:.1})",
filter_index, max_safe_exp
)));
}
let growth_factor = scale.powi(filter_index as i32);
let computed = self.inner.config.initial_capacity as f64 * growth_factor;
if computed > MAX_CAPACITY || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(format!(
"Computed capacity {:.2e} exceeds usize::MAX",
computed
)));
}
let new_capacity = computed as usize;
if new_capacity < self.inner.config.initial_capacity {
return Err(BloomCraftError::invalid_parameters(
"Capacity calculation resulted in overflow",
));
}
new_capacity
}
}
GrowthStrategy::Bounded {
scale,
max_filter_size,
} => {
if filter_index == 0 {
self.inner.config.initial_capacity
} else {
let max_safe_exp = (MAX_CAPACITY.ln()
- (self.inner.config.initial_capacity as f64).ln())
/ scale.ln();
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Bounded filter index {} would cause capacity overflow (max safe: {:.1})",
filter_index, max_safe_exp
)));
}
let computed = self.inner.config.initial_capacity as f64
* scale.powi(filter_index as i32);
if computed > MAX_CAPACITY || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Bounded capacity calculation overflow",
));
}
let geometric = computed as usize;
geometric.min(max_filter_size)
}
}
GrowthStrategy::Adaptive { .. } => {
if filter_index == 0 {
self.inner.config.initial_capacity
} else {
const SCALE: f64 = 2.0;
let max_safe_exp = (MAX_CAPACITY.ln()
- (self.inner.config.initial_capacity as f64).ln())
/ SCALE.ln();
if filter_index as f64 >= max_safe_exp {
return Err(BloomCraftError::invalid_parameters(format!(
"Adaptive filter index {} would cause capacity overflow",
filter_index
)));
}
let computed = self.inner.config.initial_capacity as f64
* SCALE.powi(filter_index as i32);
if computed > MAX_CAPACITY || !computed.is_finite() {
return Err(BloomCraftError::invalid_parameters(
"Adaptive capacity overflow",
));
}
let new_cap = computed as usize;
if new_cap < self.inner.config.initial_capacity {
return Err(BloomCraftError::invalid_parameters(
"Adaptive capacity wraparound",
));
}
new_cap
}
}
};
Ok(capacity)
}
fn calculate_next_fpr(&self, filter_index: usize) -> f64 {
let ratio = self.inner.config.error_ratio();
const MAX_SAFE_EXP: i32 = 1000;
let safe_index = (filter_index as i32).min(MAX_SAFE_EXP);
let raw_fpr = self.inner.config.target_fpr * ratio.powi(safe_index);
raw_fpr.max(MIN_FPR).min(1.0)
}
#[must_use]
pub fn filter_count(&self) -> usize {
self.inner.filters.read().unwrap().len()
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.total_items.load(Ordering::Relaxed)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn total_capacity(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters.iter().map(|f| f.expected_items()).sum()
}
#[must_use]
pub fn is_at_max_capacity(&self) -> bool {
self.filter_count() >= MAX_FILTERS
}
#[must_use]
pub fn is_near_capacity(&self) -> bool {
self.filter_count() + CAPACITY_WARNING_THRESHOLD >= MAX_FILTERS
}
#[must_use]
pub fn estimate_fpr(&self) -> f64 {
let filters = self.inner.filters.read().unwrap();
let product: f64 = filters
.iter()
.map(|f| 1.0 - f.estimate_fpr())
.product();
1.0 - product
}
#[must_use]
pub fn memory_usage(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters.iter().map(|f| f.memory_usage()).sum::<usize>()
+ std::mem::size_of::<Self>()
}
#[must_use]
pub fn current_fill_rate(&self) -> f64 {
let filters = self.inner.filters.read().unwrap();
let current_idx = self.inner.current_filter.load(Ordering::Acquire);
filters
.get(current_idx)
.map(|f| f.fill_rate())
.unwrap_or(0.0)
}
#[must_use]
pub fn bit_statistics(&self) -> (usize, usize, f64) {
let filters = self.inner.filters.read().unwrap();
let mut total_bits = 0;
let mut set_bits = 0;
for filter in filters.iter() {
let (t, s, _) = filter.bit_statistics();
total_bits += t;
set_bits += s;
}
let utilization = if total_bits > 0 {
(set_bits as f64 / total_bits as f64) * 100.0
} else {
0.0
};
(total_bits, set_bits, utilization)
}
#[must_use]
pub fn shard_count(&self) -> usize {
self.inner.config.shard_count
}
#[must_use]
pub fn count_set_bits(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters.iter().map(|f| f.count_set_bits()).sum()
}
#[must_use]
pub fn bit_count(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters.iter().map(|f| f.bit_count()).sum()
}
#[must_use]
pub fn hash_count(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters
.first()
.map(|f| f.hash_count())
.unwrap_or(0)
}
#[must_use]
pub fn expected_items(&self) -> usize {
let filters = self.inner.filters.read().unwrap();
filters.iter().map(|f| f.expected_items()).sum()
}
#[must_use]
pub fn estimate_count(&self) -> usize {
let total_bits = self.bit_count();
let set_bits = self.count_set_bits();
if set_bits == 0 || total_bits == 0 {
return 0;
}
let m = total_bits as f64;
let k = self.hash_count() as f64;
if k == 0.0 {
return 0;
}
let fill_ratio = set_bits as f64 / m;
if fill_ratio >= 1.0 {
return total_bits;
}
(-(m / k) * (1.0 - fill_ratio).ln()).round() as usize
}
#[must_use]
pub fn false_positive_rate(&self) -> f64 {
self.estimate_fpr()
}
}
impl<T, H> SharedBloomFilter<T> for AtomicScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
fn insert(&self, item: &T) {
AtomicScalableBloomFilter::insert(self, item);
}
fn contains(&self, item: &T) -> bool {
AtomicScalableBloomFilter::contains(self, item)
}
fn clear(&self) {
AtomicScalableBloomFilter::clear(self);
}
fn len(&self) -> usize {
self.count_set_bits()
}
fn is_empty(&self) -> bool {
self.count_set_bits() == 0
}
fn false_positive_rate(&self) -> f64 {
self.estimate_fpr()
}
fn estimate_count(&self) -> usize {
AtomicScalableBloomFilter::estimate_count(self)
}
fn expected_items(&self) -> usize {
self.expected_items()
}
fn bit_count(&self) -> usize {
AtomicScalableBloomFilter::bit_count(self)
}
fn hash_count(&self) -> usize {
self.hash_count()
}
fn count_set_bits(&self) -> usize {
AtomicScalableBloomFilter::count_set_bits(self)
}
}
impl<T, H> Clone for AtomicScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<T, H> fmt::Display for AtomicScalableBloomFilter<T, H>
where
T: Hash + Send + Sync,
H: BloomHasher + Clone + Default + Send + Sync,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AtomicScalableBloomFilter {{ filters: {}, capacity: {}, items: {}, est_fpr: {:.4}% }}",
self.filter_count(),
self.total_capacity(),
self.len(),
self.estimate_fpr() * 100.0
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Barrier;
use std::thread;
#[test]
fn test_new() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
assert_eq!(f.filter_count(), 1);
}
#[test]
fn test_insert_and_contains() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
f.insert(&42);
assert!(f.contains(&42));
assert_eq!(f.len(), 1);
}
#[test]
fn test_no_false_negatives() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
for i in 0..500u64 {
f.insert(&i);
}
for i in 0..500u64 {
assert!(f.contains(&i), "false negative for {}", i);
}
}
#[test]
fn test_len_is_empty() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
assert!(f.is_empty());
assert_eq!(f.len(), 0);
f.insert(&1);
assert!(!f.is_empty());
assert_eq!(f.len(), 1);
}
#[test]
fn test_clear() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
for i in 0..100u64 {
f.insert(&i);
}
assert_eq!(f.len(), 100);
f.clear();
assert_eq!(f.len(), 0);
assert!(f.is_empty());
assert_eq!(f.filter_count(), 1);
}
#[test]
fn test_concurrent_insert() {
let f = Arc::new(AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap());
let threads: Vec<_> = (0..4)
.map(|tid| {
let f = Arc::clone(&f);
thread::spawn(move || {
for i in 0..250 {
f.insert(&(tid * 250 + i));
}
})
})
.collect();
for h in threads {
h.join().unwrap();
}
assert_eq!(f.len(), 1000);
for i in 0..1000u64 {
assert!(f.contains(&i), "false negative for {}", i);
}
}
#[test]
fn test_concurrent_contains_during_insert() {
let f = Arc::new(AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap());
for i in 0..200u64 {
f.insert(&i);
}
let ready = Arc::new(Barrier::new(5));
let stop = Arc::new(AtomicBool::new(false));
let readers: Vec<_> = (0..4)
.map(|_| {
let f = Arc::clone(&f);
let ready = Arc::clone(&ready);
let stop = Arc::clone(&stop);
thread::spawn(move || {
ready.wait();
let mut i = 0u64;
while !stop.load(Ordering::Relaxed) {
let _ = f.contains(&(i % 200));
i = i.wrapping_add(1);
}
})
})
.collect();
let writer = {
let f = Arc::clone(&f);
let ready = Arc::clone(&ready);
thread::spawn(move || {
ready.wait();
for i in 200..1000u64 {
f.insert(&i);
}
})
};
writer.join().unwrap();
stop.store(true, Ordering::Relaxed);
for h in readers {
h.join().unwrap();
}
assert_eq!(f.len(), 1000);
for i in 0..1000u64 {
assert!(f.contains(&i), "false negative for {}", i);
}
}
#[test]
fn test_clear_under_concurrent_readers() {
let f = Arc::new(AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap());
for i in 0..500u64 {
f.insert(&i);
}
let stop = Arc::new(AtomicBool::new(false));
let barrier = Arc::new(Barrier::new(3));
let readers: Vec<_> = (0..2)
.map(|_| {
let f = Arc::clone(&f);
let stop = Arc::clone(&stop);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
let mut i = 0u64;
while !stop.load(Ordering::Relaxed) {
let _ = f.contains(&(i % 500));
i = i.wrapping_add(1);
}
})
})
.collect();
barrier.wait();
f.clear();
stop.store(true, Ordering::Relaxed);
for h in readers {
h.join().unwrap();
}
assert_eq!(f.len(), 0);
f.insert(&99);
assert!(f.contains(&99));
}
#[test]
fn test_automatic_growth() {
let f = AtomicScalableBloomFilter::<u64>::new(100, 0.01).unwrap();
for i in 0..120u64 {
f.insert(&i);
}
assert!(f.filter_count() >= 2, "expected growth, got {} filters", f.filter_count());
for i in 0..120u64 {
assert!(f.contains(&i), "false negative after growth for {}", i);
}
}
#[test]
fn test_growth_preserves_items() {
let f = AtomicScalableBloomFilter::<u64>::new(100, 0.01).unwrap();
for i in 0..10_000u64 {
f.insert(&i);
}
for i in 0..10_000u64 {
assert!(f.contains(&i), "false negative after growth for {}", i);
}
}
#[test]
fn test_capacity_exhausted_error() {
let strategies = [
GrowthStrategy::Geometric(1.001),
GrowthStrategy::Constant,
];
for &strategy in &strategies {
let f = AtomicScalableBloomFilter::<u64>::with_strategy(
1, 0.5, 0.9, strategy,
)
.unwrap();
for i in 0..200u64 {
f.insert(&i);
}
assert!(f.contains(&0));
assert!(f.contains(&199));
}
}
#[test]
fn test_new_invalid_parameters() {
assert!(AtomicScalableBloomFilter::<u64>::new(0, 0.01).is_err());
assert!(AtomicScalableBloomFilter::<u64>::new(1_000, 0.0).is_err());
assert!(AtomicScalableBloomFilter::<u64>::new(1_000, 1.0).is_err());
assert!(AtomicScalableBloomFilter::<u64>::new(1_000, -0.01).is_err());
}
#[test]
fn test_with_strategy() {
let strategies = [
(GrowthStrategy::Constant, 0.5),
(GrowthStrategy::Geometric(2.0), 0.5),
(GrowthStrategy::Geometric(1.5), 0.5),
(
GrowthStrategy::Bounded {
scale: 2.0,
max_filter_size: 10_000,
},
0.5,
),
(
GrowthStrategy::Adaptive {
initial_ratio: 0.5,
min_ratio: 0.3,
max_ratio: 0.9,
},
0.5,
),
];
for &(strategy, error_ratio) in &strategies {
let f = AtomicScalableBloomFilter::<u64>::with_strategy(
1_000, 0.01, error_ratio, strategy,
)
.unwrap();
f.insert(&42);
assert!(f.contains(&42));
assert!(f.filter_count() >= 1);
}
}
#[test]
fn test_with_preallocated() {
let f = AtomicScalableBloomFilter::<u64>::with_preallocated(
1_000, 0.01, 10_000,
)
.unwrap();
assert!(f.filter_count() >= 1);
for i in 0..2_000u64 {
f.insert(&i);
}
for i in 0..2_000u64 {
assert!(f.contains(&i), "false negative in preallocated for {}", i);
}
}
#[test]
fn test_accessors() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
assert_eq!(f.filter_count(), 1);
assert!(f.total_capacity() >= 1_000);
assert!(f.shard_count() > 0);
assert!(f.hash_count() > 0);
assert!(f.bit_count() > 0);
assert_eq!(f.count_set_bits(), 0);
assert!(f.expected_items() >= 1_000);
assert!(!f.is_at_max_capacity());
assert!(!f.is_near_capacity());
f.insert(&1);
assert!(f.count_set_bits() > 0);
}
#[test]
fn test_current_fill_rate() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
assert_eq!(f.current_fill_rate(), 0.0);
f.insert(&1);
let rate = f.current_fill_rate();
assert!(rate > 0.0 && rate <= 1.0);
}
#[test]
fn test_estimate_count() {
let f = AtomicScalableBloomFilter::<u64>::new(10_000, 0.01).unwrap();
assert_eq!(f.estimate_count(), 0);
for i in 0..500u64 {
f.insert(&i);
}
let est = f.estimate_count();
assert!(est > 100, "estimate_count too low: {}", est);
assert!(est < 5_000, "estimate_count too high: {}", est);
}
#[test]
fn test_estimate_fpr() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
let fpr = f.estimate_fpr();
assert!(fpr >= 0.0 && fpr <= 1.0);
assert!(fpr < 0.1);
}
#[test]
fn test_false_positive_rate_alias() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
assert_eq!(f.false_positive_rate(), f.estimate_fpr());
}
#[test]
fn test_bit_statistics() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
let (total, set, utilization) = f.bit_statistics();
assert_eq!(total, f.bit_count());
assert_eq!(set, 0);
assert_eq!(utilization, 0.0);
for i in 0..100u64 {
f.insert(&i);
}
let (_, set2, utilization2) = f.bit_statistics();
assert!(set2 > 0);
assert!(utilization2 > 0.0);
}
#[test]
fn test_insert_batch() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
let items: Vec<u64> = (0..100).collect();
f.insert_batch(&items).unwrap();
assert_eq!(f.len(), 100);
for i in 0..100u64 {
assert!(f.contains(&i));
}
}
#[test]
fn test_insert_batch_empty() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
let empty: Vec<u64> = vec![];
f.insert_batch(&empty).unwrap();
assert_eq!(f.len(), 0);
}
#[test]
fn test_contains_batch() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
for i in 0..50u64 {
f.insert(&i);
}
let queries: Vec<u64> = (0..100).collect();
let results = f.contains_batch(&queries);
assert_eq!(results.len(), 100);
for (i, &present) in results.iter().enumerate() {
assert_eq!(present, i < 50, "mismatch at index {}", i);
}
}
#[test]
fn test_clone() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
f.insert(&42);
let g = f.clone();
assert!(g.contains(&42));
assert_eq!(g.len(), 1);
f.insert(&99);
assert!(g.contains(&99));
}
#[test]
fn test_shared_bloom_filter_trait() {
use crate::core::filter::SharedBloomFilter;
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
SharedBloomFilter::insert(&f, &42);
assert!(SharedBloomFilter::contains(&f, &42));
assert_eq!(SharedBloomFilter::len(&f), f.count_set_bits());
assert!(!SharedBloomFilter::is_empty(&f));
assert!(SharedBloomFilter::false_positive_rate(&f) > 0.0);
SharedBloomFilter::clear(&f);
assert!(SharedBloomFilter::is_empty(&f));
}
#[test]
fn test_display() {
let f = AtomicScalableBloomFilter::<u64>::new(1_000, 0.01).unwrap();
let s = format!("{}", f);
assert!(s.starts_with("AtomicScalableBloomFilter {"));
assert!(s.contains("filters: 1"));
}
#[test]
fn test_growth_strategy_validate() {
assert!(GrowthStrategy::Geometric(1.0).validate().is_err());
assert!(GrowthStrategy::Geometric(0.5).validate().is_err());
assert!(GrowthStrategy::Geometric(f64::NAN).validate().is_err());
assert!(GrowthStrategy::Geometric(f64::INFINITY).validate().is_err());
assert!(GrowthStrategy::Geometric(2.0).validate().is_ok());
assert!(GrowthStrategy::Geometric(100.0).validate().is_ok());
assert!(GrowthStrategy::Bounded { scale: 0.0, max_filter_size: 100 }.validate().is_err());
assert!(GrowthStrategy::Bounded { scale: -1.0, max_filter_size: 100 }.validate().is_err());
assert!(GrowthStrategy::Bounded { scale: f64::NAN, max_filter_size: 100 }.validate().is_err());
assert!(GrowthStrategy::Bounded { scale: 2.0, max_filter_size: 100 }.validate().is_ok());
assert!(GrowthStrategy::Constant.validate().is_ok());
assert!(GrowthStrategy::Adaptive { initial_ratio: 0.5, min_ratio: 0.3, max_ratio: 0.9 }.validate().is_ok());
}
}