Skip to main content

chie_core/
tiered_cache.rs

1//! Multi-level cache hierarchy for optimal performance and cost.
2//!
3//! This module implements a multi-tiered caching system with automatic promotion
4//! and demotion between levels based on access patterns:
5//! - L1: Hot data in memory (fastest, smallest)
6//! - L2: Warm data on SSD (fast, medium)
7//! - L3: Cold data on HDD (slow, largest)
8//!
9//! # Example
10//!
11//! ```rust
12//! use chie_core::tiered_cache::{TieredCache, TieredCacheConfig};
13//! use chie_core::compression::CompressionAlgorithm;
14//! use std::path::PathBuf;
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let config = TieredCacheConfig {
18//!     l1_capacity_bytes: 100 * 1024 * 1024,      // 100 MB in memory
19//!     l2_capacity_bytes: 1024 * 1024 * 1024,     // 1 GB on SSD
20//!     l3_capacity_bytes: 10 * 1024 * 1024 * 1024, // 10 GB on HDD
21//!     l2_path: PathBuf::from("/fast-ssd/cache"),
22//!     l3_path: PathBuf::from("/slow-hdd/cache"),
23//!     promotion_threshold: 3,  // Promote after 3 accesses
24//!     compression: CompressionAlgorithm::None,
25//! };
26//!
27//! let mut cache = TieredCache::new(config).await?;
28//!
29//! // Insert data (starts in L1)
30//! cache.put("key1".to_string(), b"hot data".to_vec()).await?;
31//!
32//! // Get data (automatically promotes if accessed frequently)
33//! if let Some(data) = cache.get("key1").await? {
34//!     println!("Found data: {} bytes", data.len());
35//! }
36//!
37//! // Get cache statistics
38//! let stats = cache.stats();
39//! println!("L1 hit rate: {:.2}%", stats.l1_hit_rate() * 100.0);
40//! # Ok(())
41//! # }
42//! ```
43
44use crate::compression::{CompressionAlgorithm, Compressor};
45use serde::{Deserialize, Serialize};
46use std::cell::RefCell;
47use std::collections::HashMap;
48use std::path::PathBuf;
49use thiserror::Error;
50use tokio::fs;
51use tokio::io::{AsyncReadExt, AsyncWriteExt};
52
53/// Tiered cache error types.
54#[derive(Debug, Error)]
55pub enum TieredCacheError {
56    #[error("IO error: {0}")]
57    Io(#[from] std::io::Error),
58
59    #[error("Key not found: {0}")]
60    KeyNotFound(String),
61
62    #[error("Tier full: {tier}")]
63    TierFull { tier: String },
64
65    #[error("Serialization error: {0}")]
66    Serialization(String),
67}
68
69/// Cache tier levels.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
71pub enum CacheTier {
72    /// L1: In-memory cache (hottest data).
73    L1 = 1,
74    /// L2: SSD cache (warm data).
75    L2 = 2,
76    /// L3: HDD cache (cold data).
77    L3 = 3,
78}
79
80impl CacheTier {
81    /// Get tier name.
82    #[must_use]
83    #[inline]
84    pub const fn name(&self) -> &'static str {
85        match self {
86            Self::L1 => "L1-Memory",
87            Self::L2 => "L2-SSD",
88            Self::L3 => "L3-HDD",
89        }
90    }
91}
92
93/// Metadata for a cached item.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95struct CacheItemMetadata {
96    key: String,
97    size_bytes: u64,
98    tier: CacheTier,
99    access_count: u64,
100    last_access_ms: i64,
101    created_ms: i64,
102}
103
104impl CacheItemMetadata {
105    /// Create new metadata.
106    fn new(key: String, size_bytes: u64, tier: CacheTier) -> Self {
107        let now_ms = std::time::SystemTime::now()
108            .duration_since(std::time::UNIX_EPOCH)
109            .unwrap_or_default()
110            .as_millis() as i64;
111
112        Self {
113            key,
114            size_bytes,
115            tier,
116            access_count: 0,
117            last_access_ms: now_ms,
118            created_ms: now_ms,
119        }
120    }
121
122    /// Record an access.
123    fn record_access(&mut self) {
124        self.access_count += 1;
125        self.last_access_ms = std::time::SystemTime::now()
126            .duration_since(std::time::UNIX_EPOCH)
127            .unwrap_or_default()
128            .as_millis() as i64;
129    }
130
131    /// Check if item should be promoted.
132    #[must_use]
133    #[inline]
134    const fn should_promote(&self, threshold: u64) -> bool {
135        self.access_count >= threshold
136    }
137}
138
139/// Configuration for tiered cache.
140#[derive(Debug, Clone)]
141pub struct TieredCacheConfig {
142    /// L1 (memory) capacity in bytes.
143    pub l1_capacity_bytes: u64,
144    /// L2 (SSD) capacity in bytes.
145    pub l2_capacity_bytes: u64,
146    /// L3 (HDD) capacity in bytes.
147    pub l3_capacity_bytes: u64,
148    /// Path for L2 cache.
149    pub l2_path: PathBuf,
150    /// Path for L3 cache.
151    pub l3_path: PathBuf,
152    /// Number of accesses before promotion.
153    pub promotion_threshold: u64,
154    /// Compression algorithm for L2/L3 tiers (None = no compression).
155    pub compression: CompressionAlgorithm,
156}
157
158impl Default for TieredCacheConfig {
159    fn default() -> Self {
160        Self {
161            l1_capacity_bytes: 100 * 1024 * 1024,       // 100 MB
162            l2_capacity_bytes: 1024 * 1024 * 1024,      // 1 GB
163            l3_capacity_bytes: 10 * 1024 * 1024 * 1024, // 10 GB
164            l2_path: PathBuf::from("./cache/l2"),
165            l3_path: PathBuf::from("./cache/l3"),
166            promotion_threshold: 3,
167            compression: CompressionAlgorithm::Balanced, // Default to balanced compression
168        }
169    }
170}
171
172/// Statistics for tiered cache.
173#[derive(Debug, Clone, Default)]
174pub struct TieredCacheStats {
175    /// L1 hits.
176    pub l1_hits: u64,
177    /// L2 hits.
178    pub l2_hits: u64,
179    /// L3 hits.
180    pub l3_hits: u64,
181    /// Cache misses.
182    pub misses: u64,
183    /// Items promoted from L2 to L1.
184    pub promotions_l2_to_l1: u64,
185    /// Items promoted from L3 to L2.
186    pub promotions_l3_to_l2: u64,
187    /// Items demoted from L1 to L2.
188    pub demotions_l1_to_l2: u64,
189    /// Items demoted from L2 to L3.
190    pub demotions_l2_to_l3: u64,
191    /// Items evicted from L3.
192    pub evictions: u64,
193}
194
195impl TieredCacheStats {
196    /// Calculate L1 hit rate.
197    #[must_use]
198    #[inline]
199    pub fn l1_hit_rate(&self) -> f64 {
200        let total = self.l1_hits + self.l2_hits + self.l3_hits + self.misses;
201        if total == 0 {
202            0.0
203        } else {
204            self.l1_hits as f64 / total as f64
205        }
206    }
207
208    /// Calculate overall hit rate.
209    #[must_use]
210    #[inline]
211    pub fn overall_hit_rate(&self) -> f64 {
212        let hits = self.l1_hits + self.l2_hits + self.l3_hits;
213        let total = hits + self.misses;
214        if total == 0 {
215            0.0
216        } else {
217            hits as f64 / total as f64
218        }
219    }
220
221    /// Calculate average tier (1.0 = all L1, 3.0 = all L3).
222    #[must_use]
223    #[inline]
224    pub fn average_tier(&self) -> f64 {
225        let hits = self.l1_hits + self.l2_hits + self.l3_hits;
226        if hits == 0 {
227            0.0
228        } else {
229            (self.l1_hits as f64 + self.l2_hits as f64 * 2.0 + self.l3_hits as f64 * 3.0)
230                / hits as f64
231        }
232    }
233}
234
235/// Multi-level cache with automatic tiering.
236pub struct TieredCache {
237    config: TieredCacheConfig,
238    /// L1 cache (in-memory).
239    l1: HashMap<String, Vec<u8>>,
240    /// Metadata for all items.
241    metadata: HashMap<String, CacheItemMetadata>,
242    /// Current usage per tier.
243    l1_used: u64,
244    l2_used: u64,
245    l3_used: u64,
246    /// Statistics.
247    stats: TieredCacheStats,
248    /// Compressor for L2/L3 tiers (RefCell for interior mutability).
249    compressor: RefCell<Compressor>,
250}
251
252impl TieredCache {
253    /// Create a new tiered cache.
254    pub async fn new(config: TieredCacheConfig) -> Result<Self, TieredCacheError> {
255        // Create directories for L2 and L3
256        fs::create_dir_all(&config.l2_path).await?;
257        fs::create_dir_all(&config.l3_path).await?;
258
259        let compressor = RefCell::new(Compressor::new(config.compression));
260
261        Ok(Self {
262            compressor,
263            config,
264            l1: HashMap::new(),
265            metadata: HashMap::new(),
266            l1_used: 0,
267            l2_used: 0,
268            l3_used: 0,
269            stats: TieredCacheStats::default(),
270        })
271    }
272
273    /// Put data into cache (starts in L1).
274    pub async fn put(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
275        let size = data.len() as u64;
276
277        // Remove old entry if exists
278        if let Some(old_meta) = self.metadata.get(&key) {
279            self.remove_from_tier(&key, old_meta.tier).await?;
280        }
281
282        // Try to place in L1
283        if self.l1_used + size <= self.config.l1_capacity_bytes {
284            self.l1.insert(key.clone(), data);
285            self.l1_used += size;
286            self.metadata.insert(
287                key.clone(),
288                CacheItemMetadata::new(key, size, CacheTier::L1),
289            );
290            Ok(())
291        } else {
292            // Evict from L1 to make space or place in L2
293            self.evict_from_l1().await?;
294            if self.l1_used + size <= self.config.l1_capacity_bytes {
295                self.l1.insert(key.clone(), data);
296                self.l1_used += size;
297                self.metadata.insert(
298                    key.clone(),
299                    CacheItemMetadata::new(key, size, CacheTier::L1),
300                );
301                Ok(())
302            } else {
303                // Place directly in L2
304                self.place_in_l2(key, data, size).await
305            }
306        }
307    }
308
309    /// Get data from cache.
310    pub async fn get(&mut self, key: &str) -> Result<Option<Vec<u8>>, TieredCacheError> {
311        // Record access and get tier info
312        let (tier, should_promote) = if let Some(meta) = self.metadata.get_mut(key) {
313            meta.record_access();
314            let should_promote = meta.should_promote(self.config.promotion_threshold);
315            (meta.tier, should_promote)
316        } else {
317            self.stats.misses += 1;
318            return Ok(None);
319        };
320
321        match tier {
322            CacheTier::L1 => {
323                self.stats.l1_hits += 1;
324                Ok(self.l1.get(key).cloned())
325            }
326            CacheTier::L2 => {
327                self.stats.l2_hits += 1;
328                let data = self.read_from_l2(key).await?;
329
330                // Promote to L1 if accessed frequently
331                if should_promote {
332                    self.promote_to_l1(key.to_string(), data.clone()).await?;
333                }
334
335                Ok(Some(data))
336            }
337            CacheTier::L3 => {
338                self.stats.l3_hits += 1;
339                let data = self.read_from_l3(key).await?;
340
341                // Promote to L2 if accessed frequently
342                if should_promote {
343                    self.promote_to_l2(key.to_string(), data.clone()).await?;
344                }
345
346                Ok(Some(data))
347            }
348        }
349    }
350
351    /// Remove item from cache.
352    pub async fn remove(&mut self, key: &str) -> Result<(), TieredCacheError> {
353        if let Some(meta) = self.metadata.remove(key) {
354            self.remove_from_tier(key, meta.tier).await?;
355        }
356        Ok(())
357    }
358
359    /// Get cache statistics.
360    #[must_use]
361    #[inline]
362    pub const fn stats(&self) -> &TieredCacheStats {
363        &self.stats
364    }
365
366    /// Get L1 usage percentage.
367    #[must_use]
368    #[inline]
369    pub fn l1_usage_percent(&self) -> f64 {
370        if self.config.l1_capacity_bytes == 0 {
371            0.0
372        } else {
373            self.l1_used as f64 / self.config.l1_capacity_bytes as f64
374        }
375    }
376
377    /// Warm the cache with a list of key-value pairs.
378    ///
379    /// This is useful for cold starts where you want to pre-populate
380    /// frequently accessed data. Items are placed according to available
381    /// capacity, starting from L1.
382    pub async fn warm_with_data(
383        &mut self,
384        items: Vec<(String, Vec<u8>)>,
385    ) -> Result<usize, TieredCacheError> {
386        let mut warmed = 0;
387
388        for (key, data) in items {
389            if self.put(key, data).await.is_ok() {
390                warmed += 1;
391            }
392        }
393
394        Ok(warmed)
395    }
396
397    /// Warm the cache by loading keys from a list.
398    ///
399    /// This method attempts to load data from storage tiers (L2/L3)
400    /// and promote them to L1 for faster access on startup.
401    pub async fn warm_from_keys(&mut self, keys: &[String]) -> Result<usize, TieredCacheError> {
402        let mut warmed = 0;
403
404        for key in keys {
405            // Try to load from L2
406            if let Ok(data) = self.read_from_l2(key).await {
407                if self.put(key.clone(), data).await.is_ok() {
408                    warmed += 1;
409                    continue;
410                }
411            }
412
413            // Try to load from L3
414            if let Ok(data) = self.read_from_l3(key).await {
415                if self.put(key.clone(), data).await.is_ok() {
416                    warmed += 1;
417                }
418            }
419        }
420
421        Ok(warmed)
422    }
423
424    /// Export hot keys (most frequently accessed) for warming on next startup.
425    ///
426    /// Returns keys sorted by access count in descending order.
427    #[must_use]
428    pub fn export_hot_keys(&self, limit: usize) -> Vec<String> {
429        let mut items: Vec<_> = self
430            .metadata
431            .iter()
432            .map(|(key, meta)| (key.clone(), meta.access_count))
433            .collect();
434
435        items.sort_by(|a, b| b.1.cmp(&a.1));
436
437        items.into_iter().take(limit).map(|(key, _)| key).collect()
438    }
439
440    /// Get the number of cached items.
441    #[must_use]
442    #[inline]
443    pub fn len(&self) -> usize {
444        self.metadata.len()
445    }
446
447    /// Check if the cache is empty.
448    #[must_use]
449    #[inline]
450    pub fn is_empty(&self) -> bool {
451        self.metadata.is_empty()
452    }
453
454    // Helper methods
455
456    async fn place_in_l2(
457        &mut self,
458        key: String,
459        data: Vec<u8>,
460        size: u64,
461    ) -> Result<(), TieredCacheError> {
462        if self.l2_used + size > self.config.l2_capacity_bytes {
463            self.evict_from_l2().await?;
464        }
465
466        if self.l2_used + size <= self.config.l2_capacity_bytes {
467            self.write_to_l2(&key, &data).await?;
468            self.l2_used += size;
469            self.metadata.insert(
470                key.clone(),
471                CacheItemMetadata::new(key, size, CacheTier::L2),
472            );
473            Ok(())
474        } else {
475            self.place_in_l3(key, data, size).await
476        }
477    }
478
479    async fn place_in_l3(
480        &mut self,
481        key: String,
482        data: Vec<u8>,
483        size: u64,
484    ) -> Result<(), TieredCacheError> {
485        if self.l3_used + size > self.config.l3_capacity_bytes {
486            self.evict_from_l3().await?;
487        }
488
489        if self.l3_used + size <= self.config.l3_capacity_bytes {
490            self.write_to_l3(&key, &data).await?;
491            self.l3_used += size;
492            self.metadata.insert(
493                key.clone(),
494                CacheItemMetadata::new(key, size, CacheTier::L3),
495            );
496            Ok(())
497        } else {
498            Err(TieredCacheError::TierFull {
499                tier: "L3".to_string(),
500            })
501        }
502    }
503
504    async fn evict_from_l1(&mut self) -> Result<(), TieredCacheError> {
505        // Find LRU item in L1
506        let lru_key = self
507            .metadata
508            .iter()
509            .filter(|(_, meta)| meta.tier == CacheTier::L1)
510            .min_by_key(|(_, meta)| meta.last_access_ms)
511            .map(|(key, _)| key.clone());
512
513        if let Some(key) = lru_key {
514            if let Some(data) = self.l1.remove(&key) {
515                // Get size before calling write methods
516                let size = self.metadata.get(&key).map(|m| m.size_bytes).unwrap_or(0);
517
518                self.l1_used -= size;
519                // Demote to L2
520                self.write_to_l2(&key, &data).await?;
521                self.l2_used += size;
522
523                // Update metadata tier
524                if let Some(meta) = self.metadata.get_mut(&key) {
525                    meta.tier = CacheTier::L2;
526                }
527
528                self.stats.demotions_l1_to_l2 += 1;
529            }
530        }
531
532        Ok(())
533    }
534
535    async fn evict_from_l2(&mut self) -> Result<(), TieredCacheError> {
536        // Find LRU item in L2
537        let lru_key = self
538            .metadata
539            .iter()
540            .filter(|(_, meta)| meta.tier == CacheTier::L2)
541            .min_by_key(|(_, meta)| meta.last_access_ms)
542            .map(|(key, _)| key.clone());
543
544        if let Some(key) = lru_key {
545            // Get size before calling methods
546            let size = self.metadata.get(&key).map(|m| m.size_bytes).unwrap_or(0);
547
548            let data = self.read_from_l2(&key).await?;
549
550            self.l2_used -= size;
551            // Demote to L3
552            self.write_to_l3(&key, &data).await?;
553            self.l3_used += size;
554
555            // Update metadata tier
556            if let Some(meta) = self.metadata.get_mut(&key) {
557                meta.tier = CacheTier::L3;
558            }
559
560            self.stats.demotions_l2_to_l3 += 1;
561
562            // Remove from L2
563            let _ = fs::remove_file(self.l2_path(&key)).await;
564        }
565
566        Ok(())
567    }
568
569    async fn evict_from_l3(&mut self) -> Result<(), TieredCacheError> {
570        // Find LRU item in L3
571        let lru_key = self
572            .metadata
573            .iter()
574            .filter(|(_, meta)| meta.tier == CacheTier::L3)
575            .min_by_key(|(_, meta)| meta.last_access_ms)
576            .map(|(key, _)| key.clone());
577
578        if let Some(key) = lru_key {
579            if let Some(meta) = self.metadata.remove(&key) {
580                self.l3_used -= meta.size_bytes;
581                let _ = fs::remove_file(self.l3_path(&key)).await;
582                self.stats.evictions += 1;
583            }
584        }
585
586        Ok(())
587    }
588
589    async fn promote_to_l1(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
590        // Extract metadata without holding a mutable borrow
591        let (size, current_tier) = if let Some(meta) = self.metadata.get(&key) {
592            (meta.size_bytes, meta.tier)
593        } else {
594            return Ok(());
595        };
596
597        // Early return if already in L1
598        if current_tier == CacheTier::L1 {
599            return Ok(());
600        }
601
602        // Make space in L1 if needed
603        while self.l1_used + size > self.config.l1_capacity_bytes {
604            self.evict_from_l1().await?;
605        }
606
607        // Remove from current tier
608        match current_tier {
609            CacheTier::L2 => {
610                self.l2_used -= size;
611                let _ = fs::remove_file(self.l2_path(&key)).await;
612                self.stats.promotions_l2_to_l1 += 1;
613            }
614            CacheTier::L3 => {
615                self.l3_used -= size;
616                let _ = fs::remove_file(self.l3_path(&key)).await;
617            }
618            CacheTier::L1 => return Ok(()), // Already in L1
619        }
620
621        // Add to L1
622        self.l1.insert(key.clone(), data);
623        self.l1_used += size;
624
625        // Update metadata tier
626        if let Some(meta) = self.metadata.get_mut(&key) {
627            meta.tier = CacheTier::L1;
628        }
629
630        Ok(())
631    }
632
633    async fn promote_to_l2(&mut self, key: String, data: Vec<u8>) -> Result<(), TieredCacheError> {
634        // Extract metadata without holding a mutable borrow
635        let (size, current_tier) = if let Some(meta) = self.metadata.get(&key) {
636            (meta.size_bytes, meta.tier)
637        } else {
638            return Ok(());
639        };
640
641        if current_tier == CacheTier::L3 {
642            // Make space in L2 if needed
643            while self.l2_used + size > self.config.l2_capacity_bytes {
644                self.evict_from_l2().await?;
645            }
646
647            // Remove from L3
648            self.l3_used -= size;
649            let _ = fs::remove_file(self.l3_path(&key)).await;
650
651            // Add to L2
652            self.write_to_l2(&key, &data).await?;
653            self.l2_used += size;
654
655            // Update metadata tier
656            if let Some(meta) = self.metadata.get_mut(&key) {
657                meta.tier = CacheTier::L2;
658            }
659
660            self.stats.promotions_l3_to_l2 += 1;
661        }
662
663        Ok(())
664    }
665
666    async fn remove_from_tier(
667        &mut self,
668        key: &str,
669        tier: CacheTier,
670    ) -> Result<(), TieredCacheError> {
671        if let Some(meta) = self.metadata.get(key) {
672            match tier {
673                CacheTier::L1 => {
674                    self.l1.remove(key);
675                    self.l1_used -= meta.size_bytes;
676                }
677                CacheTier::L2 => {
678                    let _ = fs::remove_file(self.l2_path(key)).await;
679                    self.l2_used -= meta.size_bytes;
680                }
681                CacheTier::L3 => {
682                    let _ = fs::remove_file(self.l3_path(key)).await;
683                    self.l3_used -= meta.size_bytes;
684                }
685            }
686        }
687        Ok(())
688    }
689
690    fn l2_path(&self, key: &str) -> PathBuf {
691        self.config.l2_path.join(format!("{}.cache", key))
692    }
693
694    fn l3_path(&self, key: &str) -> PathBuf {
695        self.config.l3_path.join(format!("{}.cache", key))
696    }
697
698    async fn write_to_l2(&self, key: &str, data: &[u8]) -> Result<(), TieredCacheError> {
699        let path = self.l2_path(key);
700
701        // Compress data if compression is enabled
702        let write_data = if !self.config.compression.is_none() {
703            self.compressor
704                .borrow_mut()
705                .compress(data)
706                .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
707        } else {
708            data.to_vec()
709        };
710
711        let mut file = fs::File::create(path).await?;
712        file.write_all(&write_data).await?;
713        file.sync_all().await?;
714        Ok(())
715    }
716
717    async fn write_to_l3(&self, key: &str, data: &[u8]) -> Result<(), TieredCacheError> {
718        let path = self.l3_path(key);
719
720        // Compress data if compression is enabled
721        let write_data = if !self.config.compression.is_none() {
722            self.compressor
723                .borrow_mut()
724                .compress(data)
725                .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
726        } else {
727            data.to_vec()
728        };
729
730        let mut file = fs::File::create(path).await?;
731        file.write_all(&write_data).await?;
732        file.sync_all().await?;
733        Ok(())
734    }
735
736    async fn read_from_l2(&self, key: &str) -> Result<Vec<u8>, TieredCacheError> {
737        let path = self.l2_path(key);
738        let mut file = fs::File::open(path).await?;
739        let mut compressed_data = Vec::new();
740        file.read_to_end(&mut compressed_data).await?;
741
742        // Decompress if compression is enabled
743        let data = if !self.config.compression.is_none() {
744            self.compressor
745                .borrow_mut()
746                .decompress(&compressed_data)
747                .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
748        } else {
749            compressed_data
750        };
751
752        Ok(data)
753    }
754
755    async fn read_from_l3(&self, key: &str) -> Result<Vec<u8>, TieredCacheError> {
756        let path = self.l3_path(key);
757        let mut file = fs::File::open(path).await?;
758        let mut compressed_data = Vec::new();
759        file.read_to_end(&mut compressed_data).await?;
760
761        // Decompress if compression is enabled
762        let data = if !self.config.compression.is_none() {
763            self.compressor
764                .borrow_mut()
765                .decompress(&compressed_data)
766                .map_err(|e| TieredCacheError::Io(std::io::Error::other(e)))?
767        } else {
768            compressed_data
769        };
770
771        Ok(data)
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778    use tempfile::TempDir;
779
780    async fn create_test_cache() -> (TempDir, TieredCache) {
781        let temp_dir = TempDir::new().unwrap();
782        let config = TieredCacheConfig {
783            l1_capacity_bytes: 100,
784            l2_capacity_bytes: 200,
785            l3_capacity_bytes: 300,
786            l2_path: temp_dir.path().join("l2"),
787            l3_path: temp_dir.path().join("l3"),
788            promotion_threshold: 2,
789            compression: CompressionAlgorithm::None, // No compression in tests for predictable sizes
790        };
791        let cache = TieredCache::new(config).await.unwrap();
792        (temp_dir, cache)
793    }
794
795    #[tokio::test]
796    async fn test_tiered_cache_creation() {
797        let (_temp, cache) = create_test_cache().await;
798        assert_eq!(cache.l1_used, 0);
799        assert_eq!(cache.l2_used, 0);
800        assert_eq!(cache.l3_used, 0);
801    }
802
803    #[tokio::test]
804    async fn test_put_and_get_l1() {
805        let (_temp, mut cache) = create_test_cache().await;
806
807        cache
808            .put("key1".to_string(), b"small".to_vec())
809            .await
810            .unwrap();
811
812        let data = cache.get("key1").await.unwrap();
813        assert_eq!(data, Some(b"small".to_vec()));
814        assert_eq!(cache.stats.l1_hits, 1);
815    }
816
817    #[tokio::test]
818    async fn test_automatic_demotion() {
819        let (_temp, mut cache) = create_test_cache().await;
820
821        // Fill L1 beyond capacity
822        cache.put("key1".to_string(), vec![1; 60]).await.unwrap();
823        cache.put("key2".to_string(), vec![2; 60]).await.unwrap();
824
825        // This should demote key1 to L2
826        assert!(cache.stats.demotions_l1_to_l2 >= 1);
827    }
828
829    #[tokio::test]
830    async fn test_promotion_on_access() {
831        let (_temp, mut cache) = create_test_cache().await;
832
833        // Fill L1 to force item to L2
834        cache.put("key1".to_string(), vec![1; 60]).await.unwrap();
835        cache.put("key2".to_string(), vec![2; 60]).await.unwrap();
836
837        // Access key1 multiple times to trigger promotion
838        let _ = cache.get("key1").await;
839        let _ = cache.get("key1").await;
840        let _ = cache.get("key1").await;
841
842        // key1 should be promoted back to L1
843        if let Some(meta) = cache.metadata.get("key1") {
844            assert_eq!(meta.tier, CacheTier::L1);
845        }
846    }
847
848    #[tokio::test]
849    async fn test_hit_rate_calculation() {
850        let (_temp, mut cache) = create_test_cache().await;
851
852        cache
853            .put("key1".to_string(), b"data".to_vec())
854            .await
855            .unwrap();
856
857        let _ = cache.get("key1").await;
858        let _ = cache.get("key1").await;
859        let _ = cache.get("nonexistent").await;
860
861        let hit_rate = cache.stats.overall_hit_rate();
862        assert!((hit_rate - 0.666).abs() < 0.01);
863    }
864
865    #[tokio::test]
866    async fn test_remove() {
867        let (_temp, mut cache) = create_test_cache().await;
868
869        cache
870            .put("key1".to_string(), b"data".to_vec())
871            .await
872            .unwrap();
873        assert!(cache.get("key1").await.unwrap().is_some());
874
875        cache.remove("key1").await.unwrap();
876        assert!(cache.get("key1").await.unwrap().is_none());
877    }
878
879    #[tokio::test]
880    async fn test_warm_with_data() {
881        let (_temp, mut cache) = create_test_cache().await;
882
883        let warm_data = vec![
884            ("key1".to_string(), b"data1".to_vec()),
885            ("key2".to_string(), b"data2".to_vec()),
886            ("key3".to_string(), b"data3".to_vec()),
887        ];
888
889        let warmed = cache.warm_with_data(warm_data).await.unwrap();
890        assert_eq!(warmed, 3);
891
892        assert!(cache.get("key1").await.unwrap().is_some());
893        assert!(cache.get("key2").await.unwrap().is_some());
894        assert!(cache.get("key3").await.unwrap().is_some());
895    }
896
897    #[tokio::test]
898    async fn test_warm_from_keys() {
899        let (_temp, mut cache) = create_test_cache().await;
900
901        // Put some data in L2/L3 first
902        cache.put("key1".to_string(), vec![0u8; 150]).await.unwrap();
903        cache.put("key2".to_string(), vec![0u8; 150]).await.unwrap();
904
905        // These should be in L2 or L3 now
906        let _metadata_before = cache.metadata.clone();
907
908        // Create a new cache instance
909        let config = TieredCacheConfig {
910            l1_capacity_bytes: 100,
911            l2_capacity_bytes: 200,
912            l3_capacity_bytes: 300,
913            l2_path: cache.config.l2_path.clone(),
914            l3_path: cache.config.l3_path.clone(),
915            promotion_threshold: 2,
916            compression: CompressionAlgorithm::None,
917        };
918        let mut new_cache = TieredCache::new(config).await.unwrap();
919
920        // Warm from keys
921        let keys = vec!["key1".to_string(), "key2".to_string()];
922        let _warmed = new_cache.warm_from_keys(&keys).await.unwrap();
923        // Warmed count may vary depending on file system state
924    }
925
926    #[tokio::test]
927    async fn test_export_hot_keys() {
928        let (_temp, mut cache) = create_test_cache().await;
929
930        // Add some data with different access patterns
931        cache
932            .put("hot1".to_string(), b"data".to_vec())
933            .await
934            .unwrap();
935        cache
936            .put("hot2".to_string(), b"data".to_vec())
937            .await
938            .unwrap();
939        cache
940            .put("cold".to_string(), b"data".to_vec())
941            .await
942            .unwrap();
943
944        // Access hot keys multiple times
945        for _ in 0..5 {
946            let _ = cache.get("hot1").await;
947        }
948        for _ in 0..3 {
949            let _ = cache.get("hot2").await;
950        }
951        let _ = cache.get("cold").await;
952
953        // Export top 2 hot keys
954        let hot_keys = cache.export_hot_keys(2);
955        assert_eq!(hot_keys.len(), 2);
956        assert!(hot_keys.contains(&"hot1".to_string()));
957        assert!(hot_keys.contains(&"hot2".to_string()));
958    }
959
960    #[tokio::test]
961    async fn test_len_and_is_empty() {
962        let (_temp, mut cache) = create_test_cache().await;
963
964        assert!(cache.is_empty());
965        assert_eq!(cache.len(), 0);
966
967        cache
968            .put("key1".to_string(), b"data".to_vec())
969            .await
970            .unwrap();
971        assert!(!cache.is_empty());
972        assert_eq!(cache.len(), 1);
973
974        cache
975            .put("key2".to_string(), b"data".to_vec())
976            .await
977            .unwrap();
978        assert_eq!(cache.len(), 2);
979
980        cache.remove("key1").await.unwrap();
981        assert_eq!(cache.len(), 1);
982    }
983}