#![allow(clippy::uninlined_format_args)]
use super::config::{CacheConfig, CacheStats};
use super::stats::CacheStatsInner;
use crate::TursoStorage;
use do_memory_core::{Episode, Heuristic, Pattern, Result};
use do_memory_storage_redb::{AdaptiveCache, AdaptiveCacheConfig};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use uuid::Uuid;
pub struct CachedTursoStorage {
pub(crate) storage: Arc<TursoStorage>,
episode_cache: Option<AdaptiveCache<Episode>>,
pattern_cache: Option<AdaptiveCache<Pattern>>,
heuristic_cache: Option<AdaptiveCache<Heuristic>>,
config: CacheConfig,
stats: CacheStatsInner,
}
impl CachedTursoStorage {
pub fn new(storage: TursoStorage, config: CacheConfig) -> Self {
let episode_cache = if config.enable_episode_cache {
let cache_config = AdaptiveCacheConfig {
max_size: config.max_episodes,
default_ttl: config.episode_ttl,
min_ttl: config.min_ttl,
max_ttl: config.max_ttl,
hot_threshold: config.hot_threshold,
cold_threshold: config.cold_threshold,
adaptation_rate: config.adaptation_rate,
window_size: 20,
cleanup_interval_secs: config.cleanup_interval_secs,
enable_background_cleanup: config.enable_background_cleanup,
};
Some(AdaptiveCache::new(cache_config))
} else {
None
};
let pattern_cache = if config.enable_pattern_cache {
let cache_config = AdaptiveCacheConfig {
max_size: config.max_patterns,
default_ttl: config.pattern_ttl,
min_ttl: config.min_ttl,
max_ttl: config.max_ttl,
hot_threshold: config.hot_threshold,
cold_threshold: config.cold_threshold,
adaptation_rate: config.adaptation_rate,
window_size: 20,
cleanup_interval_secs: config.cleanup_interval_secs,
enable_background_cleanup: config.enable_background_cleanup,
};
Some(AdaptiveCache::new(cache_config))
} else {
None
};
let heuristic_cache = if config.enable_pattern_cache {
let cache_config = AdaptiveCacheConfig {
max_size: config.max_patterns / 2, default_ttl: config.pattern_ttl,
min_ttl: config.min_ttl,
max_ttl: config.max_ttl,
hot_threshold: config.hot_threshold,
cold_threshold: config.cold_threshold,
adaptation_rate: config.adaptation_rate,
window_size: 20,
cleanup_interval_secs: config.cleanup_interval_secs,
enable_background_cleanup: config.enable_background_cleanup,
};
Some(AdaptiveCache::new(cache_config))
} else {
None
};
Self {
storage: Arc::new(storage),
episode_cache,
pattern_cache,
heuristic_cache,
config,
stats: CacheStatsInner::default(),
}
}
pub fn storage(&self) -> &TursoStorage {
&self.storage
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
pub fn stats(&self) -> CacheStats {
self.stats.snapshot()
}
pub async fn get_episode_cached(&self, id: Uuid) -> Result<Option<Episode>> {
if let Some(ref cache) = self.episode_cache {
if let Some(episode) = cache.get_and_record(id).await {
self.stats.episode_hits.fetch_add(1, Ordering::Relaxed);
return Ok(Some(episode));
}
}
self.stats.episode_misses.fetch_add(1, Ordering::Relaxed);
let episode = self.storage.get_episode(id).await?;
if let (Some(ep), Some(cache)) = (&episode, &self.episode_cache) {
cache.record_access(id, false, Some(ep.clone())).await;
}
Ok(episode)
}
pub async fn get_pattern_cached(
&self,
id: do_memory_core::episode::PatternId,
) -> Result<Option<Pattern>> {
let cache_key = id;
if let Some(ref cache) = self.pattern_cache {
if let Some(pattern) = cache.get_and_record(cache_key).await {
self.stats.pattern_hits.fetch_add(1, Ordering::Relaxed);
return Ok(Some(pattern));
}
}
self.stats.pattern_misses.fetch_add(1, Ordering::Relaxed);
let pattern = self.storage.get_pattern(id).await?;
if let (Some(pat), Some(cache)) = (&pattern, &self.pattern_cache) {
cache
.record_access(cache_key, false, Some(pat.clone()))
.await;
}
Ok(pattern)
}
pub async fn get_heuristic_cached(&self, id: Uuid) -> Result<Option<Heuristic>> {
if let Some(ref cache) = self.heuristic_cache {
if let Some(heuristic) = cache.get_and_record(id).await {
self.stats.heuristic_hits.fetch_add(1, Ordering::Relaxed);
return Ok(Some(heuristic));
}
}
self.stats.heuristic_misses.fetch_add(1, Ordering::Relaxed);
let heuristic = self.storage.get_heuristic(id).await?;
if let (Some(h), Some(cache)) = (&heuristic, &self.heuristic_cache) {
cache.record_access(id, false, Some(h.clone())).await;
}
Ok(heuristic)
}
pub async fn store_episode_cached(&self, episode: &Episode) -> Result<()> {
self.storage.store_episode(episode).await?;
if let Some(ref cache) = self.episode_cache {
cache.remove(episode.episode_id).await;
}
Ok(())
}
pub async fn store_pattern_cached(&self, pattern: &Pattern) -> Result<()> {
self.storage.store_pattern(pattern).await?;
if let Some(ref cache) = self.pattern_cache {
cache.remove(pattern.id()).await;
}
Ok(())
}
pub async fn store_heuristic_cached(&self, heuristic: &Heuristic) -> Result<()> {
self.storage.store_heuristic(heuristic).await?;
if let Some(ref cache) = self.heuristic_cache {
cache.remove(heuristic.heuristic_id).await;
}
Ok(())
}
pub async fn delete_episode_cached(&self, id: Uuid) -> Result<()> {
self.storage.delete_episode(id).await?;
if let Some(ref cache) = self.episode_cache {
cache.remove(id).await;
}
Ok(())
}
pub async fn clear_caches(&self) {
if let Some(ref cache) = self.episode_cache {
cache.clear().await;
}
if let Some(ref cache) = self.pattern_cache {
cache.clear().await;
}
if let Some(ref cache) = self.heuristic_cache {
cache.clear().await;
}
}
pub fn record_query_hit(&self) {
self.stats.record_query_hit();
}
pub fn record_query_miss(&self) {
self.stats.record_query_miss();
}
pub fn record_eviction(&self) {
self.stats.record_eviction();
}
pub fn record_expiration(&self) {
self.stats.record_expiration();
}
pub async fn cache_sizes(&self) -> (usize, usize, usize) {
let episode_size = if let Some(ref cache) = self.episode_cache {
cache.len().await
} else {
0
};
let pattern_size = if let Some(ref cache) = self.pattern_cache {
cache.len().await
} else {
0
};
let heuristic_size = if let Some(ref cache) = self.heuristic_cache {
cache.len().await
} else {
0
};
(episode_size, pattern_size, heuristic_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_stats() {
let stats = CacheStats {
episode_hits: 1,
episode_misses: 2,
pattern_hits: 3,
pattern_misses: 4,
query_hits: 0,
query_misses: 0,
evictions: 0,
expirations: 0,
};
assert_eq!(stats.episode_hits, 1);
assert_eq!(stats.query_hits, 0);
}
#[test]
fn test_cache_stats_hit_rate() {
let stats = CacheStats {
episode_hits: 8,
episode_misses: 2,
pattern_hits: 0,
pattern_misses: 0,
query_hits: 0,
query_misses: 0,
evictions: 0,
expirations: 0,
};
assert!((stats.hit_rate() - 0.8).abs() < f64::EPSILON);
assert!((stats.episode_hit_rate() - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_zero_requests() {
let stats = CacheStats::default();
assert_eq!(stats.hit_rate(), 0.0);
assert_eq!(stats.query_hit_rate(), 0.0);
}
#[test]
fn test_cache_stats_query_hit_rate() {
let stats = CacheStats {
query_hits: 3,
query_misses: 7,
..Default::default()
};
assert!((stats.query_hit_rate() - 0.3).abs() < f64::EPSILON);
}
#[test]
fn test_cache_stats_pattern_hit_rate() {
let stats = CacheStats {
pattern_hits: 5,
pattern_misses: 5,
..Default::default()
};
assert!((stats.pattern_hit_rate() - 0.5).abs() < f64::EPSILON);
}
}