Skip to main content

multi_tier_cache/backends/
quickcache_cache.rs

1//! `QuickCache` - Fast In-Memory Cache Backend
2//!
3//! Lightweight and extremely fast in-memory cache optimized for maximum performance.
4
5use crate::error::CacheResult;
6use bytes::Bytes;
7use futures_util::future::BoxFuture;
8use quick_cache::sync::Cache;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::{Duration, Instant};
12use tracing::{debug, info};
13
14/// Cache entry with TTL information
15#[derive(Debug, Clone)]
16struct CacheEntry {
17    value: Bytes,
18    expires_at: Instant,
19}
20
21impl CacheEntry {
22    fn new(value: Bytes, ttl: Duration) -> Self {
23        Self {
24            value,
25            expires_at: Instant::now() + ttl,
26        }
27    }
28
29    fn is_expired(&self) -> bool {
30        Instant::now() > self.expires_at
31    }
32}
33
34/// `QuickCache` in-memory cache with per-key TTL support
35///
36/// This is an alternative L1 (hot tier) cache backend optimized for maximum performance:
37/// - Extremely fast in-memory access (sub-microsecond latency)
38/// - Automatic eviction via LRU
39/// - Per-key TTL support
40/// - Minimal memory overhead
41/// - Lock-free design for concurrent access
42///
43/// **When to use `QuickCache` vs Moka**:
44/// - Use `QuickCache` when you need maximum throughput and minimal latency
45/// - Use Moka when you need advanced features like time-to-idle or weight-based eviction
46pub struct QuickCacheBackend {
47    /// `QuickCache` instance
48    cache: Cache<String, Arc<CacheEntry>>,
49    /// Hit counter
50    hits: Arc<AtomicU64>,
51    /// Miss counter
52    misses: Arc<AtomicU64>,
53    /// Set counter
54    sets: Arc<AtomicU64>,
55}
56
57impl QuickCacheBackend {
58    /// Create new `QuickCache`
59    ///
60    /// # Arguments
61    ///
62    /// * `max_capacity` - Maximum number of entries (default: 2000)
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if the capacity is invalid.
67    pub fn new(max_capacity: u64) -> CacheResult<Self> {
68        info!(capacity = max_capacity, "Initializing QuickCache");
69
70        let cache = Cache::new(usize::try_from(max_capacity)?);
71
72        Ok(Self {
73            cache,
74            hits: Arc::new(AtomicU64::new(0)),
75            misses: Arc::new(AtomicU64::new(0)),
76            sets: Arc::new(AtomicU64::new(0)),
77        })
78    }
79
80    /// Get current cache size
81    #[must_use]
82    pub const fn size(&self) -> usize {
83        0 // Placeholder - quick_cache doesn't expose size
84    }
85}
86
87// ===== Trait Implementations =====
88
89use crate::traits::{CacheBackend, L2CacheBackend};
90
91/// Implement `CacheBackend` trait for `QuickCacheBackend`
92impl CacheBackend for QuickCacheBackend {
93    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
94        Box::pin(async move {
95            if let Some(entry) = self.cache.get(key) {
96                if entry.is_expired() {
97                    self.cache.remove(key);
98                    self.misses.fetch_add(1, Ordering::Relaxed);
99                    None
100                } else {
101                    self.hits.fetch_add(1, Ordering::Relaxed);
102                    Some(entry.value.clone())
103                }
104            } else {
105                self.misses.fetch_add(1, Ordering::Relaxed);
106                None
107            }
108        })
109    }
110
111    fn set_with_ttl<'a>(
112        &'a self,
113        key: &'a str,
114        value: Bytes,
115        ttl: Duration,
116    ) -> BoxFuture<'a, CacheResult<()>> {
117        Box::pin(async move {
118            let entry = Arc::new(CacheEntry::new(value, ttl));
119            self.cache.insert(key.to_string(), entry);
120            self.sets.fetch_add(1, Ordering::Relaxed);
121            debug!(key = %key, ttl_secs = %ttl.as_secs(), "[QuickCache] Cached key with TTL");
122            Ok(())
123        })
124    }
125
126    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, CacheResult<()>> {
127        Box::pin(async move {
128            self.cache.remove(key);
129            Ok(())
130        })
131    }
132
133    fn health_check(&self) -> BoxFuture<'_, bool> {
134        Box::pin(async move {
135            let test_key = "health_check_quickcache";
136            let test_value = Bytes::from_static(b"health_check");
137
138            match self
139                .set_with_ttl(test_key, test_value.clone(), Duration::from_mins(1))
140                .await
141            {
142                Ok(()) => match self.get(test_key).await {
143                    Some(retrieved) => {
144                        let _ = self.remove(test_key).await;
145                        retrieved == test_value
146                    }
147                    None => false,
148                },
149                Err(_) => false,
150            }
151        })
152    }
153
154    fn name(&self) -> &'static str {
155        "QuickCache"
156    }
157}
158
159impl L2CacheBackend for QuickCacheBackend {
160    fn get_with_ttl<'a>(
161        &'a self,
162        key: &'a str,
163    ) -> BoxFuture<'a, Option<(Bytes, Option<Duration>)>> {
164        Box::pin(async move {
165            if let Some(entry) = self.cache.get(key) {
166                if entry.is_expired() {
167                    self.cache.remove(key);
168                    self.misses.fetch_add(1, Ordering::Relaxed);
169                    None
170                } else {
171                    self.hits.fetch_add(1, Ordering::Relaxed);
172                    let now = Instant::now();
173                    let remaining = if entry.expires_at > now {
174                        Some(entry.expires_at.duration_since(now))
175                    } else {
176                        None
177                    };
178                    Some((entry.value.clone(), remaining))
179                }
180            } else {
181                self.misses.fetch_add(1, Ordering::Relaxed);
182                None
183            }
184        })
185    }
186}