#![cfg_attr(coverage_nightly, coverage(off))]
use crate::services::cache::base::{CacheEntry, CacheStats, CacheStrategy};
use anyhow::{Context, Result};
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::fs;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Serialize, Deserialize, Clone)]
struct PersistentCacheEntry<V> {
value: V,
created_timestamp: u64, size_bytes: usize,
}
impl<V> PersistentCacheEntry<V> {
fn new(value: V, size_bytes: usize) -> Self {
let created_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
value,
created_timestamp,
size_bytes,
}
}
fn age(&self) -> Duration {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Duration::from_secs(now.saturating_sub(self.created_timestamp))
}
fn into_cache_entry(self) -> CacheEntry<V> {
let age = self.age();
let created = Instant::now().checked_sub(age).expect("internal error");
CacheEntry {
value: Arc::new(self.value),
created,
access_count: Arc::new(std::sync::atomic::AtomicU32::new(0)),
size_bytes: self.size_bytes,
last_accessed: Arc::new(parking_lot::Mutex::new(Instant::now())),
}
}
}
pub struct PersistentCache<T: CacheStrategy> {
memory_cache: Arc<RwLock<FxHashMap<String, CacheEntry<T::Value>>>>,
cache_dir: PathBuf,
strategy: Arc<T>,
pub stats: CacheStats,
}
impl<T: CacheStrategy> PersistentCache<T>
where
T::Value: Serialize + for<'de> Deserialize<'de>,
{
pub fn new(strategy: T, cache_dir: PathBuf) -> Result<Self> {
fs::create_dir_all(&cache_dir).with_context(|| {
format!("Failed to create cache directory: {}", cache_dir.display())
})?;
let mut cache = Self {
memory_cache: Arc::new(RwLock::new(FxHashMap::default())),
cache_dir,
strategy: Arc::new(strategy),
stats: CacheStats::new(),
};
cache.load_from_disk()?;
Ok(cache)
}
fn cache_file_path(&self, cache_key: &str) -> PathBuf {
let mut hasher = DefaultHasher::new();
cache_key.hash(&mut hasher);
let hash = hasher.finish();
self.cache_dir.join(format!("{hash:016x}.json"))
}
fn load_from_disk(&mut self) -> Result<()> {
if !self.cache_dir.exists() {
return Ok(());
}
let entries = fs::read_dir(&self.cache_dir).with_context(|| {
format!(
"Failed to read cache directory: {}",
self.cache_dir.display()
)
})?;
let mut loaded = 0;
let mut expired = 0;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
match self.load_cache_file(&path) {
Ok(true) => loaded += 1,
Ok(false) => expired += 1,
Err(e) => {
eprintln!(
"Warning: Failed to load cache file {}: {}",
path.display(),
e
);
let _ = fs::remove_file(&path);
}
}
}
}
eprintln!("Loaded {loaded} cache entries, expired {expired} entries");
Ok(())
}
fn load_cache_file(&self, path: &Path) -> Result<bool> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read cache file: {}", path.display()))?;
if let Ok(entry) = serde_json::from_str::<PersistentCacheEntry<T::Value>>(&content) {
if let Some(ttl) = self.strategy.ttl() {
if entry.age() > ttl {
let _ = fs::remove_file(path);
return Ok(false);
}
}
let size_bytes = entry.size_bytes;
let cache_entry = entry.into_cache_entry();
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
self.memory_cache
.write()
.insert(filename.to_string(), cache_entry);
self.stats.add_bytes(size_bytes);
return Ok(true);
}
Ok(false)
}
pub fn get(&self, key: &T::Key) -> Option<Arc<T::Value>> {
let cache_key = self.strategy.cache_key(key);
{
let mut memory = self.memory_cache.write();
if let Some(entry) = memory.get_mut(&cache_key) {
if let Some(ttl) = self.strategy.ttl() {
if entry.age() > ttl {
self.stats.remove_bytes(entry.size_bytes);
memory.remove(&cache_key);
let _ = fs::remove_file(self.cache_file_path(&cache_key));
self.stats.record_miss();
return None;
}
}
if self.strategy.validate(key, &entry.value) {
entry.access();
self.stats.record_hit();
return Some(entry.value.clone());
} else {
self.stats.remove_bytes(entry.size_bytes);
memory.remove(&cache_key);
let _ = fs::remove_file(self.cache_file_path(&cache_key));
self.stats.record_miss();
return None;
}
}
}
let cache_file = self.cache_file_path(&cache_key);
if cache_file.exists() {
if let Ok(content) = fs::read_to_string(&cache_file) {
if let Ok(persistent_entry) =
serde_json::from_str::<PersistentCacheEntry<T::Value>>(&content)
{
if let Some(ttl) = self.strategy.ttl() {
if persistent_entry.age() > ttl {
let _ = fs::remove_file(&cache_file);
self.stats.record_miss();
return None;
}
}
let size_bytes = persistent_entry.size_bytes;
let cache_entry = persistent_entry.into_cache_entry();
if self.strategy.validate(key, &cache_entry.value) {
cache_entry.access();
let value = cache_entry.value.clone();
self.memory_cache.write().insert(cache_key, cache_entry);
self.stats.add_bytes(size_bytes);
self.stats.record_hit();
return Some(value);
} else {
let _ = fs::remove_file(&cache_file);
self.stats.record_miss();
return None;
}
}
}
let _ = fs::remove_file(&cache_file);
}
self.stats.record_miss();
None
}
pub fn put(&self, key: T::Key, value: T::Value) -> Result<()> {
let cache_key = self.strategy.cache_key(&key);
let size_bytes = self.estimate_size(&value);
let persistent_entry = PersistentCacheEntry::new(value.clone(), size_bytes);
let cache_file = self.cache_file_path(&cache_key);
if let Ok(content) = serde_json::to_string(&persistent_entry) {
if let Err(e) = fs::write(&cache_file, content) {
eprintln!(
"Warning: Failed to write cache file {}: {}",
cache_file.display(),
e
);
}
}
let cache_entry = CacheEntry::new(value, size_bytes);
self.memory_cache.write().insert(cache_key, cache_entry);
self.stats.add_bytes(size_bytes);
Ok(())
}
fn estimate_size(&self, _value: &T::Value) -> usize {
std::mem::size_of::<T::Value>()
}
pub fn cleanup_expired(&self) {
let mut to_remove = Vec::new();
{
let memory = self.memory_cache.read();
for (key, entry) in memory.iter() {
if let Some(ttl) = self.strategy.ttl() {
if entry.age() > ttl {
to_remove.push(key.clone());
}
}
}
}
{
let mut memory = self.memory_cache.write();
for key in &to_remove {
if let Some(entry) = memory.remove(key) {
self.stats.remove_bytes(entry.size_bytes);
let _ = fs::remove_file(self.cache_file_path(key));
}
}
}
if let Ok(entries) = fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(persistent_entry) =
serde_json::from_str::<PersistentCacheEntry<T::Value>>(&content)
{
if let Some(ttl) = self.strategy.ttl() {
if persistent_entry.age() > ttl {
let _ = fs::remove_file(&path);
}
}
}
}
}
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.memory_cache.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.memory_cache.read().is_empty()
}
pub fn remove(&self, key: &T::Key) -> Option<Arc<T::Value>> {
let cache_key = self.strategy.cache_key(key);
let value = self.memory_cache.write().remove(&cache_key).map(|entry| {
self.stats.remove_bytes(entry.size_bytes);
entry.value
});
let cache_file = self.cache_file_path(&cache_key);
let _ = fs::remove_file(&cache_file);
value
}
pub fn clear(&self) -> Result<()> {
{
let mut memory = self.memory_cache.write();
for (_, entry) in memory.iter() {
self.stats.remove_bytes(entry.size_bytes);
}
memory.clear();
}
if let Ok(entries) = fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
let _ = fs::remove_file(&path);
}
}
}
Ok(())
}
pub fn evict_if_needed(&self) {
let max_size = self.strategy.max_size();
while self.len() > max_size {
let oldest_key = {
let memory = self.memory_cache.read();
memory
.iter()
.min_by_key(|(_, entry)| entry.created)
.map(|(key, _)| key.clone())
};
if let Some(key) = oldest_key {
let mut memory = self.memory_cache.write();
if let Some(entry) = memory.remove(&key) {
self.stats.remove_bytes(entry.size_bytes);
self.stats.record_eviction();
let _ = fs::remove_file(self.cache_file_path(&key));
}
} else {
break;
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_persistent_cache_entry_creation() {
let entry = PersistentCacheEntry::new("test_value".to_string(), 100);
assert_eq!(entry.value, "test_value");
assert_eq!(entry.size_bytes, 100);
assert!(entry.created_timestamp > 0);
}
#[test]
fn test_persistent_cache_entry_age() {
let entry = PersistentCacheEntry::new("test_value".to_string(), 100);
let age = entry.age();
assert!(age.as_secs() < 10); }
#[test]
fn test_cache_file_path() {
let temp_dir = TempDir::new().expect("internal error");
assert!(temp_dir.path().exists());
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}