do_memory_storage_redb/cache/
lru.rs1use 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
20pub struct LRUCache {
22 config: CacheConfig,
23 state: Arc<RwLock<CacheState>>,
24 cleanup_task: Option<JoinHandle<()>>,
25}
26
27impl LRUCache {
28 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 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 if let Some(entry) = state.entries.get_mut(&id) {
60 if entry.is_expired() {
62 debug!("Cache entry expired on access: {}", id);
63 state.metrics.expirations += 1;
64 state.metrics.misses += 1;
65
66 state.remove_entry(&id);
68 state.update_metrics();
69 return false;
70 }
71
72 entry.touch();
74
75 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 state.metrics.misses += 1;
85 state.update_metrics();
86 false
87 }
88 } else {
89 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 if state.entries.len() >= self.config.max_size {
97 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 state.entries.insert(id, entry);
107 state.lru_queue.push_back(id);
108
109 state.update_metrics();
110 false
111 }
112 }
113
114 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 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 pub async fn get_metrics(&self) -> CacheMetrics {
133 let state = self.state.read().await;
134 state.metrics.clone()
135 }
136
137 pub async fn clear(&self) {
139 let mut state = self.state.write().await;
140 state.clear();
141 }
142
143 pub async fn cleanup_expired(&self) -> usize {
145 let mut state = self.state.write().await;
146 let mut expired_ids = Vec::new();
147
148 for (id, entry) in &state.entries {
150 if entry.is_expired() {
151 expired_ids.push(*id);
152 }
153 }
154
155 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 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 for (id, entry) in &state_guard.entries {
183 if entry.is_expired() {
184 expired_ids.push(*id);
185 }
186 }
187
188 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 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}