Skip to main content

litellm_rs/core/cache/
memory.rs

1//! In-memory cache implementation
2//!
3//! This module provides a high-performance in-memory cache using DashMap
4//! for lock-free concurrent access with LRU eviction support.
5//!
6//! Hot-path eviction metadata is maintained with per-entry atomics in sharded
7//! indexes so cache hits and writes do not serialize on a global LRU lock.
8
9use 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
63/// In-memory cache with LRU eviction and TTL expiration
64pub struct InMemoryCache<T> {
65    /// Main cache storage using DashMap for lock-free access
66    cache: Arc<DashMap<CacheKey, CacheEntry<T>>>,
67    /// Sharded eviction metadata. Shard count defaults to available CPU
68    /// parallelism rounded to a bounded power of two.
69    access_meta: Arc<Vec<DashMap<CacheKey, CacheAccessMeta>>>,
70    /// Per-shard unique candidate indexes, with one slot per tracked key.
71    /// Hits only update atomics; inserts/removals maintain these indexes in O(1).
72    /// The LRU order is only a rotating sample order, not the eviction policy.
73    access_queue: Arc<Vec<Mutex<LruCache<CacheKey, ()>>>>,
74    /// Monotonic logical clock for access ordering.
75    access_clock: AtomicU64,
76    /// Rotates the first shard inspected by sampled eviction.
77    eviction_cursor: AtomicUsize,
78    /// Configuration
79    config: DualCacheConfig,
80    /// Statistics
81    stats: Arc<AtomicCacheStats>,
82    /// Cleanup has one owner, even if startup is requested more than once.
83    cleanup_started: AtomicBool,
84    /// Persistent shutdown state, including shutdown before the task first polls.
85    shutdown: watch::Sender<bool>,
86}
87
88impl<T: Clone + Send + Sync + 'static> InMemoryCache<T> {
89    /// Create a new in-memory cache with the given configuration
90    pub fn new(config: DualCacheConfig) -> Self {
91        Self::with_stats(config, Arc::new(AtomicCacheStats::new()))
92    }
93
94    /// Create a new in-memory cache with shared statistics
95    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    /// Create with default configuration
121    pub fn with_defaults() -> Self {
122        Self::new(DualCacheConfig::memory_only())
123    }
124
125    /// Start the background cleanup task
126    pub fn start_cleanup_task(self: &Arc<Self>) {
127        if *self.shutdown.borrow() || self.cleanup_started.swap(true, Ordering::AcqRel) {
128            return;
129        }
130        // Background maintenance must not keep a discarded runtime candidate
131        // alive. Upgrade only while actually cleaning an externally owned cache.
132        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    /// Get a value from the cache
156    pub async fn get(&self, key: &CacheKey) -> Option<T> {
157        // Atomically remove expired entries to avoid TOCTOU race
158        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    /// Peek a value without recording hit/miss statistics.
182    ///
183    /// Used for under-lock Dual L1 rechecks after an accounting miss so a single
184    /// client lookup is not double-counted. Still updates eviction access meta on
185    /// a live hit and still removes expired entries.
186    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    /// Get an entry with metadata from the cache
205    pub async fn get_entry(&self, key: &CacheKey) -> Option<CacheEntry<T>> {
206        // Atomically remove expired entries to avoid TOCTOU race
207        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    /// Peek an entry without recording hit/miss statistics (see [`Self::peek`]).
230    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    /// Set a value in the cache with the default TTL
251    pub async fn set(&self, key: CacheKey, value: T) {
252        self.set_with_ttl(key, value, self.config.default_ttl).await;
253    }
254
255    /// Set a value in the cache with a specific TTL
256    pub async fn set_with_ttl(&self, key: CacheKey, value: T, ttl: Duration) {
257        // Check if we need to evict entries
258        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        // Atomic insert returns the old entry if key existed (no TOCTOU gap)
265        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    /// Set a value with size tracking
279    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        // Atomic insert returns the old entry if key existed (no TOCTOU gap)
287        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    /// Delete a value from the cache
300    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    /// Atomically delete `key` only while the live (non-expired) value matches `predicate`.
314    ///
315    /// Uses DashMap `remove_if` so a concurrent replacement under the same key is
316    /// not removed after a stale match decision. Bookkeeping removal checks live
317    /// state while holding the candidate-index lock, so a replacement inserted
318    /// after `remove_if` keeps its eviction metadata and candidate slot.
319    ///
320    /// Returns the removed value when a matching entry was deleted.
321    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    /// Check if a key exists in the cache
341    pub async fn exists(&self, key: &CacheKey) -> bool {
342        // Atomically remove expired entries to avoid TOCTOU race
343        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    /// Get the remaining TTL for a key
352    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    /// Clear all entries from the cache
361    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            // A concurrent writer may already have installed a new value after
367            // cache.clear(). Preserve its metadata, or let its pending reset
368            // install both metadata and a candidate after this lock is released.
369            shard.retain(|key, _| self.cache.contains_key(key));
370            for meta in shard.iter() {
371                queue.put(meta.key().clone(), ());
372            }
373        }
374        // Do not rewind the logical clock while concurrent writers can use it.
375        self.eviction_cursor.store(0, Ordering::Relaxed);
376        self.stats.reset();
377        debug!("Cache cleared");
378    }
379
380    /// Get the number of entries in the cache
381    pub fn len(&self) -> usize {
382        self.cache.len()
383    }
384
385    /// Check if the cache is empty
386    pub fn is_empty(&self) -> bool {
387        self.cache.is_empty()
388    }
389
390    /// Get cache statistics
391    pub fn stats(&self) -> Arc<AtomicCacheStats> {
392        Arc::clone(&self.stats)
393    }
394
395    /// Get all keys in the cache
396    pub fn keys(&self) -> Vec<CacheKey> {
397        self.cache.iter().map(|r| r.key().clone()).collect()
398    }
399
400    /// Shutdown the cache and cleanup task
401    pub fn shutdown(&self) {
402        self.shutdown.send_replace(true);
403    }
404
405    // ==================== Private Methods ====================
406
407    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        // The value may have been removed while this writer waited for the
423        // index. Never recreate bookkeeping for an already absent key.
424        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        // A read can finish after its entry was deleted. Only an insertion may
440        // create metadata, so such a read cannot leave an orphan candidate.
441        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        // Serialize with insert bookkeeping, then validate current live state.
450        // A replacement keeps its slot; a later insertion will create its own.
451        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            // Rotate under the same lock without allocating another index node.
566            // A delete cannot interleave and resurrect a removed candidate.
567            queue.promote(&key);
568            sampled.push(key);
569        }
570        sampled
571    }
572
573    /// Evict one entry based on the eviction policy
574    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    /// Evict the least recently used entry
591    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    /// Evict the least frequently used entry
605    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    /// Evict entry with shortest remaining TTL
619    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    /// Evict the oldest entry (FIFO)
637    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    /// Clean up expired entries
667    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;