mod backend;
mod invalidation;
mod key;
mod memory;
mod options;
mod redis;
mod stats;
mod tiered;
pub use backend::{CacheBackend, CacheEntry, CacheError, CacheResult};
pub use invalidation::{EntityTag, InvalidationEvent, InvalidationStrategy};
pub use key::{CacheKey, CacheKeyBuilder, KeyPattern};
pub use memory::{MemoryCache, MemoryCacheBuilder, MemoryCacheConfig};
pub use options::{CacheOptions, CachePolicy, WritePolicy};
pub use redis::{RedisCache, RedisCacheConfig, RedisConnection};
pub use stats::{CacheMetrics, CacheStats};
pub use tiered::{TieredCache, TieredCacheConfig};
use std::sync::Arc;
#[derive(Clone)]
pub struct CacheManager<B: CacheBackend> {
backend: Arc<B>,
default_options: CacheOptions,
metrics: Arc<CacheMetrics>,
}
impl<B: CacheBackend> CacheManager<B> {
pub fn new(backend: B) -> Self {
Self {
backend: Arc::new(backend),
default_options: CacheOptions::default(),
metrics: Arc::new(CacheMetrics::new()),
}
}
pub fn with_options(backend: B, options: CacheOptions) -> Self {
Self {
backend: Arc::new(backend),
default_options: options,
metrics: Arc::new(CacheMetrics::new()),
}
}
pub fn backend(&self) -> &B {
&self.backend
}
pub fn metrics(&self) -> &CacheMetrics {
&self.metrics
}
pub async fn get<T>(&self, key: &CacheKey) -> CacheResult<Option<T>>
where
T: serde::de::DeserializeOwned,
{
let start = std::time::Instant::now();
let result = self.backend.get(key).await;
let duration = start.elapsed();
match &result {
Ok(Some(_)) => self.metrics.record_hit(duration),
Ok(None) => self.metrics.record_miss(duration),
Err(_) => self.metrics.record_error(),
}
result
}
pub async fn set<T>(
&self,
key: &CacheKey,
value: &T,
options: Option<&CacheOptions>,
) -> CacheResult<()>
where
T: serde::Serialize + Sync,
{
let opts = options.unwrap_or(&self.default_options);
let start = std::time::Instant::now();
let result = self.backend.set(key, value, opts.ttl).await;
let duration = start.elapsed();
if result.is_ok() {
self.metrics.record_write(duration);
} else {
self.metrics.record_error();
}
result
}
pub async fn get_or_set<T, F, Fut>(
&self,
key: &CacheKey,
f: F,
options: Option<&CacheOptions>,
) -> CacheResult<T>
where
T: serde::Serialize + serde::de::DeserializeOwned + Sync,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = CacheResult<T>>,
{
if let Some(value) = self.get::<T>(key).await? {
return Ok(value);
}
let value = f().await?;
let _ = self.set(key, &value, options).await;
Ok(value)
}
pub async fn delete(&self, key: &CacheKey) -> CacheResult<bool> {
self.backend.delete(key).await
}
pub async fn exists(&self, key: &CacheKey) -> CacheResult<bool> {
self.backend.exists(key).await
}
pub async fn invalidate_pattern(&self, pattern: &KeyPattern) -> CacheResult<u64> {
self.backend.invalidate_pattern(pattern).await
}
pub async fn invalidate_entity(&self, entity: &str) -> CacheResult<u64> {
let pattern = KeyPattern::entity(entity);
self.invalidate_pattern(&pattern).await
}
pub async fn invalidate_record<I: std::fmt::Display>(
&self,
entity: &str,
id: I,
) -> CacheResult<u64> {
let pattern = KeyPattern::record(entity, id);
self.invalidate_pattern(&pattern).await
}
pub async fn invalidate_tags(&self, tags: &[EntityTag]) -> CacheResult<u64> {
self.backend.invalidate_tags(tags).await
}
pub async fn clear(&self) -> CacheResult<()> {
self.backend.clear().await
}
pub fn stats(&self) -> CacheStats {
self.metrics.snapshot()
}
}
pub struct CacheManagerBuilder {
default_options: CacheOptions,
}
impl Default for CacheManagerBuilder {
fn default() -> Self {
Self::new()
}
}
impl CacheManagerBuilder {
pub fn new() -> Self {
Self {
default_options: CacheOptions::default(),
}
}
pub fn default_options(mut self, options: CacheOptions) -> Self {
self.default_options = options;
self
}
pub fn memory(self, config: MemoryCacheConfig) -> CacheManager<MemoryCache> {
let backend = MemoryCache::new(config);
CacheManager::with_options(backend, self.default_options)
}
pub async fn redis(self, config: RedisCacheConfig) -> CacheResult<CacheManager<RedisCache>> {
let backend = RedisCache::new(config).await?;
Ok(CacheManager::with_options(backend, self.default_options))
}
pub async fn tiered(
self,
memory_config: MemoryCacheConfig,
redis_config: RedisCacheConfig,
) -> CacheResult<CacheManager<TieredCache<MemoryCache, RedisCache>>> {
let memory = MemoryCache::new(memory_config);
let redis = RedisCache::new(redis_config).await?;
let backend = TieredCache::new(memory, redis);
Ok(CacheManager::with_options(backend, self.default_options))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_memory_cache_basic() {
let cache = CacheManager::new(MemoryCache::new(MemoryCacheConfig::default()));
let key = CacheKey::new("test", "key1");
cache.set(&key, &"hello world", None).await.unwrap();
let value: Option<String> = cache.get(&key).await.unwrap();
assert_eq!(value, Some("hello world".to_string()));
cache.delete(&key).await.unwrap();
let value: Option<String> = cache.get(&key).await.unwrap();
assert!(value.is_none());
}
#[tokio::test]
async fn test_get_or_set() {
let cache = CacheManager::new(MemoryCache::new(MemoryCacheConfig::default()));
let key = CacheKey::new("test", "computed");
let mut call_count = 0;
let value: String = cache
.get_or_set(
&key,
|| {
call_count += 1;
async { Ok("computed value".to_string()) }
},
None,
)
.await
.unwrap();
assert_eq!(value, "computed value");
assert_eq!(call_count, 1);
let value: String = cache
.get_or_set(
&key,
|| {
call_count += 1;
async { Ok("should not be called".to_string()) }
},
None,
)
.await
.unwrap();
assert_eq!(value, "computed value");
assert_eq!(call_count, 1); }
}