1use std::collections::HashMap;
7use std::fs::{File, OpenOptions};
8use std::io::{Read, Write};
9use std::path::PathBuf;
10use std::sync::RwLock;
11use std::time::Instant;
12
13use bytes::Bytes;
14use dashmap::DashMap;
15
16use super::config::{L2Config, StorageBackend};
17use super::result::{CacheKey, CachedResult, L2Entry};
18
19#[derive(Debug)]
26pub struct L2WarmCache {
27 config: L2Config,
29
30 memory_entries: DashMap<u64, L2Entry>,
32
33 mmap_storage: Option<RwLock<MmapStorage>>,
35
36 memory_usage: std::sync::atomic::AtomicUsize,
38}
39
40#[derive(Debug)]
42struct MmapStorage {
43 path: PathBuf,
45
46 file: Option<File>,
48
49 index: HashMap<u64, MmapEntry>,
51
52 file_size: usize,
54}
55
56#[derive(Debug, Clone)]
58struct MmapEntry {
59 offset: usize,
61
62 size: usize,
64
65 expires_at: u64,
67}
68
69impl L2WarmCache {
70 pub fn new(config: L2Config) -> Self {
72 let mmap_storage = if config.storage == StorageBackend::Mmap {
73 config
74 .mmap_path
75 .as_ref()
76 .map(|path| RwLock::new(MmapStorage::new(path.clone())))
77 } else {
78 None
79 };
80
81 Self {
82 config,
83 memory_entries: DashMap::new(),
84 mmap_storage,
85 memory_usage: std::sync::atomic::AtomicUsize::new(0),
86 }
87 }
88
89 pub async fn get(&self, key: &CacheKey) -> Option<CachedResult> {
91 if !self.config.enabled {
92 return None;
93 }
94
95 let hash = key.hash_value();
96
97 if let Some(mut entry) = self.memory_entries.get_mut(&hash) {
99 if entry.is_expired() {
100 drop(entry);
101 self.memory_entries.remove(&hash);
102 return None;
103 }
104
105 entry.touch();
106 return Some(entry.result.clone());
107 }
108
109 if let Some(ref mmap) = self.mmap_storage {
111 if let Ok(storage) = mmap.read() {
112 if let Some(result) = storage.get(hash) {
113 self.promote_to_memory(key, result.clone());
115 return Some(result);
116 }
117 }
118 }
119
120 None
121 }
122
123 pub async fn put(&self, key: CacheKey, result: CachedResult) {
125 if !self.config.enabled {
126 return;
127 }
128
129 let entry_size = result.size() + std::mem::size_of::<L2Entry>();
130
131 let max_bytes = self.config.size_mb * 1024 * 1024;
133 let current_usage = self.memory_usage.load(std::sync::atomic::Ordering::Relaxed);
134
135 if current_usage + entry_size > max_bytes {
136 self.evict_to_fit(entry_size).await;
137 }
138
139 let hash = key.hash_value();
140 let fingerprint = format!("{:016x}", hash);
141 let entry = L2Entry::new(key, fingerprint, result);
142 let entry_memory = entry.memory_size;
143
144 self.memory_entries.insert(hash, entry);
145 self.memory_usage
146 .fetch_add(entry_memory, std::sync::atomic::Ordering::Relaxed);
147 }
148
149 pub async fn remove(&self, key: &CacheKey) {
151 let hash = key.hash_value();
152
153 if let Some((_, entry)) = self.memory_entries.remove(&hash) {
154 self.memory_usage
155 .fetch_sub(entry.memory_size, std::sync::atomic::Ordering::Relaxed);
156 }
157
158 if let Some(ref mmap) = self.mmap_storage {
160 if let Ok(mut storage) = mmap.write() {
161 storage.remove(hash);
162 }
163 }
164 }
165
166 pub async fn clear(&self) {
168 self.memory_entries.clear();
169 self.memory_usage
170 .store(0, std::sync::atomic::Ordering::Relaxed);
171
172 if let Some(ref mmap) = self.mmap_storage {
173 if let Ok(mut storage) = mmap.write() {
174 storage.clear();
175 }
176 }
177 }
178
179 pub fn len(&self) -> usize {
181 self.memory_entries.len()
182 }
183
184 pub fn is_empty(&self) -> bool {
186 self.memory_entries.is_empty()
187 }
188
189 pub fn memory_usage(&self) -> usize {
191 self.memory_usage.load(std::sync::atomic::Ordering::Relaxed)
192 }
193
194 pub fn stats(&self) -> L2CacheStats {
196 let total_access: u64 = self.memory_entries.iter().map(|e| e.access_count).sum();
197
198 L2CacheStats {
199 entry_count: self.memory_entries.len(),
200 memory_usage_bytes: self.memory_usage(),
201 max_memory_bytes: self.config.size_mb * 1024 * 1024,
202 total_accesses: total_access,
203 storage_backend: self.config.storage.clone(),
204 }
205 }
206
207 async fn evict_to_fit(&self, required_bytes: usize) {
209 let max_bytes = self.config.size_mb * 1024 * 1024;
210 let target = max_bytes.saturating_sub(required_bytes);
211
212 let expired: Vec<u64> = self
214 .memory_entries
215 .iter()
216 .filter(|e| e.is_expired())
217 .map(|e| *e.key())
218 .collect();
219
220 for hash in expired {
221 if let Some((_, entry)) = self.memory_entries.remove(&hash) {
222 self.memory_usage
223 .fetch_sub(entry.memory_size, std::sync::atomic::Ordering::Relaxed);
224 }
225 }
226
227 while self.memory_usage.load(std::sync::atomic::Ordering::Relaxed) > target {
229 let lru_hash = self
231 .memory_entries
232 .iter()
233 .min_by_key(|e| e.last_access)
234 .map(|e| *e.key());
235
236 if let Some(hash) = lru_hash {
237 if self.mmap_storage.is_some() {
239 if let Some(entry) = self.memory_entries.get(&hash) {
240 self.demote_to_mmap(&entry);
241 }
242 }
243
244 if let Some((_, entry)) = self.memory_entries.remove(&hash) {
245 self.memory_usage
246 .fetch_sub(entry.memory_size, std::sync::atomic::Ordering::Relaxed);
247 }
248 } else {
249 break;
250 }
251 }
252 }
253
254 fn promote_to_memory(&self, key: &CacheKey, result: CachedResult) {
256 let hash = key.hash_value();
257 let fingerprint = format!("{:016x}", hash);
258 let entry = L2Entry::new(key.clone(), fingerprint, result);
259 let entry_memory = entry.memory_size;
260
261 self.memory_entries.insert(hash, entry);
262 self.memory_usage
263 .fetch_add(entry_memory, std::sync::atomic::Ordering::Relaxed);
264 }
265
266 fn demote_to_mmap(&self, entry: &dashmap::mapref::one::Ref<u64, L2Entry>) {
268 if let Some(ref mmap) = self.mmap_storage {
269 if let Ok(mut storage) = mmap.write() {
270 storage.put(*entry.key(), &entry.result);
271 }
272 }
273 }
274
275 pub fn flush_to_disk(&self) -> Result<usize, std::io::Error> {
277 let Some(ref mmap) = self.mmap_storage else {
278 return Ok(0);
279 };
280
281 let mut storage = mmap
282 .write()
283 .map_err(|_| std::io::Error::other("Lock poisoned"))?;
284
285 let mut count = 0;
286 for entry in self.memory_entries.iter() {
287 if !entry.is_expired() {
288 storage.put(*entry.key(), &entry.result);
289 count += 1;
290 }
291 }
292
293 storage.sync()?;
294 Ok(count)
295 }
296
297 pub fn load_from_disk(&self) -> Result<usize, std::io::Error> {
299 let Some(ref mmap) = self.mmap_storage else {
300 return Ok(0);
301 };
302
303 let storage = mmap
304 .read()
305 .map_err(|_| std::io::Error::other("Lock poisoned"))?;
306
307 Ok(storage.entry_count())
308 }
309}
310
311impl MmapStorage {
312 fn new(path: PathBuf) -> Self {
313 Self {
314 path,
315 file: None,
316 index: HashMap::new(),
317 file_size: 0,
318 }
319 }
320
321 fn get(&self, hash: u64) -> Option<CachedResult> {
322 let entry = self.index.get(&hash)?;
323
324 let now = std::time::SystemTime::now()
326 .duration_since(std::time::UNIX_EPOCH)
327 .ok()?
328 .as_secs();
329
330 if now > entry.expires_at {
331 return None;
332 }
333
334 let mut file = File::open(&self.path).ok()?;
336 let mut buffer = vec![0u8; entry.size];
337
338 use std::io::Seek;
339 file.seek(std::io::SeekFrom::Start(entry.offset as u64))
340 .ok()?;
341 file.read_exact(&mut buffer).ok()?;
342
343 deserialize_result(&buffer)
345 }
346
347 fn put(&mut self, hash: u64, result: &CachedResult) {
348 let data = serialize_result(result);
349
350 let file = match &mut self.file {
352 Some(f) => f,
353 None => {
354 self.file = OpenOptions::new()
355 .create(true)
356 .truncate(true)
357 .read(true)
358 .write(true)
359 .open(&self.path)
360 .ok();
361 match &mut self.file {
362 Some(f) => f,
363 None => return,
364 }
365 }
366 };
367
368 use std::io::Seek;
370 if file.seek(std::io::SeekFrom::End(0)).is_err() {
371 return;
372 }
373
374 let offset = self.file_size;
375 if file.write_all(&data).is_ok() {
376 let expires_at = std::time::SystemTime::now()
377 .duration_since(std::time::UNIX_EPOCH)
378 .map(|d| d.as_secs() + result.ttl.as_secs())
379 .unwrap_or(0);
380
381 self.index.insert(
382 hash,
383 MmapEntry {
384 offset,
385 size: data.len(),
386 expires_at,
387 },
388 );
389 self.file_size += data.len();
390 }
391 }
392
393 fn remove(&mut self, hash: u64) {
394 self.index.remove(&hash);
395 }
397
398 fn clear(&mut self) {
399 self.index.clear();
400 self.file_size = 0;
401
402 if let Some(ref mut file) = self.file {
404 let _ = file.set_len(0);
405 }
406 }
407
408 fn sync(&mut self) -> Result<(), std::io::Error> {
409 if let Some(ref file) = self.file {
410 file.sync_all()?;
411 }
412 Ok(())
413 }
414
415 fn entry_count(&self) -> usize {
416 self.index.len()
417 }
418}
419
420fn serialize_result(result: &CachedResult) -> Vec<u8> {
422 let mut buffer = Vec::new();
423
424 buffer.extend_from_slice(&result.ttl.as_secs().to_le_bytes());
426
427 buffer.extend_from_slice(&(result.row_count as u64).to_le_bytes());
429
430 buffer.extend_from_slice(&(result.data.len() as u64).to_le_bytes());
432 buffer.extend_from_slice(&result.data);
433
434 buffer
435}
436
437fn deserialize_result(buffer: &[u8]) -> Option<CachedResult> {
439 if buffer.len() < 24 {
440 return None;
441 }
442
443 let ttl_secs = u64::from_le_bytes(buffer[0..8].try_into().ok()?);
444 let row_count = u64::from_le_bytes(buffer[8..16].try_into().ok()?) as usize;
445 let data_len = u64::from_le_bytes(buffer[16..24].try_into().ok()?) as usize;
446
447 if buffer.len() < 24 + data_len {
448 return None;
449 }
450
451 let data = Bytes::copy_from_slice(&buffer[24..24 + data_len]);
452
453 Some(CachedResult {
454 data,
455 row_count,
456 cached_at: Instant::now(),
457 ttl: std::time::Duration::from_secs(ttl_secs),
458 tables: Vec::new(), execution_time: std::time::Duration::from_millis(0),
460 })
461}
462
463#[derive(Debug, Clone)]
465pub struct L2CacheStats {
466 pub entry_count: usize,
468
469 pub memory_usage_bytes: usize,
471
472 pub max_memory_bytes: usize,
474
475 pub total_accesses: u64,
477
478 pub storage_backend: StorageBackend,
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use std::time::Duration;
486
487 fn create_result(data: &str) -> CachedResult {
488 CachedResult::new(
489 Bytes::from(data.to_string()),
490 1,
491 Duration::from_secs(60),
492 vec!["test".to_string()],
493 Duration::from_millis(5),
494 )
495 }
496
497 fn create_key(query_hash: u64) -> CacheKey {
498 CacheKey::from_parts(query_hash, "test".to_string(), None, None)
499 }
500
501 #[tokio::test]
502 async fn test_basic_get_put() {
503 let config = L2Config::default();
504 let cache = L2WarmCache::new(config);
505
506 let key = create_key(12345);
507 let result = create_result("test data");
508
509 assert!(cache.get(&key).await.is_none());
511
512 cache.put(key.clone(), result.clone()).await;
514 let cached = cache.get(&key).await;
515 assert!(cached.is_some());
516 assert_eq!(cached.unwrap().data, result.data);
517 }
518
519 #[tokio::test]
520 async fn test_different_keys() {
521 let config = L2Config::default();
522 let cache = L2WarmCache::new(config);
523
524 let key1 = create_key(11111);
525 let key2 = create_key(22222);
526 let result = create_result("data");
527
528 cache.put(key1.clone(), result.clone()).await;
529
530 assert!(cache.get(&key1).await.is_some());
531 assert!(cache.get(&key2).await.is_none());
532 }
533
534 #[tokio::test]
535 async fn test_expiration() {
536 let config = L2Config {
537 ttl: Duration::from_millis(10),
538 ..Default::default()
539 };
540 let cache = L2WarmCache::new(config);
541
542 let key = create_key(12345);
543 let mut result = create_result("data");
544 result.ttl = Duration::from_millis(10);
545
546 cache.put(key.clone(), result).await;
547 assert!(cache.get(&key).await.is_some());
548
549 std::thread::sleep(Duration::from_millis(15));
550 assert!(cache.get(&key).await.is_none());
551 }
552
553 #[tokio::test]
554 async fn test_remove() {
555 let config = L2Config::default();
556 let cache = L2WarmCache::new(config);
557
558 let key = create_key(12345);
559 let result = create_result("data");
560
561 cache.put(key.clone(), result).await;
562 assert!(cache.get(&key).await.is_some());
563
564 cache.remove(&key).await;
565 assert!(cache.get(&key).await.is_none());
566 }
567
568 #[tokio::test]
569 async fn test_clear() {
570 let config = L2Config::default();
571 let cache = L2WarmCache::new(config);
572
573 cache.put(create_key(111), create_result("1")).await;
574 cache.put(create_key(222), create_result("2")).await;
575
576 assert_eq!(cache.len(), 2);
577
578 cache.clear().await;
579
580 assert!(cache.is_empty());
581 }
582
583 #[tokio::test]
584 async fn test_memory_eviction() {
585 let config = L2Config {
586 size_mb: 1, ..Default::default()
588 };
589 let cache = L2WarmCache::new(config);
590
591 let large_data = "x".repeat(100 * 1024); for i in 0..15 {
594 cache.put(create_key(i), create_result(&large_data)).await;
595 }
596
597 assert!(cache.memory_usage() <= 1024 * 1024 + 100 * 1024);
599 }
600
601 #[tokio::test]
602 async fn test_stats() {
603 let config = L2Config::default();
604 let cache = L2WarmCache::new(config);
605
606 cache.put(create_key(111), create_result("1")).await;
607 cache.put(create_key(222), create_result("2")).await;
608
609 cache.get(&create_key(111)).await;
610 cache.get(&create_key(111)).await;
611
612 let stats = cache.stats();
613 assert_eq!(stats.entry_count, 2);
614 assert!(stats.memory_usage_bytes > 0);
615 assert_eq!(stats.storage_backend, StorageBackend::Memory);
616 }
617
618 #[tokio::test]
619 async fn test_disabled_cache() {
620 let config = L2Config {
621 enabled: false,
622 ..Default::default()
623 };
624 let cache = L2WarmCache::new(config);
625
626 let key = create_key(12345);
627 cache.put(key.clone(), create_result("data")).await;
628
629 assert!(cache.get(&key).await.is_none());
630 }
631
632 #[test]
633 fn test_serialize_deserialize() {
634 let result = create_result("test data for serialization");
635 let serialized = serialize_result(&result);
636 let deserialized = deserialize_result(&serialized).unwrap();
637
638 assert_eq!(deserialized.data, result.data);
639 assert_eq!(deserialized.row_count, result.row_count);
640 assert_eq!(deserialized.ttl.as_secs(), result.ttl.as_secs());
641 }
642}