use std::fmt;
use std::hash::{BuildHasher, Hash};
use std::marker::PhantomData;
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::sync::Arc;
use std::time::Duration;
use foldhash::fast::RandomState;
use crate::notification::RemovalCause;
use crate::policy::EvictionPolicy;
use crate::tier::InMemoryCache;
pub(crate) type EvictionListener = Arc<dyn Fn(RemovalCause) + Send + Sync + 'static>;
pub struct InMemoryCacheBuilder<K, V, H = RandomState> {
pub(crate) max_capacity: Option<u64>,
pub(crate) initial_capacity: Option<usize>,
pub(crate) time_to_live: Option<Duration>,
pub(crate) time_to_idle: Option<Duration>,
pub(crate) name: Option<&'static str>,
pub(crate) eviction_policy: EvictionPolicy,
pub(crate) eviction_listener: Option<EvictionListener>,
pub(crate) eviction_telemetry: bool,
pub(crate) hasher: H,
_phantom: PhantomData<(K, V)>,
}
impl<K, V, H: fmt::Debug> fmt::Debug for InMemoryCacheBuilder<K, V, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InMemoryCacheBuilder")
.field("max_capacity", &self.max_capacity)
.field("initial_capacity", &self.initial_capacity)
.field("time_to_live", &self.time_to_live)
.field("time_to_idle", &self.time_to_idle)
.field("name", &self.name)
.field("eviction_policy", &self.eviction_policy)
.field("eviction_listener", &self.eviction_listener.as_ref().map(|_| "<set>"))
.field("eviction_telemetry", &self.eviction_telemetry)
.field("hasher", &self.hasher)
.finish()
}
}
impl<K, V, H: UnwindSafe> UnwindSafe for InMemoryCacheBuilder<K, V, H> {}
impl<K, V, H: RefUnwindSafe> RefUnwindSafe for InMemoryCacheBuilder<K, V, H> {}
impl<K, V> Default for InMemoryCacheBuilder<K, V> {
fn default() -> Self {
Self::new()
}
}
impl<K, V> InMemoryCacheBuilder<K, V> {
#[must_use]
pub fn new() -> Self {
Self {
max_capacity: None,
initial_capacity: None,
time_to_live: None,
time_to_idle: None,
name: None,
eviction_policy: EvictionPolicy::default(),
eviction_listener: None,
eviction_telemetry: false,
hasher: RandomState::default(),
_phantom: PhantomData,
}
}
}
impl<K, V, H> InMemoryCacheBuilder<K, V, H> {
#[must_use]
pub fn max_capacity(mut self, capacity: u64) -> Self {
self.max_capacity = Some(capacity);
self
}
#[must_use]
pub fn initial_capacity(mut self, capacity: usize) -> Self {
self.initial_capacity = Some(capacity);
self
}
#[must_use]
pub fn time_to_live(mut self, duration: Duration) -> Self {
self.time_to_live = Some(duration);
self
}
#[must_use]
pub fn time_to_idle(mut self, duration: Duration) -> Self {
self.time_to_idle = Some(duration);
self
}
#[must_use]
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
self.eviction_policy = policy;
self
}
#[must_use]
pub fn on_eviction<F>(mut self, listener: F) -> Self
where
F: Fn(RemovalCause) + Send + Sync + 'static,
{
self.eviction_listener = Some(match self.eviction_listener.take() {
Some(previous) => Arc::new(move |cause| {
previous(cause);
listener(cause);
}),
None => Arc::new(listener),
});
self
}
#[must_use]
pub fn with_eviction_telemetry(mut self) -> Self {
self.eviction_telemetry = true;
self
}
#[must_use]
pub fn eviction_telemetry_enabled(&self) -> bool {
self.eviction_telemetry
}
#[must_use]
pub fn with_hasher<H2>(self, hasher: H2) -> InMemoryCacheBuilder<K, V, H2> {
InMemoryCacheBuilder {
max_capacity: self.max_capacity,
initial_capacity: self.initial_capacity,
time_to_live: self.time_to_live,
time_to_idle: self.time_to_idle,
name: self.name,
eviction_policy: self.eviction_policy,
eviction_listener: self.eviction_listener,
eviction_telemetry: self.eviction_telemetry,
hasher,
_phantom: PhantomData,
}
}
pub fn build(self) -> Result<InMemoryCache<K, V, H>, ValidationError>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
self.validate()?;
Ok(InMemoryCache::from_builder(self))
}
fn validate(&self) -> Result<(), ValidationError> {
ValidationError::invalid_capacity(self.max_capacity, self.initial_capacity).map_or(Ok(()), Err)?;
ValidationError::invalid_time_to(self.time_to_live, self.time_to_idle).map_or(Ok(()), Err)?;
Ok(())
}
pub(crate) fn build_unchecked(self) -> InMemoryCache<K, V, H>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
InMemoryCache::from_builder(self)
}
}
#[ohno::error]
#[display("invalid cache configuration: {reason}")]
pub struct ValidationError {
reason: String,
}
impl ValidationError {
fn invalid_capacity(max_capacity: Option<u64>, initial_capacity: Option<usize>) -> Option<Self> {
let max = max_capacity?;
let init = initial_capacity?;
(init as u64 > max).then(|| Self::new(format!("initial_capacity ({init}) exceeds max_capacity ({max})")))
}
fn invalid_time_to(time_to_live: Option<Duration>, time_to_idle: Option<Duration>) -> Option<Self> {
let time_to_idle = time_to_idle?;
let time_to_live = time_to_live?;
(time_to_idle > time_to_live)
.then(|| Self::new(format!("time to idle ({time_to_idle:?}) exceeds time to live ({time_to_live:?}).")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn max_capacity_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().max_capacity(100);
assert_eq!(builder.max_capacity, Some(100));
}
#[test]
fn initial_capacity_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().initial_capacity(50);
assert_eq!(builder.initial_capacity, Some(50));
}
#[test]
fn time_to_live_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_live(Duration::from_mins(5));
assert_eq!(builder.time_to_live, Some(Duration::from_mins(5)));
}
#[test]
fn time_to_idle_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_idle(Duration::from_mins(1));
assert_eq!(builder.time_to_idle, Some(Duration::from_mins(1)));
}
#[test]
fn name_stores_value() {
let builder = InMemoryCacheBuilder::<String, i32>::new().name("test");
assert_eq!(builder.name, Some("test"));
}
#[test]
fn eviction_telemetry_defaults_false() {
let builder = InMemoryCacheBuilder::<String, i32>::new();
assert!(!builder.eviction_telemetry_enabled());
}
#[test]
fn with_eviction_telemetry_sets_flag() {
let builder = InMemoryCacheBuilder::<String, i32>::new().with_eviction_telemetry();
assert!(builder.eviction_telemetry_enabled());
}
#[test]
fn with_hasher_preserves_eviction_telemetry_flag() {
let builder = InMemoryCacheBuilder::<String, i32>::new()
.with_eviction_telemetry()
.with_hasher(std::collections::hash_map::RandomState::new());
assert!(builder.eviction_telemetry_enabled());
}
#[test]
fn on_eviction_chains_existing_listener() {
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
let first_count = Arc::new(AtomicUsize::new(0));
let second_count = Arc::new(AtomicUsize::new(0));
let order: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let first_count_cb = Arc::clone(&first_count);
let second_count_cb = Arc::clone(&second_count);
let order_first = Arc::clone(&order);
let order_second = Arc::clone(&order);
let builder = InMemoryCacheBuilder::<String, i32>::new()
.on_eviction(move |_| {
first_count_cb.fetch_add(1, Ordering::Relaxed);
order_first.lock().unwrap().push("first");
})
.on_eviction(move |_| {
second_count_cb.fetch_add(1, Ordering::Relaxed);
order_second.lock().unwrap().push("second");
});
let listener = builder.eviction_listener.expect("listener should be installed");
listener(RemovalCause::Size);
assert_eq!(first_count.load(Ordering::Relaxed), 1);
assert_eq!(second_count.load(Ordering::Relaxed), 1);
assert_eq!(*order.lock().unwrap(), vec!["first", "second"]);
}
#[test]
fn debug_impl_renders_all_fields() {
let builder = InMemoryCacheBuilder::<String, i32>::new()
.max_capacity(100)
.initial_capacity(10)
.time_to_live(Duration::from_mins(1))
.time_to_idle(Duration::from_secs(30))
.name("my_cache")
.with_eviction_telemetry()
.on_eviction(|_| {});
let rendered = format!("{builder:?}");
assert!(rendered.contains("InMemoryCacheBuilder"));
assert!(rendered.contains("max_capacity: Some(100)"));
assert!(rendered.contains("initial_capacity: Some(10)"));
assert!(rendered.contains("time_to_live: Some(60s)"));
assert!(rendered.contains("time_to_idle: Some(30s)"));
assert!(rendered.contains("name: Some(\"my_cache\")"));
assert!(rendered.contains("eviction_telemetry: true"));
assert!(rendered.contains("eviction_listener: Some(\"<set>\")"));
}
#[test]
fn build_max_capacity_lt_initial_capacity_returns_validation_error() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.max_capacity(100)
.initial_capacity(101)
.build();
ohno::assert_error_message!(
result.unwrap_err(),
"invalid cache configuration: initial_capacity (101) exceeds max_capacity (100)"
);
}
#[cfg_attr(miri, ignore)] #[test]
fn build_max_capacity_eq_initial_capacity_succeeds() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.max_capacity(100)
.initial_capacity(100)
.build();
result.unwrap();
}
#[test]
fn build_ttl_less_than_tti_returns_validation_error() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.time_to_live(Duration::from_mins(1))
.time_to_idle(Duration::from_mins(2))
.build();
ohno::assert_error_message!(
result.unwrap_err(),
"invalid cache configuration: time to idle (120s) exceeds time to live (60s)."
);
}
#[cfg_attr(miri, ignore)] #[test]
fn build_ttl_eq_tti_succeeds() {
let result = InMemoryCacheBuilder::<String, i32>::new()
.time_to_live(Duration::from_mins(1))
.time_to_idle(Duration::from_mins(1))
.build();
result.unwrap();
}
#[test]
fn build_eviction_policy_stores_value() {
let policy = EvictionPolicy::lru();
let builder = InMemoryCacheBuilder::<String, i32>::new().eviction_policy(policy.clone());
assert_eq!(builder.eviction_policy, policy);
}
}