use crate::guardian::error::{GuardianError, Result};
use blake3::Hasher;
use bytes::Bytes;
use iroh_blobs::BlobFormat;
use lru::LruCache;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, info, instrument, warn};
pub struct OptimizedCache {
data_cache: Arc<RwLock<LruCache<String, CacheEntry>>>,
metadata_cache: Arc<RwLock<HashMap<String, MetadataEntry>>>,
compressed_cache: Arc<RwLock<LruCache<String, CompressedEntry>>>,
stats: Arc<RwLock<CacheStats>>,
cache_config: CacheConfig,
access_predictor: Arc<Mutex<AccessPredictor>>,
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub data: Bytes,
pub created_at: Instant,
pub last_accessed: Instant,
pub access_count: u64,
pub priority: u8,
pub original_size: usize,
pub integrity_hash: [u8; 32],
}
#[derive(Debug, Clone)]
pub struct CompressedEntry {
pub compressed_data: Bytes,
pub original_size: usize,
pub compression_level: i32,
pub compressed_at: Instant,
pub compression_ratio: f64,
}
#[derive(Debug, Clone)]
pub struct MetadataEntry {
pub size: u64,
pub format: BlobFormat,
pub providers: Vec<String>,
pub discovered_at: Instant,
pub avg_access_latency_ms: f64,
pub popularity_score: f64,
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub data_cache_hits: u64,
pub data_cache_misses: u64,
pub compressed_cache_hits: u64,
pub compressed_cache_misses: u64,
pub total_bytes_cached: u64,
pub bytes_saved_compression: u64,
pub bytes_saved_network: u64,
pub avg_access_time_us: f64,
pub hit_rate: f64,
pub evictions_count: u64,
pub compressions_count: u64,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_data_cache_size: usize,
pub max_data_entries: usize,
pub max_compressed_cache_size: usize,
pub max_compressed_entries: usize,
pub default_ttl_secs: u64,
pub compression_threshold: usize,
pub compression_level: i32,
pub eviction_threshold: f64,
pub enable_access_prediction: bool,
}
#[derive(Debug)]
pub struct AccessPredictor {
access_history: HashMap<String, Vec<Instant>>,
#[allow(dead_code)]
patterns: HashMap<String, AccessPattern>,
analysis_window_secs: u64,
}
#[derive(Debug, Clone)]
pub struct AccessPattern {
pub avg_frequency: f64,
pub peak_hours: Vec<u8>,
pub reaccess_probability: f64,
pub pattern_type: PatternType,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternType {
OneTime,
Regular,
Burst,
Seasonal,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_data_cache_size: 256 * 1024 * 1024, max_data_entries: 10_000,
max_compressed_cache_size: 1024 * 1024 * 1024, max_compressed_entries: 50_000,
default_ttl_secs: 3600, compression_threshold: 64 * 1024, compression_level: 6, eviction_threshold: 0.85,
enable_access_prediction: true,
}
}
}
impl OptimizedCache {
pub fn new(cache_config: CacheConfig) -> Self {
let data_cache_size = NonZeroUsize::new(cache_config.max_data_entries)
.unwrap_or(NonZeroUsize::new(10_000).unwrap());
let compressed_cache_size = NonZeroUsize::new(cache_config.max_compressed_entries)
.unwrap_or(NonZeroUsize::new(50_000).unwrap());
Self {
data_cache: Arc::new(RwLock::new(LruCache::new(data_cache_size))),
metadata_cache: Arc::new(RwLock::new(HashMap::new())),
compressed_cache: Arc::new(RwLock::new(LruCache::new(compressed_cache_size))),
stats: Arc::new(RwLock::new(CacheStats::default())),
cache_config,
access_predictor: Arc::new(Mutex::new(AccessPredictor {
access_history: HashMap::new(),
patterns: HashMap::new(),
analysis_window_secs: 3600 * 24, })),
}
}
#[instrument(skip(self))]
pub async fn get(&self, cid: &str) -> Option<Bytes> {
let start_time = Instant::now();
if self.cache_config.enable_access_prediction {
self.update_access_history(cid).await;
}
{
let mut cache = self.data_cache.write().await;
if let Some(entry) = cache.get_mut(cid) {
entry.last_accessed = Instant::now();
entry.access_count += 1;
let mut stats = self.stats.write().await;
stats.data_cache_hits += 1;
stats.avg_access_time_us =
(stats.avg_access_time_us + start_time.elapsed().as_micros() as f64) / 2.0;
debug!("Cache hit (data): {} ({} bytes)", cid, entry.data.len());
return Some(entry.data.clone());
}
}
{
let mut compressed_cache = self.compressed_cache.write().await;
if let Some(compressed_entry) = compressed_cache.get_mut(cid) {
match self
.decompress_data(
&compressed_entry.compressed_data,
compressed_entry.original_size,
)
.await
{
Ok(decompressed) => {
let cache_entry = CacheEntry {
data: decompressed.clone(),
created_at: compressed_entry.compressed_at,
last_accessed: Instant::now(),
access_count: 1,
priority: 7, original_size: compressed_entry.original_size,
integrity_hash: self.calculate_hash(&decompressed),
};
{
let mut data_cache = self.data_cache.write().await;
data_cache.put(cid.to_string(), cache_entry);
}
let mut stats = self.stats.write().await;
stats.compressed_cache_hits += 1;
stats.avg_access_time_us = (stats.avg_access_time_us
+ start_time.elapsed().as_micros() as f64)
/ 2.0;
debug!(
"Cache hit (compressed): {} ({} bytes decompressed)",
cid,
decompressed.len()
);
return Some(decompressed);
}
Err(e) => {
warn!("Failed to decompress cached data for {}: {}", cid, e);
compressed_cache.pop(cid);
}
}
}
}
let mut stats = self.stats.write().await;
stats.data_cache_misses += 1;
stats.compressed_cache_misses += 1;
debug!("Cache miss: {}", cid);
None
}
#[instrument(skip(self, data))]
pub async fn put(&self, cid: &str, data: Bytes) -> Result<()> {
let data_size = data.len();
let integrity_hash = self.calculate_hash(&data);
let should_compress = data_size >= self.cache_config.compression_threshold;
if should_compress {
match self.compress_data(&data).await {
Ok((compressed_data, compression_ratio)) => {
let compressed_entry = CompressedEntry {
compressed_data,
original_size: data_size,
compression_level: self.cache_config.compression_level,
compressed_at: Instant::now(),
compression_ratio,
};
{
let mut compressed_cache = self.compressed_cache.write().await;
compressed_cache.put(cid.to_string(), compressed_entry);
}
let mut stats = self.stats.write().await;
stats.compressions_count += 1;
stats.bytes_saved_compression +=
(data_size as f64 * (1.0 - compression_ratio)) as u64;
stats.total_bytes_cached += data_size as u64;
info!(
"Data compressed and stored: {} ({} bytes -> {} bytes, ratio: {:.2})",
cid,
data_size,
(data_size as f64 * compression_ratio) as usize,
compression_ratio
);
}
Err(e) => {
warn!(
"Compression failed for {}: {}. Storing without compression.",
cid, e
);
self.store_uncompressed(cid, data, integrity_hash).await?;
}
}
} else {
self.store_uncompressed(cid, data, integrity_hash).await?;
}
self.check_and_evict().await?;
Ok(())
}
async fn store_uncompressed(
&self,
cid: &str,
data: Bytes,
integrity_hash: [u8; 32],
) -> Result<()> {
let cache_entry = CacheEntry {
data: data.clone(),
created_at: Instant::now(),
last_accessed: Instant::now(),
access_count: 1,
priority: 5, original_size: data.len(),
integrity_hash,
};
{
let mut data_cache = self.data_cache.write().await;
data_cache.put(cid.to_string(), cache_entry);
}
let mut stats = self.stats.write().await;
stats.total_bytes_cached += data.len() as u64;
debug!(
"Data stored (without compression): {} ({} bytes)",
cid,
data.len()
);
Ok(())
}
async fn compress_data(&self, data: &Bytes) -> Result<(Bytes, f64)> {
let original_size = data.len();
let compressed = tokio::task::spawn_blocking({
let data = data.clone();
let compression_level = self.cache_config.compression_level;
move || {
zstd::bulk::compress(&data, compression_level)
.map_err(|e| GuardianError::Other(format!("Compression failed: {}", e)))
}
})
.await
.map_err(|e| GuardianError::Other(format!("Compression task failed: {}", e)))??;
let compressed_size = compressed.len();
let compression_ratio = compressed_size as f64 / original_size as f64;
Ok((Bytes::from(compressed), compression_ratio))
}
async fn decompress_data(
&self,
compressed_data: &Bytes,
expected_size: usize,
) -> Result<Bytes> {
let decompressed = tokio::task::spawn_blocking({
let compressed_data = compressed_data.clone();
move || {
zstd::bulk::decompress(&compressed_data, expected_size)
.map_err(|e| GuardianError::Other(format!("Decompression failed: {}", e)))
}
})
.await
.map_err(|e| GuardianError::Other(format!("Decompression task failed: {}", e)))??;
Ok(Bytes::from(decompressed))
}
fn calculate_hash(&self, data: &Bytes) -> [u8; 32] {
let mut hasher = Hasher::new();
hasher.update(data);
hasher.finalize().into()
}
async fn update_access_history(&self, cid: &str) {
let mut predictor = self.access_predictor.lock().await;
let now = Instant::now();
predictor
.access_history
.entry(cid.to_string())
.or_insert_with(Vec::new)
.push(now);
let analysis_window = predictor.analysis_window_secs; if let Some(history) = predictor.access_history.get_mut(cid) {
if let Some(cutoff) = now.checked_sub(Duration::from_secs(analysis_window)) {
history.retain(|&access_time| access_time > cutoff);
}
}
}
async fn check_and_evict(&self) -> Result<()> {
let stats = self.stats.read().await;
let current_usage = stats.total_bytes_cached as f64;
let max_usage = (self.cache_config.max_data_cache_size
+ self.cache_config.max_compressed_cache_size) as f64;
if current_usage / max_usage > self.cache_config.eviction_threshold {
drop(stats); self.intelligent_eviction().await?;
}
Ok(())
}
async fn intelligent_eviction(&self) -> Result<()> {
debug!("Starting intelligent cache eviction");
let candidates = {
let data_cache = self.data_cache.read().await;
data_cache
.iter()
.map(|(cid, entry)| {
let age_score = Instant::now()
.saturating_duration_since(entry.last_accessed)
.as_secs() as f64;
let frequency_score = 1.0 / (entry.access_count as f64 + 1.0);
let priority_score = (10 - entry.priority) as f64;
let eviction_score =
age_score * 0.4 + frequency_score * 0.3 + priority_score * 0.3;
(cid.clone(), eviction_score, entry.data.len())
})
.collect::<Vec<_>>()
};
let mut sorted_candidates = candidates;
sorted_candidates
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let eviction_count = (sorted_candidates.len() as f64 * 0.2).ceil() as usize;
let mut bytes_freed = 0u64;
{
let mut data_cache = self.data_cache.write().await;
for (cid, _score, size) in sorted_candidates.iter().take(eviction_count) {
if data_cache.pop(cid).is_some() {
bytes_freed += *size as u64;
}
}
}
{
let mut stats = self.stats.write().await;
stats.evictions_count += eviction_count as u64;
stats.total_bytes_cached = stats.total_bytes_cached.saturating_sub(bytes_freed);
}
info!(
"Eviction complete: {} entries removed, {} bytes freed",
eviction_count, bytes_freed
);
Ok(())
}
pub async fn get_stats(&self) -> CacheStats {
let stats = self.stats.read().await;
let mut stats_copy = stats.clone();
let total_requests = stats_copy.data_cache_hits
+ stats_copy.data_cache_misses
+ stats_copy.compressed_cache_hits
+ stats_copy.compressed_cache_misses;
let total_hits = stats_copy.data_cache_hits + stats_copy.compressed_cache_hits;
if total_requests > 0 {
stats_copy.hit_rate = total_hits as f64 / total_requests as f64;
}
stats_copy
}
pub async fn clear(&self) -> Result<()> {
{
let mut data_cache = self.data_cache.write().await;
data_cache.clear();
}
{
let mut compressed_cache = self.compressed_cache.write().await;
compressed_cache.clear();
}
{
let mut metadata_cache = self.metadata_cache.write().await;
metadata_cache.clear();
}
{
let mut stats = self.stats.write().await;
*stats = CacheStats::default();
}
info!("Cache cleared completely");
Ok(())
}
}