use core::time::Duration;
use core::hash::Hash;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
#[ derive( Debug, Clone ) ]
struct CacheEntry< V >
{
value : V,
expires_at : Option< Instant >,
last_accessed : Instant,
}
impl< V > CacheEntry< V >
{
#[ inline ]
fn new( value : V, ttl : Option< Duration > ) -> Self
{
let now = Instant::now( );
Self {
value,
expires_at : ttl.map( |d| now + d ),
last_accessed : now,
}
}
#[ inline ]
fn is_expired( &self ) -> bool
{
self.expires_at.is_some_and( |exp| Instant::now( ) >= exp )
}
#[ inline ]
fn touch( &mut self )
{
self.last_accessed = Instant::now( );
}
}
#[ derive( Debug, Clone ) ]
pub struct CacheConfig
{
pub max_entries : usize,
pub default_ttl : Option< Duration >,
}
impl Default for CacheConfig
{
#[ inline ]
fn default() -> Self
{
Self {
max_entries : 1000,
default_ttl : Some( Duration::from_secs( 300 )), }
}
}
#[ derive( Debug, Clone, Copy, Default ) ]
pub struct CacheStats
{
pub hits : u64,
pub misses : u64,
pub evictions : u64,
pub entries : usize,
}
impl CacheStats
{
#[ inline ]
#[ must_use ]
pub fn hit_rate( &self ) -> f64
{
let total = self.hits + self.misses;
if total == 0
{
0.0
} else {
self.hits as f64 / total as f64
}
}
#[ inline ]
#[ must_use ]
pub fn total_requests( &self ) -> u64
{
self.hits + self.misses
}
}
struct CacheState< K, V >
{
entries : HashMap< K, CacheEntry< V > >,
config : CacheConfig,
stats : CacheStats,
}
#[ derive( Clone ) ]
pub struct Cache< K, V >
{
state : Arc< RwLock< CacheState< K, V > > >,
}
impl< K, V > Cache< K, V >
where
K : Eq + Hash + Clone,
V : Clone,
{
#[ inline ]
#[ must_use ]
pub fn new( config : CacheConfig ) -> Self
{
Self {
state : Arc::new( RwLock::new( CacheState {
entries : HashMap::new( ),
config,
stats : CacheStats::default( ),
} )),
}
}
#[ inline ]
pub async fn insert( &self, key : K, value : V, ttl : Option< Duration > )
{
let mut state = self.state.write( ).await;
let effective_ttl = ttl.or( state.config.default_ttl );
if state.entries.len( ) >= state.config.max_entries && !state.entries.contains_key( &key )
{
let lru_key = state.entries.iter( )
.min_by_key( |( _, entry )| entry.last_accessed )
.map( |( k, _ )| k.clone( ));
if let Some( lru_key ) = lru_key
{
state.entries.remove( &lru_key );
state.stats.evictions += 1;
}
}
let entry = CacheEntry::new( value, effective_ttl );
state.entries.insert( key, entry );
state.stats.entries = state.entries.len( );
}
#[ inline ]
pub async fn get( &self, key : &K ) -> Option< V >
{
let mut state = self.state.write( ).await;
if let Some( entry ) = state.entries.get_mut( key )
{
if entry.is_expired( )
{
state.entries.remove( key );
state.stats.entries = state.entries.len( );
state.stats.misses += 1;
None
} else {
entry.touch( );
let value = entry.value.clone( );
state.stats.hits += 1;
Some( value )
}
} else {
state.stats.misses += 1;
None
}
}
#[ inline ]
pub async fn contains_key( &self, key : &K ) -> bool
{
let state = self.state.read( ).await;
state.entries.get( key )
.is_some_and( |entry| !entry.is_expired( ))
}
#[ inline ]
pub async fn remove( &self, key : &K ) -> Option< V >
{
let mut state = self.state.write( ).await;
let value = state.entries.remove( key ).map( |entry| entry.value );
state.stats.entries = state.entries.len( );
value
}
#[ inline ]
pub async fn clear( &self )
{
let mut state = self.state.write( ).await;
state.entries.clear( );
state.stats.entries = 0;
}
#[ inline ]
pub async fn cleanup_expired( &self ) -> usize
{
let mut state = self.state.write( ).await;
let before = state.entries.len( );
state.entries.retain( |_, entry| !entry.is_expired( ));
let removed = before - state.entries.len( );
state.stats.entries = state.entries.len( );
removed
}
#[ inline ]
pub async fn stats( &self ) -> CacheStats
{
let state = self.state.read( ).await;
state.stats
}
#[ inline ]
pub async fn reset_stats( &self )
{
let mut state = self.state.write( ).await;
state.stats = CacheStats {
entries : state.entries.len( ),
..Default::default( )
};
}
#[ inline ]
pub async fn len( &self ) -> usize
{
let state = self.state.read( ).await;
state.entries.len( )
}
#[ inline ]
pub async fn is_empty( &self ) -> bool
{
let state = self.state.read( ).await;
state.entries.is_empty( )
}
#[ inline ]
pub async fn config( &self ) -> CacheConfig
{
let state = self.state.read( ).await;
state.config.clone( )
}
}
impl< K, V > core::fmt::Debug for Cache< K, V >
{
#[ inline ]
fn fmt( &self, f : &mut core::fmt::Formatter< '_ > ) -> core::fmt::Result
{
f.debug_struct( "Cache" )
.field( "state", &"< CacheState >" )
.finish( )
}
}
#[ derive( Debug ) ]
pub enum CacheError
{
CacheFull,
NotFound,
}
impl core::fmt::Display for CacheError
{
#[ inline ]
fn fmt( &self, f : &mut core::fmt::Formatter< '_ > ) -> core::fmt::Result
{
match self
{
Self::CacheFull => write!( f, "Cache is full" ),
Self::NotFound => write!( f, "Entry not found in cache" ),
}
}
}
impl std::error::Error for CacheError {}