use std::{
fmt::Display,
hash::RandomState,
num::{NonZeroU32, NonZeroUsize},
ops::{Add, Deref, DerefMut},
};
use crate::{CuckooFilter, data_block::DataBlockFieldConfiguration};
#[derive(Debug)]
pub enum ConfigError {
BucketTooBig,
BucketCountTooBig,
BitCountTooHigh,
BitCountTooLow,
}
impl Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::BucketTooBig => {
f.write_str("Filter configuration requires buckets that are too big!")
}
ConfigError::BucketCountTooBig => {
f.write_str("Filter configuration requires bucket count that is too high! Reduce max number of entries or increase bucket size.")
}
ConfigError::BitCountTooHigh => f.write_str(&format!(
"Bit count is too high! Max is {}.",
BitCount::MAX.0
)),
ConfigError::BitCountTooLow => {
f.write_str(&format!("Bit count too low! Min is {}.", BitCount::MIN.0))
}
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
pub struct CuckooConfigurationBuilder {
pub(crate) fingerprint_bits: BitCount,
pub(crate) bucket_size: NonZeroUsize,
pub(crate) max_entries: usize,
pub(crate) max_kicks: usize,
pub(crate) lru: Option<LruConfig>,
pub(crate) ttl: Option<TtlConfig>,
pub(crate) counter: Option<CounterConfig>,
}
impl CuckooConfigurationBuilder {
#[must_use]
pub const fn fingerprint_bits(mut self, bits: BitCount) -> Self {
self.fingerprint_bits = bits;
self
}
#[must_use]
pub const fn bucket_size(mut self, size: NonZeroUsize) -> Self {
self.bucket_size = size;
self
}
#[must_use]
pub const fn max_kicks(mut self, kicks: usize) -> Self {
self.max_kicks = kicks;
self
}
#[must_use]
pub const fn with_lru(mut self, lru: LruConfig) -> Self {
self.lru = Some(lru);
self
}
#[must_use]
pub const fn with_ttl(mut self, ttl: TtlConfig) -> Self {
self.ttl = Some(ttl);
self
}
#[must_use]
pub const fn with_counter(mut self, counter: CounterConfig) -> Self {
self.counter = Some(counter);
self
}
pub fn build(&self) -> Result<CuckooConfiguration, ConfigError> {
let required_bucket_count = self.max_entries.div_ceil(self.bucket_size.get());
let bucket_count = required_bucket_count
.checked_next_power_of_two()
.ok_or(ConfigError::BucketCountTooBig)?;
let ttl_start = *self.fingerprint_bits
+ if let Some(LruConfig { counter_bits, .. }) = self.lru {
*counter_bits
} else {
0
};
let counter_start = ttl_start
+ if let Some(TtlConfig { ttl_bits, .. }) = self.ttl {
*ttl_bits
} else {
0
};
let mut data_block_size = *self.fingerprint_bits;
if let Some(LruConfig { counter_bits, .. }) = self.lru {
data_block_size += *counter_bits;
}
if let Some(TtlConfig { ttl_bits, .. }) = self.ttl {
data_block_size += *ttl_bits;
}
if let Some(CounterConfig { counter_bits, .. }) = self.counter {
data_block_size += *counter_bits;
}
data_block_size = data_block_size.div_ceil(8);
Ok(CuckooConfiguration {
bucket_size: self.bucket_size.get(),
max_kicks: self.max_kicks,
fingerprint_field_config: DataBlockFieldConfiguration::new(0..*self.fingerprint_bits),
lru_field_config: self.lru.clone().map(|lru| {
(
lru,
DataBlockFieldConfiguration::new(
*self.fingerprint_bits
..*self.fingerprint_bits
+ self
.lru
.as_ref()
.map(|l| l.counter_bits)
.unwrap_or(BitCount(0)),
),
)
}),
ttl_field_config: self.ttl.clone().map(|ttl| {
(
ttl,
DataBlockFieldConfiguration::new(
ttl_start
..ttl_start
+ *self.ttl.as_ref().map(|t| t.ttl_bits).unwrap_or(BitCount(0)),
),
)
}),
counter_field_config: self.counter.clone().map(|counter| {
(
counter,
DataBlockFieldConfiguration::new(
counter_start
..counter_start
+ *self
.counter
.as_ref()
.map(|c| c.counter_bits)
.unwrap_or(BitCount(0)),
),
)
}),
data_block_size,
bucket_byte_size: self
.bucket_size
.get()
.checked_mul(data_block_size)
.ok_or(ConfigError::BucketTooBig)?,
bucket_count,
#[expect(clippy::cast_possible_truncation)]
buckets_mask: (bucket_count - 1) as u32,
})
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum LruAgingStrategy {
#[default]
Halving,
Decrement(u32),
}
impl LruAgingStrategy {
pub(crate) const fn age_value(&self, value: u32) -> u32 {
match self {
LruAgingStrategy::Halving => value >> 1,
LruAgingStrategy::Decrement(dec) => value.saturating_sub(*dec),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LruConfig {
pub counter_bits: BitCount,
pub aging_strategy: LruAgingStrategy,
pub starting_value: u32,
pub remove_on_zero: bool,
pub increment: u32,
}
impl Default for LruConfig {
fn default() -> Self {
Self {
counter_bits: BitCount(8),
aging_strategy: LruAgingStrategy::default(),
starting_value: 1,
remove_on_zero: false,
increment: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TtlConfig {
pub ttl: NonZeroU32,
pub ttl_bits: BitCount,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterConfig {
pub counter_bits: BitCount,
pub change_on_insert: i32,
pub change_on_lookup: i32,
}
impl Default for CounterConfig {
fn default() -> Self {
Self {
counter_bits: BitCount(4),
change_on_insert: 1,
change_on_lookup: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CuckooConfiguration {
pub(crate) bucket_size: usize,
pub(crate) max_kicks: usize,
pub(crate) fingerprint_field_config: DataBlockFieldConfiguration,
pub(crate) lru_field_config: Option<(LruConfig, DataBlockFieldConfiguration)>,
pub(crate) counter_field_config: Option<(CounterConfig, DataBlockFieldConfiguration)>,
pub(crate) ttl_field_config: Option<(TtlConfig, DataBlockFieldConfiguration)>,
pub(crate) data_block_size: usize,
pub(crate) bucket_byte_size: usize,
pub(crate) bucket_count: usize,
pub(crate) buckets_mask: u32,
}
impl CuckooConfiguration {
#[must_use]
pub const fn builder(max_entries: usize) -> CuckooConfigurationBuilder {
CuckooConfigurationBuilder {
fingerprint_bits: BitCount(8),
#[expect(clippy::expect_used)]
bucket_size: NonZeroUsize::new(4).expect("4 != 0"),
max_entries,
max_kicks: 500,
lru: None,
ttl: None,
counter: None,
}
}
#[must_use]
pub const fn get_configured_memory_usage(&self) -> usize {
CuckooFilter::<RandomState>::get_expected_memory_usage(
self.bucket_byte_size,
self.bucket_count,
)
}
#[must_use]
pub fn compatible_layout(&self, other: &Self) -> bool {
if self.bucket_size != other.bucket_size {
return false;
}
if self.bucket_count != other.bucket_count {
return false;
}
if self.buckets_mask != other.buckets_mask {
return false;
}
if self.bucket_byte_size != other.bucket_byte_size {
return false;
}
if self.data_block_size != other.data_block_size {
return false;
}
if self.fingerprint_field_config != other.fingerprint_field_config {
return false;
}
if !Self::field_compatible(&self.lru_field_config, &other.lru_field_config) {
return false;
}
if !Self::field_compatible(&self.counter_field_config, &other.counter_field_config) {
return false;
}
if !Self::field_compatible(&self.ttl_field_config, &other.ttl_field_config) {
return false;
}
true
}
#[must_use]
pub const fn bucket_size(&self) -> usize {
self.bucket_size
}
#[must_use]
pub const fn max_kicks(&self) -> usize {
self.max_kicks
}
#[must_use]
pub fn lru_config(&self) -> Option<LruConfig> {
self.lru_field_config
.as_ref()
.map(|(lru, _field)| lru.clone())
}
#[must_use]
pub fn ttl_config(&self) -> Option<TtlConfig> {
self.ttl_field_config
.as_ref()
.map(|(ttl, _field)| ttl.clone())
}
#[must_use]
pub fn counter_config(&self) -> Option<CounterConfig> {
self.counter_field_config
.as_ref()
.map(|(counter, _field)| counter.clone())
}
#[must_use]
pub const fn get_bucket_count(&self) -> usize {
self.bucket_count
}
fn field_compatible<T>(
self_field: &Option<(T, DataBlockFieldConfiguration)>,
other_field: &Option<(T, DataBlockFieldConfiguration)>,
) -> bool {
match (self_field, other_field) {
(None, Some(_)) | (Some(_), None) => false,
(None, None) => true,
(Some((_, self_field_config)), Some((_, other_field_config))) => {
self_field_config == other_field_config
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BitCount(usize);
impl BitCount {
pub const MAX: BitCount = BitCount(32);
pub const MIN: BitCount = BitCount(1);
}
impl Deref for BitCount {
type Target = usize;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BitCount {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<usize> for BitCount {
type Error = ConfigError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
if value > Self::MAX.0 {
return Err(ConfigError::BitCountTooHigh);
}
if value < Self::MIN.0 {
return Err(ConfigError::BitCountTooLow);
}
Ok(Self(value))
}
}
impl From<BitCount> for usize {
fn from(value: BitCount) -> Self {
value.0
}
}
impl From<BitCount> for u64 {
fn from(value: BitCount) -> Self {
value.0 as u64
}
}
impl From<BitCount> for u32 {
#[expect(clippy::cast_possible_truncation)]
fn from(value: BitCount) -> Self {
value.0 as u32
}
}
impl From<BitCount> for u16 {
#[expect(clippy::cast_possible_truncation)]
fn from(value: BitCount) -> Self {
value.0 as u16
}
}
impl From<BitCount> for u8 {
#[expect(clippy::cast_possible_truncation)]
fn from(value: BitCount) -> Self {
value.0 as u8
}
}
impl Add<usize> for BitCount {
type Output = usize;
fn add(self, rhs: usize) -> Self::Output {
self.0 + rhs
}
}
impl Add<BitCount> for usize {
type Output = usize;
fn add(self, rhs: BitCount) -> Self::Output {
self + rhs.0
}
}