#[cfg(feature = "caching")]
use moka::future::Cache;
#[cfg(feature = "caching")]
use std::hash::Hash;
#[cfg(feature = "caching")]
use std::sync::Arc;
#[cfg(feature = "caching")]
use std::time::Duration;
#[cfg(feature = "caching")]
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_capacity: u64,
pub ttl_seconds: u64,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_capacity: 10_000, ttl_seconds: 300, }
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub total_lookups: u64,
pub hits: u64,
pub misses: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
if self.total_lookups == 0 {
0.0
} else {
(self.hits as f64 / self.total_lookups as f64) * 100.0
}
}
pub fn miss_rate(&self) -> f64 {
100.0 - self.hit_rate()
}
}
#[cfg(feature = "caching")]
pub struct QueryCache<K, V> {
cache: Cache<K, V>,
stats: Arc<RwLock<CacheStats>>,
}
#[cfg(feature = "caching")]
impl<K, V> QueryCache<K, V>
where
K: Hash + Eq + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
pub fn new(config: CacheConfig) -> Self {
let cache = Cache::builder()
.max_capacity(config.max_capacity)
.time_to_live(Duration::from_secs(config.ttl_seconds))
.build();
Self {
cache,
stats: Arc::new(RwLock::new(CacheStats::default())),
}
}
pub async fn insert(&self, key: K, value: V) {
self.cache.insert(key, value).await;
}
pub async fn get(&self, key: &K) -> Option<V>
where
K: Clone,
{
let mut stats = self.stats.write().await;
stats.total_lookups += 1;
if let Some(value) = self.cache.get(key).await {
stats.hits += 1;
Some(value)
} else {
stats.misses += 1;
None
}
}
pub async fn get_or_insert<F, Fut>(&self, key: K, f: F) -> V
where
K: Clone,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = V>,
{
if let Some(value) = self.get(&key).await {
return value;
}
let value = f().await;
self.insert(key, value.clone()).await;
value
}
pub async fn invalidate(&self, key: &K) {
self.cache.invalidate(key).await;
}
pub async fn clear(&self) {
self.cache.invalidate_all();
self.cache.run_pending_tasks().await;
}
pub async fn stats(&self) -> CacheStats {
self.stats.read().await.clone()
}
pub async fn reset_stats(&self) {
let mut stats = self.stats.write().await;
*stats = CacheStats::default();
}
pub fn entry_count(&self) -> u64 {
self.cache.entry_count()
}
}
#[cfg(not(feature = "caching"))]
pub struct QueryCache<K, V> {
_phantom: std::marker::PhantomData<(K, V)>,
}
#[cfg(not(feature = "caching"))]
impl<K, V> QueryCache<K, V> {
pub fn new(_config: CacheConfig) -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub async fn insert(&self, _key: K, _value: V) {}
pub async fn get(&self, _key: &K) -> Option<V> {
None
}
pub async fn get_or_insert<F, Fut>(&self, _key: K, f: F) -> V
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = V>,
{
f().await
}
pub async fn invalidate(&self, _key: &K) {}
pub async fn clear(&self) {}
pub async fn stats(&self) -> CacheStats {
CacheStats::default()
}
pub async fn reset_stats(&self) {}
pub fn entry_count(&self) -> u64 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[cfg(feature = "caching")]
async fn test_cache_basic_operations() {
let cache = QueryCache::new(CacheConfig {
max_capacity: 100,
ttl_seconds: 60,
});
cache.insert("key1".to_string(), "value1".to_string()).await;
let value = cache.get(&"key1".to_string()).await;
assert_eq!(value, Some("value1".to_string()));
let missing = cache.get(&"nonexistent".to_string()).await;
assert_eq!(missing, None);
}
#[tokio::test]
#[cfg(feature = "caching")]
async fn test_cache_statistics() {
let cache = QueryCache::new(CacheConfig::default());
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
cache.insert(1, "one".to_string()).await;
let _ = cache.get(&1).await;
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 1);
assert_eq!(stats.hits, 1);
assert_eq!(stats.hit_rate(), 100.0);
let _ = cache.get(&2).await;
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 2);
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.hit_rate(), 50.0);
}
#[tokio::test]
#[cfg(feature = "caching")]
async fn test_get_or_insert() {
let cache = QueryCache::new(CacheConfig::default());
let mut call_count = 0;
let value1 = cache
.get_or_insert(1, || async {
call_count += 1;
"computed".to_string()
})
.await;
assert_eq!(value1, "computed");
assert_eq!(call_count, 1);
let value2 = cache
.get_or_insert(1, || async {
call_count += 1;
"should_not_be_called".to_string()
})
.await;
assert_eq!(value2, "computed");
assert_eq!(call_count, 1);
let stats = cache.stats().await;
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
#[cfg(feature = "caching")]
async fn test_cache_invalidation() {
let cache = QueryCache::new(CacheConfig::default());
cache.insert("key", "value".to_string()).await;
assert!(cache.get(&"key").await.is_some());
cache.invalidate(&"key").await;
assert!(cache.get(&"key").await.is_none());
}
#[tokio::test]
#[cfg(feature = "caching")]
async fn test_cache_clear() {
let cache = QueryCache::new(CacheConfig::default());
cache.insert(1, "one".to_string()).await;
cache.insert(2, "two".to_string()).await;
cache.insert(3, "three".to_string()).await;
assert!(cache.get(&1).await.is_some());
assert!(cache.get(&2).await.is_some());
assert!(cache.get(&3).await.is_some());
cache.clear().await;
assert!(cache.get(&1).await.is_none());
assert!(cache.get(&2).await.is_none());
assert!(cache.get(&3).await.is_none());
}
#[tokio::test]
#[cfg(not(feature = "caching"))]
async fn test_no_op_cache() {
let cache = QueryCache::new(CacheConfig::default());
cache.insert("key", "value".to_string()).await;
assert_eq!(cache.get(&"key").await, None);
let value = cache
.get_or_insert("key", || async { "computed".to_string() })
.await;
assert_eq!(value, "computed");
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 0);
assert_eq!(cache.entry_count(), 0);
}
}