Skip to main content

llm_optimizer_processor/state/
cached_backend.rs

1//! Tiered caching layer for optimal performance with distributed state
2//!
3//! This module implements a three-tiered caching architecture:
4//! - L1: In-memory cache (moka) - fastest, local to instance
5//! - L2: Redis cache - shared across instances, faster than DB
6//! - L3: PostgreSQL/Sled backend - durable storage, source of truth
7//!
8//! ## Architecture
9//!
10//! ```text
11//! ┌─────────────────────────────────────────────┐
12//! │         CachedStateBackend<B>              │
13//! │                                             │
14//! │  ┌──────────────────────────────────────┐  │
15//! │  │  L1: In-Memory Cache (moka)          │  │
16//! │  │  - Thread-safe, lock-free            │  │
17//! │  │  - LRU eviction                      │  │
18//! │  │  - TTL support                       │  │
19//! │  │  - ~1-10μs latency                   │  │
20//! │  └──────────────────────────────────────┘  │
21//! │              ↓ miss                         │
22//! │  ┌──────────────────────────────────────┐  │
23//! │  │  L2: Redis Cache                     │  │
24//! │  │  - Shared across instances           │  │
25//! │  │  - ~100-500μs latency                │  │
26//! │  │  - TTL-based invalidation            │  │
27//! │  └──────────────────────────────────────┘  │
28//! │              ↓ miss                         │
29//! │  ┌──────────────────────────────────────┐  │
30//! │  │  L3: Backend (PostgreSQL/Sled)       │  │
31//! │  │  - Durable, persistent               │  │
32//! │  │  - Source of truth                   │  │
33//! │  │  - ~1-10ms latency                   │  │
34//! │  └──────────────────────────────────────┘  │
35//! └─────────────────────────────────────────────┘
36//! ```
37//!
38//! ## Features
39//!
40//! - **Write Strategies**: Write-through, write-back, write-behind
41//! - **Read-through**: Automatic cache population on miss
42//! - **Refresh-ahead**: Proactive refresh before TTL expiry
43//! - **Statistics**: Per-layer hit rates, latency, evictions
44//! - **Bulk Operations**: Optimized batch get/put operations
45//! - **Cache Warming**: Preload hot keys on startup
46//!
47//! ## Example Usage
48//!
49//! ```rust,no_run
50//! use processor::state::{
51//!     CachedStateBackend, CacheConfig, WriteStrategy, EvictionPolicy,
52//!     MemoryStateBackend,
53//! };
54//! use std::time::Duration;
55//! use redis::Client;
56//!
57//! #[tokio::main]
58//! async fn main() -> anyhow::Result<()> {
59//!     // Configure cache
60//!     let config = CacheConfig {
61//!         l1_capacity: 10_000,
62//!         l1_ttl: Duration::from_secs(300),
63//!         l2_ttl: Duration::from_secs(3600),
64//!         write_strategy: WriteStrategy::WriteThrough,
65//!         eviction_policy: EvictionPolicy::LRU,
66//!         enable_stats: true,
67//!     };
68//!
69//!     // Create backend
70//!     let base_backend = MemoryStateBackend::new(None);
71//!     let redis_client = Client::open("redis://127.0.0.1/")?;
72//!
73//!     let cached_backend = CachedStateBackend::new(
74//!         base_backend,
75//!         Some(redis_client),
76//!         config,
77//!     ).await?;
78//!
79//!     // Use it like any StateBackend
80//!     cached_backend.put(b"key1", b"value1").await?;
81//!     let value = cached_backend.get(b"key1").await?;
82//!
83//!     // Check stats
84//!     let stats = cached_backend.stats().await;
85//!     println!("L1 hit rate: {:.2}%", stats.l1_hit_rate * 100.0);
86//!     println!("L2 hit rate: {:.2}%", stats.l2_hit_rate * 100.0);
87//!
88//!     Ok(())
89//! }
90//! ```
91
92use std::sync::atomic::{AtomicU64, Ordering};
93use std::sync::Arc;
94use std::time::{Duration, Instant, SystemTime};
95
96use async_trait::async_trait;
97use moka::future::Cache as MokaCache;
98use redis::aio::ConnectionManager;
99use redis::{AsyncCommands, Client as RedisClient};
100use serde::{Deserialize, Serialize};
101use tokio::sync::RwLock;
102use tracing::{debug, warn, instrument};
103
104use crate::error::{StateError, StateResult};
105use crate::state::StateBackend;
106
107/// Write strategy for cache updates
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109pub enum WriteStrategy {
110    /// Write to cache and backend synchronously
111    /// - Consistency: Strong
112    /// - Latency: High
113    /// - Use case: Critical data that must be durable immediately
114    WriteThrough,
115
116    /// Write to cache, async write to backend
117    /// - Consistency: Eventual
118    /// - Latency: Low
119    /// - Use case: High write throughput, tolerates brief data loss
120    WriteBack,
121
122    /// Batch writes to backend
123    /// - Consistency: Eventual (with batch delay)
124    /// - Latency: Lowest
125    /// - Use case: Very high write throughput, tolerates data loss
126    WriteBehind,
127}
128
129/// Cache eviction policy
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131pub enum EvictionPolicy {
132    /// Least Recently Used - evict oldest accessed entry
133    LRU,
134
135    /// Least Frequently Used - evict least accessed entry
136    LFU,
137
138    /// First In First Out - evict oldest entry
139    FIFO,
140}
141
142/// Cache configuration
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct CacheConfig {
145    /// Maximum number of entries in L1 cache
146    pub l1_capacity: usize,
147
148    /// L1 cache entry TTL
149    pub l1_ttl: Duration,
150
151    /// L2 cache entry TTL (Redis)
152    pub l2_ttl: Duration,
153
154    /// Write strategy
155    pub write_strategy: WriteStrategy,
156
157    /// Eviction policy for L1
158    pub eviction_policy: EvictionPolicy,
159
160    /// Enable statistics collection
161    pub enable_stats: bool,
162
163    /// Refresh-ahead threshold (fraction of TTL)
164    /// If set to 0.8, entries accessed when 80% of TTL has elapsed
165    /// will be refreshed asynchronously
166    pub refresh_ahead_threshold: Option<f64>,
167
168    /// Batch write interval for write-behind strategy
169    pub write_behind_interval: Option<Duration>,
170
171    /// Maximum batch size for write-behind
172    pub write_behind_batch_size: Option<usize>,
173}
174
175impl Default for CacheConfig {
176    fn default() -> Self {
177        Self {
178            l1_capacity: 10_000,
179            l1_ttl: Duration::from_secs(300),      // 5 minutes
180            l2_ttl: Duration::from_secs(3600),     // 1 hour
181            write_strategy: WriteStrategy::WriteThrough,
182            eviction_policy: EvictionPolicy::LRU,
183            enable_stats: true,
184            refresh_ahead_threshold: Some(0.8),
185            write_behind_interval: Some(Duration::from_secs(10)),
186            write_behind_batch_size: Some(1000),
187        }
188    }
189}
190
191/// Cache statistics
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
193pub struct CacheStats {
194    // Hit/miss counts
195    pub l1_hits: u64,
196    pub l1_misses: u64,
197    pub l2_hits: u64,
198    pub l2_misses: u64,
199    pub l3_hits: u64,
200    pub l3_misses: u64,
201
202    // Hit rates (calculated)
203    pub l1_hit_rate: f64,
204    pub l2_hit_rate: f64,
205    pub l3_hit_rate: f64,
206    pub overall_hit_rate: f64,
207
208    // Latency stats (in microseconds)
209    pub l1_avg_latency_us: f64,
210    pub l2_avg_latency_us: f64,
211    pub l3_avg_latency_us: f64,
212
213    // Eviction counts
214    pub l1_evictions: u64,
215    pub l2_evictions: u64,
216
217    // Cache sizes
218    pub l1_size: u64,
219    pub l1_capacity: u64,
220    pub l2_size: u64,
221
222    // Write stats
223    pub writes_total: u64,
224    pub writes_batched: u64,
225
226    // Refresh stats
227    pub refresh_ahead_count: u64,
228}
229
230impl CacheStats {
231    /// Calculate derived statistics
232    fn calculate_rates(&mut self) {
233        let l1_total = self.l1_hits + self.l1_misses;
234        let l2_total = self.l2_hits + self.l2_misses;
235        let l3_total = self.l3_hits + self.l3_misses;
236
237        self.l1_hit_rate = if l1_total > 0 {
238            self.l1_hits as f64 / l1_total as f64
239        } else {
240            0.0
241        };
242
243        self.l2_hit_rate = if l2_total > 0 {
244            self.l2_hits as f64 / l2_total as f64
245        } else {
246            0.0
247        };
248
249        self.l3_hit_rate = if l3_total > 0 {
250            self.l3_hits as f64 / l3_total as f64
251        } else {
252            0.0
253        };
254
255        let overall_total = self.l1_hits + self.l2_hits + self.l3_hits
256            + self.l1_misses + self.l2_misses + self.l3_misses;
257        let overall_hits = self.l1_hits + self.l2_hits + self.l3_hits;
258
259        self.overall_hit_rate = if overall_total > 0 {
260            overall_hits as f64 / overall_total as f64
261        } else {
262            0.0
263        };
264    }
265}
266
267/// Entry metadata for tracking TTL and refresh
268#[derive(Debug, Clone)]
269struct CacheEntry {
270    value: Vec<u8>,
271    inserted_at: SystemTime,
272    ttl: Duration,
273}
274
275impl CacheEntry {
276    fn new(value: Vec<u8>, ttl: Duration) -> Self {
277        Self {
278            value,
279            inserted_at: SystemTime::now(),
280            ttl,
281        }
282    }
283
284    fn is_expired(&self) -> bool {
285        self.inserted_at
286            .elapsed()
287            .map(|elapsed| elapsed >= self.ttl)
288            .unwrap_or(true)
289    }
290
291    fn needs_refresh(&self, threshold: f64) -> bool {
292        self.inserted_at
293            .elapsed()
294            .map(|elapsed| {
295                let threshold_duration = Duration::from_secs_f64(self.ttl.as_secs_f64() * threshold);
296                elapsed >= threshold_duration
297            })
298            .unwrap_or(false)
299    }
300}
301
302/// Statistics collector
303struct StatsCollector {
304    l1_hits: AtomicU64,
305    l1_misses: AtomicU64,
306    l2_hits: AtomicU64,
307    l2_misses: AtomicU64,
308    l3_hits: AtomicU64,
309    l3_misses: AtomicU64,
310    l1_evictions: AtomicU64,
311    l2_evictions: AtomicU64,
312    writes_total: AtomicU64,
313    writes_batched: AtomicU64,
314    refresh_ahead_count: AtomicU64,
315
316    // Latency tracking (sum of latencies for averaging)
317    l1_latency_sum_us: AtomicU64,
318    l2_latency_sum_us: AtomicU64,
319    l3_latency_sum_us: AtomicU64,
320}
321
322impl StatsCollector {
323    fn new() -> Self {
324        Self {
325            l1_hits: AtomicU64::new(0),
326            l1_misses: AtomicU64::new(0),
327            l2_hits: AtomicU64::new(0),
328            l2_misses: AtomicU64::new(0),
329            l3_hits: AtomicU64::new(0),
330            l3_misses: AtomicU64::new(0),
331            l1_evictions: AtomicU64::new(0),
332            l2_evictions: AtomicU64::new(0),
333            writes_total: AtomicU64::new(0),
334            writes_batched: AtomicU64::new(0),
335            refresh_ahead_count: AtomicU64::new(0),
336            l1_latency_sum_us: AtomicU64::new(0),
337            l2_latency_sum_us: AtomicU64::new(0),
338            l3_latency_sum_us: AtomicU64::new(0),
339        }
340    }
341
342    fn record_l1_hit(&self, latency_us: u64) {
343        self.l1_hits.fetch_add(1, Ordering::Relaxed);
344        self.l1_latency_sum_us.fetch_add(latency_us, Ordering::Relaxed);
345    }
346
347    fn record_l1_miss(&self) {
348        self.l1_misses.fetch_add(1, Ordering::Relaxed);
349    }
350
351    fn record_l2_hit(&self, latency_us: u64) {
352        self.l2_hits.fetch_add(1, Ordering::Relaxed);
353        self.l2_latency_sum_us.fetch_add(latency_us, Ordering::Relaxed);
354    }
355
356    fn record_l2_miss(&self) {
357        self.l2_misses.fetch_add(1, Ordering::Relaxed);
358    }
359
360    fn record_l3_hit(&self, latency_us: u64) {
361        self.l3_hits.fetch_add(1, Ordering::Relaxed);
362        self.l3_latency_sum_us.fetch_add(latency_us, Ordering::Relaxed);
363    }
364
365    fn record_l3_miss(&self) {
366        self.l3_misses.fetch_add(1, Ordering::Relaxed);
367    }
368
369    fn record_write(&self, batched: bool) {
370        self.writes_total.fetch_add(1, Ordering::Relaxed);
371        if batched {
372            self.writes_batched.fetch_add(1, Ordering::Relaxed);
373        }
374    }
375
376    fn record_refresh_ahead(&self) {
377        self.refresh_ahead_count.fetch_add(1, Ordering::Relaxed);
378    }
379
380    fn snapshot(&self, l1_cache: &MokaCache<Vec<u8>, CacheEntry>, config: &CacheConfig) -> CacheStats {
381        let l1_hits = self.l1_hits.load(Ordering::Relaxed);
382        let l1_misses = self.l1_misses.load(Ordering::Relaxed);
383        let l2_hits = self.l2_hits.load(Ordering::Relaxed);
384        let l2_misses = self.l2_misses.load(Ordering::Relaxed);
385        let l3_hits = self.l3_hits.load(Ordering::Relaxed);
386        let l3_misses = self.l3_misses.load(Ordering::Relaxed);
387
388        let l1_latency_sum = self.l1_latency_sum_us.load(Ordering::Relaxed);
389        let l2_latency_sum = self.l2_latency_sum_us.load(Ordering::Relaxed);
390        let l3_latency_sum = self.l3_latency_sum_us.load(Ordering::Relaxed);
391
392        let mut stats = CacheStats {
393            l1_hits,
394            l1_misses,
395            l2_hits,
396            l2_misses,
397            l3_hits,
398            l3_misses,
399            l1_evictions: self.l1_evictions.load(Ordering::Relaxed),
400            l2_evictions: self.l2_evictions.load(Ordering::Relaxed),
401            writes_total: self.writes_total.load(Ordering::Relaxed),
402            writes_batched: self.writes_batched.load(Ordering::Relaxed),
403            refresh_ahead_count: self.refresh_ahead_count.load(Ordering::Relaxed),
404            l1_size: l1_cache.entry_count(),
405            l1_capacity: config.l1_capacity as u64,
406            l2_size: 0, // Would need Redis DBSIZE, skipped for performance
407            l1_avg_latency_us: if l1_hits > 0 {
408                l1_latency_sum as f64 / l1_hits as f64
409            } else {
410                0.0
411            },
412            l2_avg_latency_us: if l2_hits > 0 {
413                l2_latency_sum as f64 / l2_hits as f64
414            } else {
415                0.0
416            },
417            l3_avg_latency_us: if l3_hits > 0 {
418                l3_latency_sum as f64 / l3_hits as f64
419            } else {
420                0.0
421            },
422            ..Default::default()
423        };
424
425        stats.calculate_rates();
426        stats
427    }
428}
429
430/// Write-behind batch queue
431struct WriteBehindQueue {
432    pending: RwLock<Vec<(Vec<u8>, Vec<u8>)>>,
433    config: CacheConfig,
434}
435
436impl WriteBehindQueue {
437    fn new(config: CacheConfig) -> Self {
438        Self {
439            pending: RwLock::new(Vec::new()),
440            config,
441        }
442    }
443
444    async fn enqueue(&self, key: Vec<u8>, value: Vec<u8>) -> bool {
445        let mut pending = self.pending.write().await;
446        pending.push((key, value));
447
448        pending.len() >= self.config.write_behind_batch_size.unwrap_or(1000)
449    }
450
451    async fn drain(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
452        let mut pending = self.pending.write().await;
453        std::mem::take(&mut *pending)
454    }
455
456    async fn len(&self) -> usize {
457        self.pending.read().await.len()
458    }
459}
460
461/// Tiered caching state backend
462///
463/// Generic over any `StateBackend` implementation to provide three-tier caching:
464/// L1 (in-memory), L2 (Redis), L3 (backend storage).
465pub struct CachedStateBackend<B: StateBackend> {
466    /// L1 cache (moka)
467    l1_cache: MokaCache<Vec<u8>, CacheEntry>,
468
469    /// L2 cache (Redis) - optional
470    l2_cache: Option<ConnectionManager>,
471
472    /// L3 backend (source of truth)
473    backend: Arc<B>,
474
475    /// Configuration
476    config: CacheConfig,
477
478    /// Statistics collector
479    stats: Arc<StatsCollector>,
480
481    /// Write-behind queue (if using write-behind strategy)
482    write_behind_queue: Option<Arc<WriteBehindQueue>>,
483}
484
485impl<B: StateBackend + 'static> CachedStateBackend<B> {
486    /// Create a new cached state backend
487    ///
488    /// # Arguments
489    ///
490    /// * `backend` - The underlying state backend (L3)
491    /// * `redis_client` - Optional Redis client for L2 cache
492    /// * `config` - Cache configuration
493    ///
494    /// # Returns
495    ///
496    /// Returns the cached backend wrapped in a Result
497    pub async fn new(
498        backend: B,
499        redis_client: Option<RedisClient>,
500        config: CacheConfig,
501    ) -> StateResult<Self> {
502        // Build L1 cache
503        let l1_cache = MokaCache::builder()
504            .max_capacity(config.l1_capacity as u64)
505            .time_to_live(config.l1_ttl)
506            .build();
507
508        // Connect to Redis for L2 if provided
509        let l2_cache = if let Some(client) = redis_client {
510            let manager = client
511                .get_connection_manager()
512                .await
513                .map_err(|e| StateError::StorageError {
514                    backend_type: "redis".to_string(),
515                    details: e.to_string(),
516                })?;
517            Some(manager)
518        } else {
519            None
520        };
521
522        // Create write-behind queue if needed
523        let write_behind_queue = if config.write_strategy == WriteStrategy::WriteBehind {
524            Some(Arc::new(WriteBehindQueue::new(config.clone())))
525        } else {
526            None
527        };
528
529        let cached = Self {
530            l1_cache,
531            l2_cache,
532            backend: Arc::new(backend),
533            config: config.clone(),
534            stats: Arc::new(StatsCollector::new()),
535            write_behind_queue,
536        };
537
538        // Start write-behind background task if needed
539        if let Some(queue) = &cached.write_behind_queue {
540            cached.start_write_behind_task(queue.clone()).await;
541        }
542
543        Ok(cached)
544    }
545
546    /// Start background task for write-behind strategy
547    async fn start_write_behind_task(&self, queue: Arc<WriteBehindQueue>) {
548        let backend = self.backend.clone();
549        let stats = self.stats.clone();
550        let interval = self.config.write_behind_interval.unwrap_or(Duration::from_secs(10));
551
552        tokio::spawn(async move {
553            let mut interval_timer = tokio::time::interval(interval);
554            loop {
555                interval_timer.tick().await;
556
557                let batch = queue.drain().await;
558                if batch.is_empty() {
559                    continue;
560                }
561
562                debug!("Flushing write-behind batch: {} entries", batch.len());
563
564                for (key, value) in batch {
565                    if let Err(e) = backend.put(&key, &value).await {
566                        warn!("Write-behind flush failed for key: {:?}", e);
567                    } else {
568                        stats.record_write(true);
569                    }
570                }
571            }
572        });
573    }
574
575    /// Get value from L1 cache
576    #[instrument(skip(self, key), level = "trace")]
577    async fn get_from_l1(&self, key: &[u8]) -> Option<Vec<u8>> {
578        let start = Instant::now();
579
580        if let Some(entry) = self.l1_cache.get(&key.to_vec()).await {
581            if !entry.is_expired() {
582                let latency_us = start.elapsed().as_micros() as u64;
583                if self.config.enable_stats {
584                    self.stats.record_l1_hit(latency_us);
585                }
586
587                // Check if refresh-ahead is needed
588                if let Some(threshold) = self.config.refresh_ahead_threshold {
589                    if entry.needs_refresh(threshold) {
590                        self.refresh_ahead(key.to_vec()).await;
591                    }
592                }
593
594                return Some(entry.value.clone());
595            }
596        }
597
598        if self.config.enable_stats {
599            self.stats.record_l1_miss();
600        }
601        None
602    }
603
604    /// Get value from L2 cache (Redis)
605    #[instrument(skip(self, key), level = "trace")]
606    async fn get_from_l2(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
607        let Some(ref mut conn) = self.l2_cache.clone() else {
608            return Ok(None);
609        };
610
611        let start = Instant::now();
612        let redis_key = format!("state:{}", hex::encode(key));
613
614        let result: Result<Option<Vec<u8>>, redis::RedisError> =
615            conn.clone().get(&redis_key).await;
616
617        match result {
618            Ok(Some(value)) => {
619                let latency_us = start.elapsed().as_micros() as u64;
620                if self.config.enable_stats {
621                    self.stats.record_l2_hit(latency_us);
622                }
623
624                // Populate L1 cache
625                let entry = CacheEntry::new(value.clone(), self.config.l1_ttl);
626                self.l1_cache.insert(key.to_vec(), entry).await;
627
628                Ok(Some(value))
629            }
630            Ok(None) => {
631                if self.config.enable_stats {
632                    self.stats.record_l2_miss();
633                }
634                Ok(None)
635            }
636            Err(e) => {
637                warn!("L2 cache error: {}", e);
638                if self.config.enable_stats {
639                    self.stats.record_l2_miss();
640                }
641                Ok(None)
642            }
643        }
644    }
645
646    /// Get value from L3 backend
647    #[instrument(skip(self, key), level = "trace")]
648    async fn get_from_l3(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
649        let start = Instant::now();
650
651        let result = self.backend.get(key).await?;
652
653        let latency_us = start.elapsed().as_micros() as u64;
654
655        if result.is_some() {
656            if self.config.enable_stats {
657                self.stats.record_l3_hit(latency_us);
658            }
659        } else {
660            if self.config.enable_stats {
661                self.stats.record_l3_miss();
662            }
663        }
664
665        Ok(result)
666    }
667
668    /// Put value to L1 cache
669    async fn put_to_l1(&self, key: &[u8], value: &[u8]) {
670        let entry = CacheEntry::new(value.to_vec(), self.config.l1_ttl);
671        self.l1_cache.insert(key.to_vec(), entry).await;
672    }
673
674    /// Put value to L2 cache (Redis)
675    async fn put_to_l2(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
676        let Some(ref mut conn) = self.l2_cache.clone() else {
677            return Ok(());
678        };
679
680        let redis_key = format!("state:{}", hex::encode(key));
681        let ttl_seconds = self.config.l2_ttl.as_secs();
682
683        let _: () = conn.clone()
684            .set_ex(&redis_key, value, ttl_seconds as u64)
685            .await
686            .map_err(|e| StateError::StorageError {
687                backend_type: "redis".to_string(),
688                details: e.to_string(),
689            })?;
690
691        Ok(())
692    }
693
694    /// Put value to L3 backend
695    async fn put_to_l3(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
696        self.backend.put(key, value).await
697    }
698
699    /// Refresh a key asynchronously (refresh-ahead strategy)
700    async fn refresh_ahead(&self, key: Vec<u8>) {
701        if self.config.enable_stats {
702            self.stats.record_refresh_ahead();
703        }
704
705        let backend = self.backend.clone();
706        let l1_cache = self.l1_cache.clone();
707        let l2_cache = self.l2_cache.clone();
708        let l1_ttl = self.config.l1_ttl;
709        let l2_ttl = self.config.l2_ttl;
710
711        tokio::spawn(async move {
712            debug!("Refresh-ahead for key: {:?}", key);
713
714            // Fetch from backend
715            if let Ok(Some(value)) = backend.get(&key).await {
716                // Update L1
717                let entry = CacheEntry::new(value.clone(), l1_ttl);
718                l1_cache.insert(key.clone(), entry).await;
719
720                // Update L2 if available
721                if let Some(mut conn) = l2_cache {
722                    let redis_key = format!("state:{}", hex::encode(&key));
723                    let ttl_seconds = l2_ttl.as_secs();
724                    let _: Result<(), _> = conn.set_ex(&redis_key, &value, ttl_seconds as u64).await;
725                }
726            }
727        });
728    }
729
730    /// Invalidate a key from all cache layers
731    #[instrument(skip(self, key), level = "debug")]
732    pub async fn invalidate(&self, key: &[u8]) -> StateResult<()> {
733        // Remove from L1
734        self.l1_cache.invalidate(&key.to_vec()).await;
735
736        // Remove from L2
737        if let Some(ref mut conn) = self.l2_cache.clone() {
738            let redis_key = format!("state:{}", hex::encode(key));
739            let _: Result<(), _> = conn.clone().del(&redis_key).await;
740        }
741
742        Ok(())
743    }
744
745    /// Clear all caches (L1 and L2)
746    pub async fn clear_cache(&self) -> StateResult<()> {
747        // Clear L1
748        self.l1_cache.invalidate_all();
749
750        // Clear L2 (flush all state keys)
751        if let Some(ref mut conn) = self.l2_cache.clone() {
752            // Note: This flushes ALL keys matching the pattern
753            // In production, use a separate Redis DB or namespace
754            let pattern = "state:*";
755            let keys: Result<Vec<String>, _> = redis::cmd("KEYS")
756                .arg(pattern)
757                .query_async(&mut conn.clone())
758                .await;
759
760            if let Ok(keys) = keys {
761                if !keys.is_empty() {
762                    let _: Result<(), _> = conn.clone().del(keys).await;
763                }
764            }
765        }
766
767        Ok(())
768    }
769
770    /// Get current cache statistics
771    pub async fn stats(&self) -> CacheStats {
772        self.stats.snapshot(&self.l1_cache, &self.config)
773    }
774
775    /// Warm up cache with frequently accessed keys
776    ///
777    /// Preloads specified keys from backend into cache layers
778    pub async fn warm_cache(&self, keys: &[Vec<u8>]) -> StateResult<()> {
779        debug!("Warming cache with {} keys", keys.len());
780
781        for key in keys {
782            if let Some(value) = self.backend.get(key).await? {
783                // Populate L1
784                self.put_to_l1(key, &value).await;
785
786                // Populate L2
787                let _ = self.put_to_l2(key, &value).await;
788            }
789        }
790
791        Ok(())
792    }
793
794    /// Get multiple keys in batch (optimized)
795    pub async fn get_batch(&self, keys: &[Vec<u8>]) -> StateResult<Vec<Option<Vec<u8>>>> {
796        let mut results = Vec::with_capacity(keys.len());
797
798        for key in keys {
799            results.push(self.get(key).await?);
800        }
801
802        Ok(results)
803    }
804
805    /// Put multiple key-value pairs in batch (optimized)
806    pub async fn put_batch(&self, pairs: &[(Vec<u8>, Vec<u8>)]) -> StateResult<()> {
807        for (key, value) in pairs {
808            self.put(key, value).await?;
809        }
810
811        Ok(())
812    }
813}
814
815// Need to add hex crate for encoding keys
816// Will be added to dependencies
817
818// Implement StateBackend trait
819#[async_trait]
820impl<B: StateBackend + 'static> StateBackend for CachedStateBackend<B> {
821    #[instrument(skip(self, key), level = "debug")]
822    async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
823        // Try L1 cache first
824        if let Some(value) = self.get_from_l1(key).await {
825            return Ok(Some(value));
826        }
827
828        // Try L2 cache
829        if let Some(value) = self.get_from_l2(key).await? {
830            return Ok(Some(value));
831        }
832
833        // Finally, try L3 backend
834        let value = self.get_from_l3(key).await?;
835
836        // If found in L3, populate upper caches (read-through)
837        if let Some(ref val) = value {
838            self.put_to_l1(key, val).await;
839            let _ = self.put_to_l2(key, val).await; // Ignore Redis errors
840        }
841
842        Ok(value)
843    }
844
845    #[instrument(skip(self, key, value), level = "debug")]
846    async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
847        match self.config.write_strategy {
848            WriteStrategy::WriteThrough => {
849                // Write to all layers synchronously
850                self.put_to_l1(key, value).await;
851                let _ = self.put_to_l2(key, value).await;
852                self.put_to_l3(key, value).await?;
853
854                if self.config.enable_stats {
855                    self.stats.record_write(false);
856                }
857            }
858            WriteStrategy::WriteBack => {
859                // Write to cache immediately, backend asynchronously
860                self.put_to_l1(key, value).await;
861                let _ = self.put_to_l2(key, value).await;
862
863                let backend = self.backend.clone();
864                let key = key.to_vec();
865                let value = value.to_vec();
866                let stats = self.stats.clone();
867                let enable_stats = self.config.enable_stats;
868
869                tokio::spawn(async move {
870                    if let Err(e) = backend.put(&key, &value).await {
871                        warn!("Write-back failed: {:?}", e);
872                    } else if enable_stats {
873                        stats.record_write(false);
874                    }
875                });
876            }
877            WriteStrategy::WriteBehind => {
878                // Write to cache immediately, queue for batch write
879                self.put_to_l1(key, value).await;
880                let _ = self.put_to_l2(key, value).await;
881
882                if let Some(ref queue) = self.write_behind_queue {
883                    queue.enqueue(key.to_vec(), value.to_vec()).await;
884                }
885            }
886        }
887
888        Ok(())
889    }
890
891    #[instrument(skip(self, key), level = "debug")]
892    async fn delete(&self, key: &[u8]) -> StateResult<()> {
893        // Remove from all cache layers
894        self.invalidate(key).await?;
895
896        // Delete from backend
897        self.backend.delete(key).await
898    }
899
900    async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
901        // List keys always goes to backend (source of truth)
902        self.backend.list_keys(prefix).await
903    }
904
905    async fn clear(&self) -> StateResult<()> {
906        // Clear caches
907        self.clear_cache().await?;
908
909        // Clear backend
910        self.backend.clear().await
911    }
912
913    async fn count(&self) -> StateResult<usize> {
914        // Count from backend (source of truth)
915        self.backend.count().await
916    }
917
918    async fn contains(&self, key: &[u8]) -> StateResult<bool> {
919        // Check L1 first
920        if self.l1_cache.get(&key.to_vec()).await.is_some() {
921            return Ok(true);
922        }
923
924        // Fall through to backend
925        self.backend.contains(key).await
926    }
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use crate::state::MemoryStateBackend;
933    use std::time::Duration;
934
935    #[tokio::test]
936    async fn test_cache_config_default() {
937        let config = CacheConfig::default();
938        assert_eq!(config.l1_capacity, 10_000);
939        assert_eq!(config.write_strategy, WriteStrategy::WriteThrough);
940        assert_eq!(config.eviction_policy, EvictionPolicy::LRU);
941    }
942
943    #[tokio::test]
944    async fn test_cached_backend_creation() {
945        let backend = MemoryStateBackend::new(None);
946        let config = CacheConfig::default();
947
948        let cached = CachedStateBackend::new(backend, None, config).await;
949        assert!(cached.is_ok());
950    }
951
952    #[tokio::test]
953    async fn test_l1_cache_hit() {
954        let backend = MemoryStateBackend::new(None);
955        let config = CacheConfig {
956            enable_stats: true,
957            ..Default::default()
958        };
959
960        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
961
962        // First put
963        cached.put(b"key1", b"value1").await.unwrap();
964
965        // First get - should be L1 hit
966        let value = cached.get(b"key1").await.unwrap();
967        assert_eq!(value, Some(b"value1".to_vec()));
968
969        // Check stats
970        let stats = cached.stats().await;
971        assert_eq!(stats.l1_hits, 1);
972        assert!(stats.l1_hit_rate > 0.0);
973    }
974
975    #[tokio::test]
976    async fn test_l3_fallback() {
977        let backend = MemoryStateBackend::new(None);
978        backend.put(b"existing", b"data").await.unwrap();
979
980        let config = CacheConfig {
981            enable_stats: true,
982            ..Default::default()
983        };
984
985        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
986
987        // Get should fall through to L3
988        let value = cached.get(b"existing").await.unwrap();
989        assert_eq!(value, Some(b"data".to_vec()));
990
991        // Check stats - should be L3 hit
992        let stats = cached.stats().await;
993        assert_eq!(stats.l3_hits, 1);
994        assert_eq!(stats.l1_hits, 0);
995    }
996
997    #[tokio::test]
998    async fn test_write_through_strategy() {
999        let backend = MemoryStateBackend::new(None);
1000        let config = CacheConfig {
1001            write_strategy: WriteStrategy::WriteThrough,
1002            ..Default::default()
1003        };
1004
1005        let cached = CachedStateBackend::new(backend.clone(), None, config).await.unwrap();
1006
1007        cached.put(b"key1", b"value1").await.unwrap();
1008
1009        // Value should be in backend immediately
1010        let backend_value = backend.get(b"key1").await.unwrap();
1011        assert_eq!(backend_value, Some(b"value1".to_vec()));
1012    }
1013
1014    #[tokio::test]
1015    async fn test_invalidate() {
1016        let backend = MemoryStateBackend::new(None);
1017        let config = CacheConfig::default();
1018
1019        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
1020
1021        cached.put(b"key1", b"value1").await.unwrap();
1022
1023        // Should be in L1
1024        let value = cached.get(b"key1").await.unwrap();
1025        assert_eq!(value, Some(b"value1".to_vec()));
1026
1027        // Invalidate
1028        cached.invalidate(b"key1").await.unwrap();
1029
1030        // Should still be in backend, but need to fetch from L3
1031        let value = cached.get(b"key1").await.unwrap();
1032        assert_eq!(value, Some(b"value1".to_vec()));
1033    }
1034
1035    #[tokio::test]
1036    async fn test_cache_stats() {
1037        let backend = MemoryStateBackend::new(None);
1038        let config = CacheConfig {
1039            enable_stats: true,
1040            ..Default::default()
1041        };
1042
1043        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
1044
1045        // Generate some traffic
1046        cached.put(b"k1", b"v1").await.unwrap();
1047        cached.put(b"k2", b"v2").await.unwrap();
1048
1049        cached.get(b"k1").await.unwrap(); // L1 hit
1050        cached.get(b"k1").await.unwrap(); // L1 hit
1051        cached.get(b"k3").await.unwrap(); // L3 miss
1052
1053        let stats = cached.stats().await;
1054        assert_eq!(stats.l1_hits, 2);
1055        assert_eq!(stats.l3_misses, 1);
1056        assert!(stats.l1_hit_rate > 0.0);
1057    }
1058
1059    #[tokio::test]
1060    async fn test_batch_operations() {
1061        let backend = MemoryStateBackend::new(None);
1062        let config = CacheConfig::default();
1063
1064        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
1065
1066        // Batch put
1067        let pairs = vec![
1068            (b"k1".to_vec(), b"v1".to_vec()),
1069            (b"k2".to_vec(), b"v2".to_vec()),
1070            (b"k3".to_vec(), b"v3".to_vec()),
1071        ];
1072        cached.put_batch(&pairs).await.unwrap();
1073
1074        // Batch get
1075        let keys = vec![b"k1".to_vec(), b"k2".to_vec(), b"k3".to_vec()];
1076        let values = cached.get_batch(&keys).await.unwrap();
1077
1078        assert_eq!(values.len(), 3);
1079        assert_eq!(values[0], Some(b"v1".to_vec()));
1080        assert_eq!(values[1], Some(b"v2".to_vec()));
1081        assert_eq!(values[2], Some(b"v3".to_vec()));
1082    }
1083
1084    #[tokio::test]
1085    async fn test_cache_warming() {
1086        let backend = MemoryStateBackend::new(None);
1087
1088        // Prepopulate backend
1089        backend.put(b"hot1", b"data1").await.unwrap();
1090        backend.put(b"hot2", b"data2").await.unwrap();
1091        backend.put(b"hot3", b"data3").await.unwrap();
1092
1093        let config = CacheConfig {
1094            enable_stats: true,
1095            ..Default::default()
1096        };
1097
1098        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
1099
1100        // Warm cache
1101        let hot_keys = vec![b"hot1".to_vec(), b"hot2".to_vec(), b"hot3".to_vec()];
1102        cached.warm_cache(&hot_keys).await.unwrap();
1103
1104        // All gets should be L1 hits
1105        cached.get(b"hot1").await.unwrap();
1106        cached.get(b"hot2").await.unwrap();
1107        cached.get(b"hot3").await.unwrap();
1108
1109        let stats = cached.stats().await;
1110        assert_eq!(stats.l1_hits, 3);
1111        assert_eq!(stats.l3_hits, 0);
1112    }
1113
1114    #[tokio::test]
1115    async fn test_clear_cache() {
1116        let backend = MemoryStateBackend::new(None);
1117        let config = CacheConfig::default();
1118
1119        let cached = CachedStateBackend::new(backend, None, config).await.unwrap();
1120
1121        cached.put(b"k1", b"v1").await.unwrap();
1122        cached.put(b"k2", b"v2").await.unwrap();
1123
1124        // Clear cache only
1125        cached.clear_cache().await.unwrap();
1126
1127        // Data should still be in backend (L3)
1128        // But will cause L3 hits instead of L1
1129        let value = cached.get(b"k1").await.unwrap();
1130        assert_eq!(value, Some(b"v1".to_vec()));
1131
1132        let stats = cached.stats().await;
1133        assert_eq!(stats.l3_hits, 1); // Had to fetch from L3
1134    }
1135
1136    #[tokio::test]
1137    async fn test_entry_expiration() {
1138        let entry = CacheEntry::new(b"value".to_vec(), Duration::from_millis(50));
1139
1140        assert!(!entry.is_expired());
1141
1142        tokio::time::sleep(Duration::from_millis(60)).await;
1143
1144        assert!(entry.is_expired());
1145    }
1146
1147    #[tokio::test]
1148    async fn test_refresh_ahead_detection() {
1149        let entry = CacheEntry::new(b"value".to_vec(), Duration::from_millis(100));
1150
1151        assert!(!entry.needs_refresh(0.8));
1152
1153        tokio::time::sleep(Duration::from_millis(85)).await;
1154
1155        assert!(entry.needs_refresh(0.8));
1156    }
1157
1158    #[tokio::test]
1159    async fn test_stats_calculation() {
1160        let mut stats = CacheStats {
1161            l1_hits: 80,
1162            l1_misses: 20,
1163            l2_hits: 15,
1164            l2_misses: 5,
1165            l3_hits: 4,
1166            l3_misses: 1,
1167            ..Default::default()
1168        };
1169
1170        stats.calculate_rates();
1171
1172        assert_eq!(stats.l1_hit_rate, 0.8);
1173        assert_eq!(stats.l2_hit_rate, 0.75);
1174        assert_eq!(stats.l3_hit_rate, 0.8);
1175
1176        // Overall: (80+15+4) / (80+20+15+5+4+1) = 99/125 = 0.792
1177        assert!((stats.overall_hit_rate - 0.792).abs() < 0.001);
1178    }
1179}