Skip to main content

do_memory_storage_redb/cache/
lru.rs

1//! LRU cache implementation with TTL expiration
2//!
3//! This module implements the main LRUCache struct with methods for:
4//! - Recording cache hits/misses
5//! - LRU eviction when cache is full
6//! - TTL-based expiration
7//! - Background cleanup task
8
9use super::state::CacheState;
10use super::traits::Cache;
11use super::types::{CacheConfig, CacheEntry, CacheMetrics};
12use async_trait::async_trait;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15use tokio::task::JoinHandle;
16use tokio::time::{Duration as TokioDuration, interval};
17use tracing::{debug, info};
18use uuid::Uuid;
19
20/// LRU cache with TTL expiration
21pub struct LRUCache {
22    config: CacheConfig,
23    state: Arc<RwLock<CacheState>>,
24    cleanup_task: Option<JoinHandle<()>>,
25}
26
27impl LRUCache {
28    /// Create a new LRU cache
29    pub fn new(config: CacheConfig) -> Self {
30        let state = Arc::new(RwLock::new(CacheState::new()));
31
32        let cleanup_task = if config.enable_background_cleanup && config.cleanup_interval_secs > 0 {
33            Some(Self::start_cleanup_task(
34                Arc::clone(&state),
35                config.cleanup_interval_secs,
36            ))
37        } else {
38            None
39        };
40
41        info!(
42            "Initialized LRU cache: max_size={}, ttl={}s, cleanup={}s",
43            config.max_size, config.default_ttl_secs, config.cleanup_interval_secs
44        );
45
46        Self {
47            config,
48            state,
49            cleanup_task,
50        }
51    }
52
53    /// Record a cache access (hit or miss)
54    pub async fn record_access(&self, id: Uuid, hit: bool, size_bytes: Option<usize>) -> bool {
55        let mut state = self.state.write().await;
56
57        if hit {
58            // Cache hit: update access time and move to back of LRU queue
59            if let Some(entry) = state.entries.get_mut(&id) {
60                // Check if expired
61                if entry.is_expired() {
62                    debug!("Cache entry expired on access: {}", id);
63                    state.metrics.expirations += 1;
64                    state.metrics.misses += 1;
65
66                    // Remove expired entry
67                    state.remove_entry(&id);
68                    state.update_metrics();
69                    return false;
70                }
71
72                // Update access time
73                entry.touch();
74
75                // Move to back of LRU queue (most recently used)
76                state.lru_queue.retain(|&qid| qid != id);
77                state.lru_queue.push_back(id);
78
79                state.metrics.hits += 1;
80                state.update_metrics();
81                true
82            } else {
83                // Entry not found - treat as miss
84                state.metrics.misses += 1;
85                state.update_metrics();
86                false
87            }
88        } else {
89            // Cache miss: add new entry
90            state.metrics.misses += 1;
91
92            let size = size_bytes.unwrap_or(0);
93            let entry = CacheEntry::new(self.config.default_ttl_secs, size);
94
95            // Check if we need to evict
96            if state.entries.len() >= self.config.max_size {
97                // Evict oldest entry (front of queue)
98                if let Some(oldest_id) = state.lru_queue.pop_front() {
99                    state.entries.remove(&oldest_id);
100                    state.metrics.evictions += 1;
101                    debug!("Evicted LRU entry: {}", oldest_id);
102                }
103            }
104
105            // Add new entry
106            state.entries.insert(id, entry);
107            state.lru_queue.push_back(id);
108
109            state.update_metrics();
110            false
111        }
112    }
113
114    /// Remove an entry from the cache
115    pub async fn remove(&self, id: Uuid) {
116        let mut state = self.state.write().await;
117        state.remove_entry(&id);
118        state.update_metrics();
119    }
120
121    /// Check if an entry exists and is not expired
122    pub async fn contains(&self, id: Uuid) -> bool {
123        let state = self.state.read().await;
124        if let Some(entry) = state.entries.get(&id) {
125            !entry.is_expired()
126        } else {
127            false
128        }
129    }
130
131    /// Get current cache metrics
132    pub async fn get_metrics(&self) -> CacheMetrics {
133        let state = self.state.read().await;
134        state.metrics.clone()
135    }
136
137    /// Clear all entries from cache
138    pub async fn clear(&self) {
139        let mut state = self.state.write().await;
140        state.clear();
141    }
142
143    /// Manually cleanup expired entries
144    pub async fn cleanup_expired(&self) -> usize {
145        let mut state = self.state.write().await;
146        let mut expired_ids = Vec::new();
147
148        // Find expired entries
149        for (id, entry) in &state.entries {
150            if entry.is_expired() {
151                expired_ids.push(*id);
152            }
153        }
154
155        // Remove them
156        let count = expired_ids.len();
157        for id in expired_ids {
158            state.remove_entry(&id);
159            state.metrics.expirations += 1;
160        }
161
162        state.update_metrics();
163
164        if count > 0 {
165            debug!("Cleaned up {} expired cache entries", count);
166        }
167
168        count
169    }
170
171    /// Start background cleanup task
172    fn start_cleanup_task(state: Arc<RwLock<CacheState>>, interval_secs: u64) -> JoinHandle<()> {
173        tokio::spawn(async move {
174            let mut ticker = interval(TokioDuration::from_secs(interval_secs));
175            loop {
176                ticker.tick().await;
177
178                let mut state_guard = state.write().await;
179                let mut expired_ids = Vec::new();
180
181                // Find expired entries
182                for (id, entry) in &state_guard.entries {
183                    if entry.is_expired() {
184                        expired_ids.push(*id);
185                    }
186                }
187
188                // Remove them
189                let count = expired_ids.len();
190                for id in expired_ids {
191                    state_guard.remove_entry(&id);
192                    state_guard.metrics.expirations += 1;
193                }
194
195                state_guard.update_metrics();
196                drop(state_guard);
197
198                if count > 0 {
199                    debug!("Background cleanup removed {} expired entries", count);
200                }
201            }
202        })
203    }
204
205    /// Stop the background cleanup task
206    pub fn stop_cleanup(&mut self) {
207        if let Some(task) = self.cleanup_task.take() {
208            task.abort();
209        }
210    }
211}
212
213impl Drop for LRUCache {
214    fn drop(&mut self) {
215        self.stop_cleanup();
216    }
217}
218
219#[async_trait]
220impl Cache for LRUCache {
221    async fn record_access(&self, id: Uuid, hit: bool, size_bytes: Option<usize>) -> bool {
222        self.record_access(id, hit, size_bytes).await
223    }
224
225    async fn remove(&self, id: Uuid) {
226        self.remove(id).await
227    }
228
229    async fn contains(&self, id: Uuid) -> bool {
230        self.contains(id).await
231    }
232
233    async fn get_metrics(&self) -> CacheMetrics {
234        self.get_metrics().await
235    }
236
237    async fn clear(&self) {
238        self.clear().await
239    }
240
241    async fn cleanup_expired(&self) -> usize {
242        self.cleanup_expired().await
243    }
244}