1use super::types::{AtomicCacheStats, CacheEntry, CacheKey, DualCacheConfig, EvictionPolicy};
10use dashmap::DashMap;
11use lru::LruCache;
12use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
13use std::sync::{Arc, Mutex, MutexGuard};
14use std::time::{Duration, Instant};
15use tokio::sync::watch;
16use tracing::{debug, trace};
17
18const EVICTION_SAMPLE_SIZE: usize = 64;
19const MIN_ACCESS_SHARDS: usize = 4;
20const MAX_ACCESS_SHARDS: usize = 64;
21
22#[derive(Debug)]
23struct CacheAccessMeta {
24 last_access_tick: AtomicU64,
25 access_count: AtomicU64,
26}
27
28impl CacheAccessMeta {
29 fn new(insert_tick: u64) -> Self {
30 Self {
31 last_access_tick: AtomicU64::new(insert_tick),
32 access_count: AtomicU64::new(0),
33 }
34 }
35
36 fn record_access(&self, tick: u64) -> u64 {
37 self.last_access_tick.store(tick, Ordering::Relaxed);
38 self.access_count.fetch_add(1, Ordering::Relaxed) + 1
39 }
40
41 fn reset_for_insert(&self, tick: u64) {
42 self.last_access_tick.store(tick, Ordering::Relaxed);
43 self.access_count.store(0, Ordering::Relaxed);
44 }
45
46 fn snapshot(&self) -> (u64, u64) {
47 (
48 self.last_access_tick.load(Ordering::Relaxed),
49 self.access_count.load(Ordering::Relaxed),
50 )
51 }
52}
53
54#[derive(Debug)]
55struct EvictionCandidate {
56 key: CacheKey,
57 last_access_tick: u64,
58 access_count: u64,
59 created_at: Instant,
60 remaining_ttl: Option<Duration>,
61}
62
63pub struct InMemoryCache<T> {
65 cache: Arc<DashMap<CacheKey, CacheEntry<T>>>,
67 access_meta: Arc<Vec<DashMap<CacheKey, CacheAccessMeta>>>,
70 access_queue: Arc<Vec<Mutex<LruCache<CacheKey, ()>>>>,
74 access_clock: AtomicU64,
76 eviction_cursor: AtomicUsize,
78 config: DualCacheConfig,
80 stats: Arc<AtomicCacheStats>,
82 cleanup_started: AtomicBool,
84 shutdown: watch::Sender<bool>,
86}
87
88impl<T: Clone + Send + Sync + 'static> InMemoryCache<T> {
89 pub fn new(config: DualCacheConfig) -> Self {
91 Self::with_stats(config, Arc::new(AtomicCacheStats::new()))
92 }
93
94 pub fn with_stats(config: DualCacheConfig, stats: Arc<AtomicCacheStats>) -> Self {
96 let cache = Arc::new(DashMap::with_capacity(config.max_size));
97 let (shutdown, _) = watch::channel(false);
98 let access_shards = default_access_shard_count();
99 let access_meta: Arc<Vec<DashMap<CacheKey, CacheAccessMeta>>> =
100 Arc::new((0..access_shards).map(|_| DashMap::new()).collect());
101 let access_queue: Arc<Vec<Mutex<LruCache<CacheKey, ()>>>> = Arc::new(
102 (0..access_shards)
103 .map(|_| Mutex::new(LruCache::unbounded()))
104 .collect(),
105 );
106
107 Self {
108 cache,
109 access_meta,
110 access_queue,
111 access_clock: AtomicU64::new(0),
112 eviction_cursor: AtomicUsize::new(0),
113 config,
114 stats,
115 cleanup_started: AtomicBool::new(false),
116 shutdown,
117 }
118 }
119
120 pub fn with_defaults() -> Self {
122 Self::new(DualCacheConfig::memory_only())
123 }
124
125 pub fn start_cleanup_task(self: &Arc<Self>) {
127 if *self.shutdown.borrow() || self.cleanup_started.swap(true, Ordering::AcqRel) {
128 return;
129 }
130 let cache = Arc::downgrade(self);
133 let mut shutdown = self.shutdown.subscribe();
134 let interval = self.config.cleanup_interval;
135
136 tokio::spawn(async move {
137 loop {
138 if *shutdown.borrow() {
139 break;
140 }
141 tokio::select! {
142 _ = tokio::time::sleep(interval) => {
143 let Some(cache) = cache.upgrade() else { break };
144 cache.cleanup_expired().await;
145 }
146 _ = shutdown.changed() => {
147 debug!("In-memory cache cleanup task shutting down");
148 break;
149 }
150 }
151 }
152 });
153 }
154
155 pub async fn get(&self, key: &CacheKey) -> Option<T> {
157 if let Some((_, removed)) = self.cache.remove_if(key, |_k, v| v.is_expired()) {
159 self.remove_access_meta(key);
160 self.stats.sub_total_size(removed.size_bytes);
161 self.stats.set_entry_count(self.cache.len());
162 self.stats.record_memory_miss();
163 trace!(key = %key, "Cache entry expired");
164 return None;
165 }
166
167 if let Some(entry) = self.cache.get(key) {
168 let value = entry.value.clone();
169 drop(entry);
170 self.record_access(key);
171 self.stats.record_memory_hit();
172 trace!(key = %key, "Cache hit");
173 Some(value)
174 } else {
175 self.stats.record_memory_miss();
176 trace!(key = %key, "Cache miss");
177 None
178 }
179 }
180
181 pub async fn peek(&self, key: &CacheKey) -> Option<T> {
187 if let Some((_, removed)) = self.cache.remove_if(key, |_k, v| v.is_expired()) {
188 self.remove_access_meta(key);
189 self.stats.sub_total_size(removed.size_bytes);
190 self.stats.set_entry_count(self.cache.len());
191 return None;
192 }
193
194 if let Some(entry) = self.cache.get(key) {
195 let value = entry.value.clone();
196 drop(entry);
197 self.record_access(key);
198 Some(value)
199 } else {
200 None
201 }
202 }
203
204 pub async fn get_entry(&self, key: &CacheKey) -> Option<CacheEntry<T>> {
206 if let Some((_, removed)) = self.cache.remove_if(key, |_k, v| v.is_expired()) {
208 self.remove_access_meta(key);
209 self.stats.sub_total_size(removed.size_bytes);
210 self.stats.set_entry_count(self.cache.len());
211 self.stats.record_memory_miss();
212 return None;
213 }
214
215 if let Some(entry) = self.cache.get(key) {
216 let mut snapshot = entry.clone();
217 drop(entry);
218 let access_count = self.record_access(key);
219 snapshot.access_count = access_count;
220 snapshot.last_accessed = Instant::now();
221 self.stats.record_memory_hit();
222 Some(snapshot)
223 } else {
224 self.stats.record_memory_miss();
225 None
226 }
227 }
228
229 pub async fn peek_entry(&self, key: &CacheKey) -> Option<CacheEntry<T>> {
231 if let Some((_, removed)) = self.cache.remove_if(key, |_k, v| v.is_expired()) {
232 self.remove_access_meta(key);
233 self.stats.sub_total_size(removed.size_bytes);
234 self.stats.set_entry_count(self.cache.len());
235 return None;
236 }
237
238 if let Some(entry) = self.cache.get(key) {
239 let mut snapshot = entry.clone();
240 drop(entry);
241 let access_count = self.record_access(key);
242 snapshot.access_count = access_count;
243 snapshot.last_accessed = Instant::now();
244 Some(snapshot)
245 } else {
246 None
247 }
248 }
249
250 pub async fn set(&self, key: CacheKey, value: T) {
252 self.set_with_ttl(key, value, self.config.default_ttl).await;
253 }
254
255 pub async fn set_with_ttl(&self, key: CacheKey, value: T, ttl: Duration) {
257 if self.cache.len() >= self.config.max_size {
259 self.evict_one().await;
260 }
261
262 let entry = CacheEntry::new(value, ttl);
263 let new_size = entry.size_bytes;
264 let old = self.cache.insert(key.clone(), entry);
266 self.reset_access_meta_for_insert(&key);
267 self.stats.record_write();
268
269 if let Some(old_entry) = old {
270 self.stats.sub_total_size(old_entry.size_bytes);
271 }
272
273 self.stats.add_total_size(new_size);
274 self.stats.set_entry_count(self.cache.len());
275 trace!(key = %key, ttl_secs = ttl.as_secs(), "Cache set");
276 }
277
278 pub async fn set_with_size(&self, key: CacheKey, value: T, ttl: Duration, size_bytes: usize) {
280 if self.cache.len() >= self.config.max_size {
281 self.evict_one().await;
282 }
283
284 let entry = CacheEntry::with_size(value, ttl, size_bytes);
285 let new_size = entry.size_bytes;
286 let old = self.cache.insert(key.clone(), entry);
288 self.reset_access_meta_for_insert(&key);
289 self.stats.record_write();
290
291 if let Some(old_entry) = old {
292 self.stats.sub_total_size(old_entry.size_bytes);
293 }
294
295 self.stats.add_total_size(new_size);
296 self.stats.set_entry_count(self.cache.len());
297 }
298
299 pub async fn delete(&self, key: &CacheKey) -> bool {
301 if let Some((_, removed)) = self.cache.remove(key) {
302 self.remove_access_meta(key);
303 self.stats.record_deletion();
304 self.stats.sub_total_size(removed.size_bytes);
305 self.stats.set_entry_count(self.cache.len());
306 trace!(key = %key, "Cache delete");
307 true
308 } else {
309 false
310 }
311 }
312
313 pub async fn delete_if<F>(&self, key: &CacheKey, predicate: F) -> Option<T>
322 where
323 F: Fn(&T) -> bool,
324 {
325 let removed = self.cache.remove_if(key, |_k, entry| {
326 !entry.is_expired() && predicate(&entry.value)
327 });
328 if let Some((_, removed)) = removed {
329 self.remove_access_meta(key);
330 self.stats.record_deletion();
331 self.stats.sub_total_size(removed.size_bytes);
332 self.stats.set_entry_count(self.cache.len());
333 trace!(key = %key, "Cache conditional delete");
334 Some(removed.value)
335 } else {
336 None
337 }
338 }
339
340 pub async fn exists(&self, key: &CacheKey) -> bool {
342 if self.cache.remove_if(key, |_k, v| v.is_expired()).is_some() {
344 self.remove_access_meta(key);
345 self.stats.set_entry_count(self.cache.len());
346 return false;
347 }
348 self.cache.contains_key(key)
349 }
350
351 pub fn ttl(&self, key: &CacheKey) -> Option<Duration> {
353 if let Some(entry) = self.cache.get(key) {
354 entry.remaining_ttl()
355 } else {
356 None
357 }
358 }
359
360 pub async fn clear(&self) {
362 self.cache.clear();
363 for (index, shard) in self.access_meta.iter().enumerate() {
364 let mut queue = lock_queue(&self.access_queue[index]);
365 queue.clear();
366 shard.retain(|key, _| self.cache.contains_key(key));
370 for meta in shard.iter() {
371 queue.put(meta.key().clone(), ());
372 }
373 }
374 self.eviction_cursor.store(0, Ordering::Relaxed);
376 self.stats.reset();
377 debug!("Cache cleared");
378 }
379
380 pub fn len(&self) -> usize {
382 self.cache.len()
383 }
384
385 pub fn is_empty(&self) -> bool {
387 self.cache.is_empty()
388 }
389
390 pub fn stats(&self) -> Arc<AtomicCacheStats> {
392 Arc::clone(&self.stats)
393 }
394
395 pub fn keys(&self) -> Vec<CacheKey> {
397 self.cache.iter().map(|r| r.key().clone()).collect()
398 }
399
400 pub fn shutdown(&self) {
402 self.shutdown.send_replace(true);
403 }
404
405 fn next_access_tick(&self) -> u64 {
408 self.access_clock.fetch_add(1, Ordering::Relaxed) + 1
409 }
410
411 fn access_shard_index(&self, key: &CacheKey) -> usize {
412 key.hash_value() as usize % self.access_meta.len()
413 }
414
415 fn access_shard(&self, key: &CacheKey) -> &DashMap<CacheKey, CacheAccessMeta> {
416 &self.access_meta[self.access_shard_index(key)]
417 }
418
419 fn reset_access_meta_for_insert(&self, key: &CacheKey) {
420 let index = self.access_shard_index(key);
421 let mut queue = lock_queue(&self.access_queue[index]);
422 if !self.cache.contains_key(key) {
425 return;
426 }
427 let tick = self.next_access_tick();
428 let shard = &self.access_meta[index];
429 if let Some(meta) = shard.get(key) {
430 meta.reset_for_insert(tick);
431 } else {
432 shard.insert(key.clone(), CacheAccessMeta::new(tick));
433 }
434 queue.put(key.clone(), ());
435 }
436
437 fn record_access(&self, key: &CacheKey) -> u64 {
438 let tick = self.next_access_tick();
439 self.access_shard(key)
442 .get(key)
443 .map_or(0, |meta| meta.record_access(tick))
444 }
445
446 fn remove_access_meta(&self, key: &CacheKey) {
447 let index = self.access_shard_index(key);
448 let mut queue = lock_queue(&self.access_queue[index]);
449 if !self.cache.contains_key(key) {
452 self.access_meta[index].remove(key);
453 queue.pop(key);
454 }
455 }
456
457 fn eviction_candidates(&self) -> Vec<EvictionCandidate> {
458 let target = self.cache.len().min(EVICTION_SAMPLE_SIZE);
459 let mut candidates = Vec::with_capacity(target);
460 if self.cache.is_empty() {
461 return candidates;
462 }
463
464 let shard_count = self.access_meta.len();
465 let start = self.eviction_cursor.fetch_add(1, Ordering::Relaxed) % shard_count;
466
467 for offset in 0..shard_count {
468 if candidates.len() >= target {
469 break;
470 }
471
472 let shard_index = (start + offset) % shard_count;
473 let sampled = self.sample_eviction_keys(shard_index, target - candidates.len());
474 if sampled.is_empty() {
475 continue;
476 }
477 let shard = &self.access_meta[(start + offset) % shard_count];
478
479 for key in sampled {
480 let snapshot = shard.get(&key).map(|meta| meta.snapshot());
481 if let Some((last_access_tick, access_count)) = snapshot {
482 if let Some(entry) = self.cache.get(&key) {
483 if candidates.len() < target {
484 candidates.push(EvictionCandidate {
485 key: key.clone(),
486 last_access_tick,
487 access_count,
488 created_at: entry.created_at,
489 remaining_ttl: entry.remaining_ttl(),
490 });
491 }
492 } else {
493 self.remove_access_meta(&key);
494 }
495 }
496 }
497 }
498
499 candidates
500 }
501
502 fn expired_eviction_candidate(&self) -> Option<EvictionCandidate> {
503 let shard_count = self.access_meta.len();
504 let start = self.eviction_cursor.fetch_add(1, Ordering::Relaxed) % shard_count;
505 let mut inspected = 0;
506
507 for offset in 0..shard_count {
508 if inspected >= EVICTION_SAMPLE_SIZE {
509 break;
510 }
511
512 let shard_index = (start + offset) % shard_count;
513 let sampled = self.sample_eviction_keys(shard_index, EVICTION_SAMPLE_SIZE - inspected);
514 inspected += sampled.len();
515 if sampled.is_empty() {
516 continue;
517 }
518
519 let shard = &self.access_meta[shard_index];
520 let mut expired = None;
521
522 for key in sampled {
523 let snapshot = shard.get(&key).map(|meta| meta.snapshot());
524 if let Some((last_access_tick, access_count)) = snapshot {
525 if let Some(entry) = self.cache.get(&key) {
526 if entry.is_expired() && expired.is_none() {
527 expired = Some(EvictionCandidate {
528 key: key.clone(),
529 last_access_tick,
530 access_count,
531 created_at: entry.created_at,
532 remaining_ttl: entry.remaining_ttl(),
533 });
534 }
535 } else {
536 self.remove_access_meta(&key);
537 }
538 }
539 }
540
541 if expired.is_some() {
542 return expired;
543 }
544 }
545
546 None
547 }
548
549 fn sample_eviction_keys(&self, shard_index: usize, budget: usize) -> Vec<CacheKey> {
550 if budget == 0 {
551 return Vec::new();
552 }
553
554 let mut queue = lock_queue(&self.access_queue[shard_index]);
555 let attempts = budget
556 .saturating_mul(2)
557 .min(EVICTION_SAMPLE_SIZE)
558 .min(queue.len());
559 let mut sampled = Vec::with_capacity(attempts);
560 for _ in 0..attempts {
561 let Some((key, ())) = queue.peek_lru() else {
562 break;
563 };
564 let key = key.clone();
565 queue.promote(&key);
568 sampled.push(key);
569 }
570 sampled
571 }
572
573 async fn evict_one(&self) -> bool {
575 for _ in 0..EVICTION_SAMPLE_SIZE {
576 let removed = match self.config.eviction_policy {
577 EvictionPolicy::LRU => self.evict_lru().await,
578 EvictionPolicy::LFU => self.evict_lfu().await,
579 EvictionPolicy::TTL => self.evict_ttl().await,
580 EvictionPolicy::FIFO => self.evict_fifo().await,
581 };
582 if removed || self.cache.len() < self.config.max_size {
583 return removed;
584 }
585 }
586
587 false
588 }
589
590 async fn evict_lru(&self) -> bool {
592 let candidate = self
593 .eviction_candidates()
594 .into_iter()
595 .min_by_key(|candidate| candidate.last_access_tick);
596
597 if let Some(candidate) = candidate {
598 return self.evict_candidate(candidate, "LRU");
599 }
600
601 false
602 }
603
604 async fn evict_lfu(&self) -> bool {
606 let candidate = self
607 .eviction_candidates()
608 .into_iter()
609 .min_by_key(|candidate| (candidate.access_count, candidate.last_access_tick));
610
611 if let Some(candidate) = candidate {
612 return self.evict_candidate(candidate, "LFU");
613 }
614
615 false
616 }
617
618 async fn evict_ttl(&self) -> bool {
620 if let Some(candidate) = self.expired_eviction_candidate() {
621 return self.evict_candidate(candidate, "TTL");
622 }
623
624 let candidate = self
625 .eviction_candidates()
626 .into_iter()
627 .min_by_key(|candidate| candidate.remaining_ttl.unwrap_or(Duration::ZERO));
628
629 if let Some(candidate) = candidate {
630 return self.evict_candidate(candidate, "TTL");
631 }
632
633 false
634 }
635
636 async fn evict_fifo(&self) -> bool {
638 let candidate = self
639 .eviction_candidates()
640 .into_iter()
641 .min_by_key(|candidate| candidate.created_at);
642
643 if let Some(candidate) = candidate {
644 return self.evict_candidate(candidate, "FIFO");
645 }
646
647 false
648 }
649
650 fn evict_candidate(&self, candidate: EvictionCandidate, policy: &'static str) -> bool {
651 if let Some((_, removed)) = self.cache.remove_if(&candidate.key, |_key, entry| {
652 entry.created_at == candidate.created_at
653 }) {
654 self.stats.sub_total_size(removed.size_bytes);
655 self.stats.record_eviction();
656 self.stats.set_entry_count(self.cache.len());
657 trace!(key = %candidate.key, policy = policy, "Cache eviction");
658 self.remove_access_meta(&candidate.key);
659 return true;
660 }
661
662 self.remove_access_meta(&candidate.key);
663 false
664 }
665
666 async fn cleanup_expired(&self) {
668 let mut expired_keys = Vec::new();
669
670 for entry in self.cache.iter() {
671 if entry.value().is_expired() {
672 expired_keys.push(entry.key().clone());
673 }
674 }
675
676 let mut count = 0;
677 for key in expired_keys {
678 if let Some((_, removed)) = self.cache.remove_if(&key, |_key, entry| entry.is_expired())
679 {
680 self.stats.sub_total_size(removed.size_bytes);
681 self.remove_access_meta(&key);
682 self.stats.record_eviction();
683 count += 1;
684 }
685 }
686
687 if count > 0 {
688 debug!(count = count, "Cleaned up expired entries");
689 self.stats.set_entry_count(self.cache.len());
690 }
691 }
692}
693
694fn default_access_shard_count() -> usize {
695 std::thread::available_parallelism()
696 .map(usize::from)
697 .unwrap_or(MIN_ACCESS_SHARDS)
698 .next_power_of_two()
699 .clamp(MIN_ACCESS_SHARDS, MAX_ACCESS_SHARDS)
700}
701
702fn lock_queue(queue: &Mutex<LruCache<CacheKey, ()>>) -> MutexGuard<'_, LruCache<CacheKey, ()>> {
703 queue
704 .lock()
705 .unwrap_or_else(|poisoned| poisoned.into_inner())
706}
707
708impl<T> Drop for InMemoryCache<T> {
709 fn drop(&mut self) {
710 self.shutdown.send_replace(true);
711 }
712}
713
714#[cfg(test)]
715#[path = "memory_tests.rs"]
716mod tests;