use std::hash::{BuildHasher, Hash};
use std::time::{Duration, Instant};
use cachet_tier::{CacheEntry, CacheTier, Error, SizeError};
use foldhash::fast::RandomState;
use moka::Expiry;
use moka::future::Cache;
use thread_aware::{Arc, PerProcess, ThreadAware};
use crate::builder::InMemoryCacheBuilder;
#[derive(Debug, Clone, ThreadAware)]
pub struct InMemoryCache<K, V, H = RandomState>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
inner: Arc<Cache<K, CacheEntry<V>, H>, PerProcess>,
}
impl<K, V> Default for InMemoryCache<K, V>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<K, V> InMemoryCache<K, V>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
#[must_use]
pub fn new() -> Self {
Self::builder().build_unchecked()
}
#[must_use]
pub fn with_max_capacity(max_capacity: u64) -> Self {
Self::builder().max_capacity(max_capacity).build_unchecked()
}
#[cfg_attr(test, mutants::skip)] #[must_use]
pub fn builder() -> InMemoryCacheBuilder<K, V> {
InMemoryCacheBuilder::new()
}
}
impl<K, V, H> InMemoryCache<K, V, H>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
pub(crate) fn from_builder(builder: InMemoryCacheBuilder<K, V, H>) -> Self {
let mut moka_builder = Cache::builder();
if let Some(capacity) = builder.max_capacity {
moka_builder = moka_builder.max_capacity(capacity);
}
if let Some(capacity) = builder.initial_capacity {
moka_builder = moka_builder.initial_capacity(capacity);
}
if let Some(ttl) = builder.time_to_live {
moka_builder = moka_builder.time_to_live(ttl);
}
if let Some(tti) = builder.time_to_idle {
moka_builder = moka_builder.time_to_idle(tti);
}
if let Some(name) = builder.name {
moka_builder = moka_builder.name(name);
}
if let Some(listener) = builder.eviction_listener {
moka_builder = moka_builder.eviction_listener(move |_key, _value, cause| {
listener(crate::notification::from_moka(cause));
});
}
Self {
inner: Arc::from_unaware(
moka_builder
.expire_after(EntryExpiry)
.eviction_policy(builder.eviction_policy.into_moka_policy())
.build_with_hasher(builder.hasher),
),
}
}
}
impl<K, V, H> CacheTier<K, V> for InMemoryCache<K, V, H>
where
K: Clone + Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
H: BuildHasher + Clone + Send + Sync + 'static,
{
async fn get(&self, key: &K) -> Result<Option<CacheEntry<V>>, Error> {
Ok(self.inner.get(key).await)
}
async fn insert(&self, key: K, entry: CacheEntry<V>) -> Result<(), Error> {
self.inner.insert(key.clone(), entry).await;
Ok(())
}
async fn invalidate(&self, key: &K) -> Result<(), Error> {
self.inner.invalidate(key).await;
Ok(())
}
async fn clear(&self) -> Result<(), Error> {
self.inner.invalidate_all();
Ok(())
}
async fn len(&self) -> Result<u64, SizeError> {
Ok(self.inner.entry_count())
}
}
struct EntryExpiry;
impl<K, V> Expiry<K, CacheEntry<V>> for EntryExpiry {
fn expire_after_create(&self, _key: &K, value: &CacheEntry<V>, _created_at: Instant) -> Option<Duration> {
value.ttl()
}
fn expire_after_update(
&self,
_key: &K,
value: &CacheEntry<V>,
_updated_at: Instant,
_duration_until_expiry: Option<Duration>,
) -> Option<Duration> {
value.ttl()
}
}
#[cfg(test)]
mod tests {
use std::time::SystemTime;
use super::*;
#[cfg_attr(miri, ignore)] #[test]
fn with_max_capacity_sets_max_capacity() {
let cache = InMemoryCache::<String, i32>::with_max_capacity(100);
assert_eq!(cache.inner.policy().max_capacity(), Some(100));
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn len_returns_nonzero_after_insert() {
let cache = InMemoryCache::<String, i32>::new();
cache.inner.insert("key".to_string(), CacheEntry::new(42)).await;
cache.inner.run_pending_tasks().await;
assert!(cache.len().await.expect("len should return Ok") > 0);
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn custom_hasher_get_insert_invalidate() {
use std::collections::hash_map::RandomState;
let cache = InMemoryCache::<String, i32>::builder()
.with_hasher(RandomState::new())
.max_capacity(100)
.build()
.expect("Failed to build cache");
cache.insert("key".to_string(), CacheEntry::new(42)).await.unwrap();
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.unwrap();
assert_eq!(*value.unwrap().value(), 42);
assert_eq!(cache.len().await.expect("len should return Ok"), 1);
cache.invalidate(&"key".to_string()).await.unwrap();
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.unwrap();
assert!(value.is_none());
}
#[cfg_attr(miri, ignore)]
#[test]
fn builder_max_capacity_sets_limit() {
let expected_max_capacity = 100;
let builder = InMemoryCacheBuilder::<String, i32>::new().max_capacity(expected_max_capacity);
assert_eq!(builder.max_capacity, Some(expected_max_capacity));
}
#[cfg_attr(miri, ignore)]
#[test]
fn builder_initial_capacity_sets_initial_capacity() {
let expected_initial_capacity = 50;
let builder = InMemoryCacheBuilder::<String, i32>::new().initial_capacity(expected_initial_capacity);
assert_eq!(builder.initial_capacity, Some(50));
}
#[cfg_attr(miri, ignore)]
#[test]
fn builder_time_to_live_sets_ttl() {
let expected_ttl = Duration::from_secs(300);
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_live(expected_ttl);
assert_eq!(builder.time_to_live, Some(expected_ttl));
}
#[cfg_attr(miri, ignore)]
#[test]
fn builder_time_to_idle_sets_tti() {
let expected_tti = Duration::from_secs(60);
let builder = InMemoryCacheBuilder::<String, i32>::new().time_to_idle(expected_tti);
assert_eq!(builder.time_to_idle, Some(expected_tti));
}
#[cfg_attr(miri, ignore)]
#[test]
fn builder_name_sets_cache_name() {
let builder = InMemoryCacheBuilder::<String, i32>::new().name("test-cache");
assert_eq!(builder.name, Some("test-cache"));
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn insert_and_get_returns_value() {
let cache = InMemoryCache::<String, i32>::new();
cache
.insert("key".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should succeed");
assert_eq!(*value.unwrap().value(), 42);
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn get_returns_none_after_per_entry_ttl() {
let cache = InMemoryCache::<String, i32>::new();
cache
.insert("key".to_string(), CacheEntry::expires_at(42, Duration::ZERO, SystemTime::now()))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should return none");
assert!(value.is_none());
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn update_with_per_entry_ttl_expires_entry() {
let cache = InMemoryCache::<String, i32>::new();
cache
.insert("key".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
cache
.insert("key".to_string(), CacheEntry::expires_at(99, Duration::ZERO, SystemTime::now()))
.await
.expect("Update should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should succeed");
assert!(value.is_none(), "Entry should expire after update with zero TTL");
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn get_returns_none_after_cache_ttl() {
let cache = InMemoryCache::<String, i32>::builder()
.time_to_live(Duration::ZERO)
.build()
.expect("Failed to build cache");
cache
.insert("key".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should return none");
assert!(value.is_none());
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn get_returns_none_after_cache_tti() {
let cache = InMemoryCache::<String, i32>::builder()
.time_to_idle(Duration::ZERO)
.build()
.expect("Failed to build cache");
cache
.insert("key".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should return none");
assert!(value.is_none());
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn invalidate_removes_entry() {
let cache = InMemoryCache::<String, i32>::new();
cache
.insert("key".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
cache.invalidate(&"key".to_string()).await.expect("Invalidate should succeed");
cache.inner.run_pending_tasks().await;
let value = cache.get(&"key".to_string()).await.expect("Get should return none");
assert!(value.is_none());
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn clear_removes_all_entries() {
let cache = InMemoryCache::<String, i32>::new();
cache
.insert("key1".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache
.insert("key2".to_string(), CacheEntry::new(43))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
cache.clear().await.expect("Clear should succeed");
cache.inner.run_pending_tasks().await;
let value1 = cache.get(&"key1".to_string()).await.expect("Get should return none");
let value2 = cache.get(&"key2".to_string()).await.expect("Get should return none");
assert!(value1.is_none());
assert!(value2.is_none());
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn len_returns_correct_count() {
let cache = InMemoryCache::<String, i32>::new();
assert_eq!(cache.len().await.expect("len should return Ok"), 0);
cache
.insert("key1".to_string(), CacheEntry::new(42))
.await
.expect("Insert should succeed");
cache
.insert("key2".to_string(), CacheEntry::new(43))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
assert_eq!(cache.len().await.expect("len should return Ok"), 2);
cache.invalidate(&"key1".to_string()).await.expect("Invalidate should succeed");
cache.inner.run_pending_tasks().await;
assert_eq!(cache.len().await.expect("len should return Ok"), 1);
cache.clear().await.expect("Clear should succeed");
cache.inner.run_pending_tasks().await;
assert_eq!(cache.len().await.expect("len should return Ok"), 0);
}
#[cfg_attr(miri, ignore)] #[tokio::test]
async fn max_capacity_evicts_at_capacity() {
let capacity = 5;
let cache = InMemoryCache::<String, u64>::builder()
.max_capacity(capacity)
.build()
.expect("Cache should build successfully");
for i in 0..capacity {
cache
.insert(format!("key{i}"), CacheEntry::new(i))
.await
.expect("Insert should succeed");
}
cache.inner.run_pending_tasks().await;
cache
.insert(format!("key{capacity}"), CacheEntry::new(capacity))
.await
.expect("Insert should succeed");
cache.inner.run_pending_tasks().await;
assert_eq!(cache.len().await.expect("len should return Ok"), capacity);
}
}