1use crate::error::{EdgeError, Result};
7use bytes::Bytes;
8use chrono::{DateTime, Utc};
9use lru::LruCache;
10use parking_lot::RwLock;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::num::NonZeroUsize;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum CachePolicy {
20 Lru,
22 Lfu,
24 Ttl,
26 SizeBased,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct CacheConfig {
33 pub max_size: usize,
35 pub policy: CachePolicy,
37 pub ttl_secs: Option<u64>,
39 pub persistent: bool,
41 pub cache_dir: Option<PathBuf>,
43 pub max_entries: usize,
45}
46
47impl Default for CacheConfig {
48 fn default() -> Self {
49 Self {
50 max_size: crate::DEFAULT_CACHE_SIZE,
51 policy: CachePolicy::Lru,
52 ttl_secs: Some(3600), persistent: false,
54 cache_dir: None,
55 max_entries: 1000,
56 }
57 }
58}
59
60impl CacheConfig {
61 pub fn minimal() -> Self {
63 Self {
64 max_size: 1024 * 1024, policy: CachePolicy::Lru,
66 ttl_secs: Some(1800), persistent: false,
68 cache_dir: None,
69 max_entries: 100,
70 }
71 }
72
73 pub fn offline_first() -> Self {
75 Self {
76 max_size: 50 * 1024 * 1024, policy: CachePolicy::Lru,
78 ttl_secs: None, persistent: true,
80 cache_dir: Some(PathBuf::from(".oxigdal_cache")),
81 max_entries: 5000,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CacheEntry {
89 pub key: String,
91 pub data: Bytes,
93 pub created_at: DateTime<Utc>,
95 pub accessed_at: DateTime<Utc>,
97 pub access_count: u64,
99 pub size: usize,
101 pub expires_at: Option<DateTime<Utc>>,
103}
104
105impl CacheEntry {
106 pub fn new(key: String, data: Bytes) -> Self {
108 let now = Utc::now();
109 let size = data.len();
110 Self {
111 key,
112 data,
113 created_at: now,
114 accessed_at: now,
115 access_count: 0,
116 size,
117 expires_at: None,
118 }
119 }
120
121 pub fn with_ttl(key: String, data: Bytes, ttl_secs: u64) -> Self {
123 let mut entry = Self::new(key, data);
124 entry.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_secs as i64));
125 entry
126 }
127
128 pub fn is_expired(&self) -> bool {
130 if let Some(expires_at) = self.expires_at {
131 Utc::now() > expires_at
132 } else {
133 false
134 }
135 }
136
137 pub fn mark_accessed(&mut self) {
139 self.accessed_at = Utc::now();
140 self.access_count = self.access_count.saturating_add(1);
141 }
142}
143
144pub struct Cache {
146 config: CacheConfig,
147 lru_cache: Arc<RwLock<LruCache<String, CacheEntry>>>,
148 metadata: Arc<RwLock<HashMap<String, CacheMetadata>>>,
149 current_size: Arc<RwLock<usize>>,
150 persistent_storage: Option<sled::Db>,
151}
152
153#[derive(Debug, Clone)]
155struct CacheMetadata {
156 size: usize,
158 access_count: u64,
160}
161
162impl Cache {
163 pub fn new(config: CacheConfig) -> Result<Self> {
165 let max_entries = NonZeroUsize::new(config.max_entries)
166 .ok_or_else(|| EdgeError::invalid_config("max_entries must be greater than 0"))?;
167
168 let lru_cache = Arc::new(RwLock::new(LruCache::new(max_entries)));
169 let metadata = Arc::new(RwLock::new(HashMap::new()));
170 let current_size = Arc::new(RwLock::new(0));
171
172 let persistent_storage = if config.persistent {
173 if let Some(cache_dir) = &config.cache_dir {
174 let db = sled::open(cache_dir).map_err(|e| EdgeError::storage(e.to_string()))?;
175 Some(db)
176 } else {
177 None
178 }
179 } else {
180 None
181 };
182
183 Ok(Self {
184 config,
185 lru_cache,
186 metadata,
187 current_size,
188 persistent_storage,
189 })
190 }
191
192 pub fn get(&self, key: &str) -> Result<Option<Bytes>> {
194 let mut cache = self.lru_cache.write();
196 if let Some(entry) = cache.get_mut(key) {
197 if !entry.is_expired() {
198 entry.mark_accessed();
199 return Ok(Some(entry.data.clone()));
200 } else {
201 cache.pop(key);
203 let mut meta = self.metadata.write();
204 meta.remove(key);
205 }
206 }
207 drop(cache);
208
209 if let Some(db) = &self.persistent_storage {
211 if let Some(value) = db.get(key).map_err(|e| EdgeError::storage(e.to_string()))? {
212 let entry: CacheEntry = serde_json::from_slice(&value)
213 .map_err(|e| EdgeError::deserialization(e.to_string()))?;
214
215 if !entry.is_expired() {
216 let mut cache = self.lru_cache.write();
218 cache.put(key.to_string(), entry.clone());
219 return Ok(Some(entry.data));
220 }
221 }
222 }
223
224 Ok(None)
225 }
226
227 pub fn put(&self, key: String, data: Bytes) -> Result<()> {
229 let entry_size = data.len();
230
231 if entry_size > self.config.max_size {
233 return Err(EdgeError::cache(format!(
234 "Entry size {} exceeds max cache size {}",
235 entry_size, self.config.max_size
236 )));
237 }
238
239 let entry = if let Some(ttl) = self.config.ttl_secs {
241 CacheEntry::with_ttl(key.clone(), data, ttl)
242 } else {
243 CacheEntry::new(key.clone(), data)
244 };
245
246 self.evict_if_needed(entry_size)?;
248
249 let mut cache = self.lru_cache.write();
251 cache.put(key.clone(), entry.clone());
252 drop(cache);
253
254 let mut meta = self.metadata.write();
256 meta.insert(
257 key.clone(),
258 CacheMetadata {
259 size: entry_size,
260 access_count: 0,
261 },
262 );
263 drop(meta);
264
265 let mut current_size = self.current_size.write();
267 *current_size = current_size.saturating_add(entry_size);
268 drop(current_size);
269
270 if let Some(db) = &self.persistent_storage {
272 let serialized =
273 serde_json::to_vec(&entry).map_err(|e| EdgeError::serialization(e.to_string()))?;
274 db.insert(key.as_bytes(), serialized)
275 .map_err(|e| EdgeError::storage(e.to_string()))?;
276 }
277
278 Ok(())
279 }
280
281 pub fn remove(&self, key: &str) -> Result<Option<Bytes>> {
283 let mut cache = self.lru_cache.write();
284 let entry = cache.pop(key);
285 drop(cache);
286
287 if let Some(ref e) = entry {
288 let mut meta = self.metadata.write();
290 meta.remove(key);
291 drop(meta);
292
293 let mut current_size = self.current_size.write();
295 *current_size = current_size.saturating_sub(e.size);
296 drop(current_size);
297
298 if let Some(db) = &self.persistent_storage {
300 db.remove(key.as_bytes())
301 .map_err(|e| EdgeError::storage(e.to_string()))?;
302 }
303 }
304
305 Ok(entry.map(|e| e.data))
306 }
307
308 pub fn clear(&self) -> Result<()> {
310 let mut cache = self.lru_cache.write();
311 cache.clear();
312 drop(cache);
313
314 let mut meta = self.metadata.write();
315 meta.clear();
316 drop(meta);
317
318 let mut current_size = self.current_size.write();
319 *current_size = 0;
320 drop(current_size);
321
322 if let Some(db) = &self.persistent_storage {
323 db.clear().map_err(|e| EdgeError::storage(e.to_string()))?;
324 }
325
326 Ok(())
327 }
328
329 pub fn size(&self) -> usize {
331 *self.current_size.read()
332 }
333
334 pub fn len(&self) -> usize {
336 self.lru_cache.read().len()
337 }
338
339 pub fn is_empty(&self) -> bool {
341 self.len() == 0
342 }
343
344 fn evict_if_needed(&self, new_entry_size: usize) -> Result<()> {
346 let current_size = *self.current_size.read();
347 let target_size = self.config.max_size.saturating_sub(new_entry_size);
348
349 if current_size <= target_size {
350 return Ok(());
351 }
352
353 let mut to_evict = Vec::new();
354 let mut freed_size = 0;
355
356 match self.config.policy {
357 CachePolicy::Lru => {
358 let mut cache = self.lru_cache.write();
360 while freed_size < current_size.saturating_sub(target_size) && !cache.is_empty() {
361 if let Some((key, entry)) = cache.pop_lru() {
362 freed_size = freed_size.saturating_add(entry.size);
363 to_evict.push(key);
364 }
365 }
366 }
367 CachePolicy::Lfu => {
368 let meta = self.metadata.read();
370 let mut entries: Vec<_> = meta.iter().collect();
371 entries.sort_by_key(|(_, m)| m.access_count);
372
373 for (key, metadata) in entries {
374 if freed_size >= current_size.saturating_sub(target_size) {
375 break;
376 }
377 freed_size = freed_size.saturating_add(metadata.size);
378 to_evict.push(key.clone());
379 }
380 }
381 CachePolicy::Ttl => {
382 let cache = self.lru_cache.read();
384 for (key, entry) in cache.iter() {
385 if entry.is_expired() {
386 freed_size = freed_size.saturating_add(entry.size);
387 to_evict.push(key.clone());
388 }
389 }
390 }
391 CachePolicy::SizeBased => {
392 let meta = self.metadata.read();
394 let mut entries: Vec<_> = meta.iter().collect();
395 entries.sort_by_key(|(_, m)| std::cmp::Reverse(m.size));
396
397 for (key, metadata) in entries {
398 if freed_size >= current_size.saturating_sub(target_size) {
399 break;
400 }
401 freed_size = freed_size.saturating_add(metadata.size);
402 to_evict.push(key.clone());
403 }
404 }
405 }
406
407 for key in to_evict {
409 self.remove(&key)?;
410 }
411
412 Ok(())
413 }
414
415 pub fn stats(&self) -> CacheStats {
417 CacheStats {
418 entries: self.len(),
419 size_bytes: self.size(),
420 max_size_bytes: self.config.max_size,
421 max_entries: self.config.max_entries,
422 policy: self.config.policy,
423 }
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct CacheStats {
430 pub entries: usize,
432 pub size_bytes: usize,
434 pub max_size_bytes: usize,
436 pub max_entries: usize,
438 pub policy: CachePolicy,
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_cache_creation() {
448 let config = CacheConfig::default();
449 let cache = Cache::new(config);
450 assert!(cache.is_ok());
451 }
452
453 #[test]
454 fn test_cache_put_get() -> Result<()> {
455 let config = CacheConfig::minimal();
456 let cache = Cache::new(config)?;
457
458 let key = "test_key".to_string();
459 let data = Bytes::from("test_data");
460
461 cache.put(key.clone(), data.clone())?;
462 let retrieved = cache.get(&key)?;
463
464 assert_eq!(retrieved, Some(data));
465 Ok(())
466 }
467
468 #[test]
469 fn test_cache_eviction() -> Result<()> {
470 let mut config = CacheConfig::minimal();
471 config.max_size = 100;
472 config.max_entries = 10;
473
474 let cache = Cache::new(config)?;
475
476 for i in 0..5 {
478 let key = format!("key_{}", i);
479 let data = Bytes::from(vec![0u8; 25]);
480 cache.put(key, data)?;
481 }
482
483 let key = "new_key".to_string();
485 let data = Bytes::from(vec![0u8; 25]);
486 cache.put(key.clone(), data.clone())?;
487
488 let retrieved = cache.get(&key)?;
489 assert_eq!(retrieved, Some(data));
490
491 Ok(())
492 }
493
494 #[test]
495 fn test_cache_remove() -> Result<()> {
496 let config = CacheConfig::minimal();
497 let cache = Cache::new(config)?;
498
499 let key = "test_key".to_string();
500 let data = Bytes::from("test_data");
501
502 cache.put(key.clone(), data.clone())?;
503 let removed = cache.remove(&key)?;
504
505 assert_eq!(removed, Some(data));
506 assert_eq!(cache.get(&key)?, None);
507
508 Ok(())
509 }
510
511 #[test]
512 fn test_cache_clear() -> Result<()> {
513 let config = CacheConfig::minimal();
514 let cache = Cache::new(config)?;
515
516 for i in 0..5 {
517 let key = format!("key_{}", i);
518 let data = Bytes::from(format!("data_{}", i));
519 cache.put(key, data)?;
520 }
521
522 assert_eq!(cache.len(), 5);
523
524 cache.clear()?;
525 assert_eq!(cache.len(), 0);
526 assert!(cache.is_empty());
527
528 Ok(())
529 }
530
531 #[test]
532 fn test_entry_expiration() {
533 let key = "test".to_string();
534 let data = Bytes::from("data");
535
536 let entry = CacheEntry::with_ttl(key, data, 0);
537 std::thread::sleep(std::time::Duration::from_millis(10));
538 assert!(entry.is_expired());
539 }
540}