Skip to main content

micro_moka/unsync/
cache.rs

1use super::{CacheBuilder, IndexDeque, Iter, Slab, SlabEntry, SENTINEL};
2use crate::Policy;
3
4use hashbrown::HashTable;
5use std::{
6    borrow::Borrow,
7    collections::hash_map::RandomState,
8    fmt,
9    hash::{BuildHasher, Hash},
10};
11
12/// An in-memory cache that is _not_ thread-safe.
13///
14/// `Cache` utilizes a hash table [`hashbrown::HashTable`][hb-hashtable] for the
15/// central key-value storage. `Cache` performs a best-effort bounding of the
16/// map using an entry replacement algorithm to determine which entries to evict
17/// when the capacity is exceeded.
18///
19/// [hb-hashtable]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashTable.html
20///
21/// # Characteristic difference between `unsync` and `sync`/`future` caches
22///
23/// If you use a cache from a single thread application, `unsync::Cache` may
24/// outperform other caches for updates and retrievals because other caches have some
25/// overhead on syncing internal data structures between threads.
26///
27/// # Examples
28///
29/// Cache entries are manually added using the insert method, and are stored in the
30/// cache until either evicted or manually invalidated.
31///
32/// Here's an example of reading and updating a cache by using the main thread:
33///
34///```rust
35/// use micro_moka::unsync::Cache;
36///
37/// const NUM_KEYS: usize = 64;
38///
39/// fn value(n: usize) -> String {
40///     format!("value {}", n)
41/// }
42///
43/// // Create a cache that can store up to 10,000 entries.
44/// let mut cache = Cache::new(10_000);
45///
46/// // Insert 64 entries.
47/// for key in 0..NUM_KEYS {
48///     cache.insert(key, value(key));
49/// }
50///
51/// // Invalidate every 4 element of the inserted entries.
52/// for key in (0..NUM_KEYS).step_by(4) {
53///     cache.invalidate(&key);
54/// }
55///
56/// // Verify the result.
57/// for key in 0..NUM_KEYS {
58///     if key % 4 == 0 {
59///         assert_eq!(cache.get(&key), None);
60///     } else {
61///         assert_eq!(cache.get(&key), Some(&value(key)));
62///     }
63/// }
64/// ```
65///
66/// # Hashing Algorithm
67///
68/// By default, `Cache` uses a hashing algorithm selected to provide resistance
69/// against HashDoS attacks. It will the same one used by
70/// `std::collections::HashMap`, which is currently SipHash 1-3.
71///
72/// While SipHash's performance is very competitive for medium sized keys, other
73/// hashing algorithms will outperform it for small keys such as integers as well as
74/// large keys such as long strings. However those algorithms will typically not
75/// protect against attacks such as HashDoS.
76///
77/// The hashing algorithm can be replaced on a per-`Cache` basis using the
78/// [`build_with_hasher`][build-with-hasher-method] method of the
79/// `CacheBuilder`. Many alternative algorithms are available on crates.io, such
80/// as the [aHash][ahash-crate] crate.
81///
82/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher
83/// [ahash-crate]: https://crates.io/crates/ahash
84///
85pub struct Cache<K, V, S = RandomState> {
86    max_capacity: Option<u64>,
87    entry_count: u64,
88    table: HashTable<u32>,
89    build_hasher: S,
90    slab: Slab<K, V>,
91    deque: IndexDeque,
92}
93
94impl<K, V, S> fmt::Debug for Cache<K, V, S>
95where
96    K: fmt::Debug + Eq + Hash,
97    V: fmt::Debug,
98    S: BuildHasher + Clone,
99{
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        let mut d_map = f.debug_map();
102
103        for (k, v) in self.iter() {
104            d_map.entry(&k, &v);
105        }
106
107        d_map.finish()
108    }
109}
110
111impl<K, V> Cache<K, V, RandomState>
112where
113    K: Hash + Eq,
114{
115    /// Constructs a new `Cache<K, V>` that will store up to the `max_capacity` entries.
116    ///
117    /// To adjust various configuration knobs such as `initial_capacity`, use the
118    /// [`CacheBuilder`][builder-struct].
119    ///
120    /// [builder-struct]: ./struct.CacheBuilder.html
121    pub fn new(max_capacity: u64) -> Self {
122        let build_hasher = RandomState::default();
123        Self::with_everything(Some(max_capacity), None, build_hasher)
124    }
125
126    /// Returns a [`CacheBuilder`][builder-struct], which can builds a `Cache` with
127    /// various configuration knobs.
128    ///
129    /// [builder-struct]: ./struct.CacheBuilder.html
130    pub fn builder() -> CacheBuilder<K, V, Cache<K, V, RandomState>> {
131        CacheBuilder::default()
132    }
133}
134
135//
136// public
137//
138impl<K, V, S> Cache<K, V, S> {
139    /// Returns a read-only cache policy of this cache.
140    ///
141    /// At this time, cache policy cannot be modified after cache creation.
142    /// A future version may support to modify it.
143    pub fn policy(&self) -> Policy {
144        Policy::new(self.max_capacity)
145    }
146
147    /// Returns the number of entries in this cache.
148    ///
149    /// # Example
150    ///
151    /// ```rust
152    /// use micro_moka::unsync::Cache;
153    ///
154    /// let mut cache = Cache::new(10);
155    /// cache.insert('n', "Netherland Dwarf");
156    /// cache.insert('l', "Lop Eared");
157    /// cache.insert('d', "Dutch");
158    ///
159    /// // Ensure an entry exists.
160    /// assert!(cache.contains_key(&'n'));
161    ///
162    /// // Followings will print the actual numbers.
163    /// println!("{}", cache.entry_count());   // -> 3
164    /// ```
165    ///
166    pub fn entry_count(&self) -> u64 {
167        self.entry_count
168    }
169
170    /// Returns the total weighted size of entries in this cache.
171    ///
172    /// This is equivalent to `entry_count` as weight support has been removed.
173    pub fn weighted_size(&self) -> u64 {
174        self.entry_count
175    }
176}
177
178impl<K, V, S> Cache<K, V, S>
179where
180    K: Hash + Eq,
181    S: BuildHasher + Clone,
182{
183    pub(crate) fn with_everything(
184        max_capacity: Option<u64>,
185        initial_capacity: Option<usize>,
186        build_hasher: S,
187    ) -> Self {
188        let init_cap = initial_capacity.unwrap_or_default();
189
190        Self {
191            max_capacity,
192            entry_count: 0,
193            table: HashTable::with_capacity(init_cap),
194            build_hasher,
195            slab: if init_cap > 0 {
196                Slab::with_capacity(init_cap)
197            } else {
198                Slab::new()
199            },
200            deque: IndexDeque::default(),
201        }
202    }
203
204    /// Returns `true` if the cache contains a value for the key.
205    ///
206    /// Unlike the `get` method, this method is not considered a cache read operation,
207    /// so it does not mark the entry as visited.
208    ///
209    /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq`
210    /// on the borrowed form _must_ match those for the key type.
211    #[inline]
212    pub fn contains_key<Q>(&self, key: &Q) -> bool
213    where
214        K: Borrow<Q>,
215        Q: Hash + Eq + ?Sized,
216    {
217        let hash = self.hash(key);
218        self.table
219            .find(hash, |&idx| self.slab.get(idx).key.borrow() == key)
220            .is_some()
221    }
222
223    /// Returns an immutable reference of the value corresponding to the key.
224    ///
225    /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq`
226    /// on the borrowed form _must_ match those for the key type.
227    #[inline]
228    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
229    where
230        K: Borrow<Q>,
231        Q: Hash + Eq + ?Sized,
232    {
233        let hash = self.hash(key);
234
235        let idx = match self
236            .table
237            .find(hash, |&idx| self.slab.get(idx).key.borrow() == key)
238        {
239            Some(&idx) => idx,
240            None => return None,
241        };
242
243        self.slab.get_mut(idx).visited = true;
244        Some(&self.slab.get(idx).value)
245    }
246
247    /// Returns an immutable reference of the value corresponding to the key,
248    /// without marking the entry as visited.
249    ///
250    /// Unlike [`get`](#method.get), this method does not count as a cache read
251    /// for eviction purposes: the entry's visited bit is not set. This is useful
252    /// when you want to inspect the cache without influencing which entries get
253    /// evicted, or when you only have a shared (`&self`) reference.
254    ///
255    /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq`
256    /// on the borrowed form _must_ match those for the key type.
257    ///
258    /// # Example
259    ///
260    /// ```rust
261    /// use micro_moka::unsync::Cache;
262    ///
263    /// let mut cache = Cache::new(100);
264    /// cache.insert("a", "alice");
265    ///
266    /// // peek() returns the value without affecting eviction order.
267    /// assert_eq!(cache.peek(&"a"), Some(&"alice"));
268    /// assert_eq!(cache.peek(&"missing"), None);
269    /// ```
270    #[inline]
271    pub fn peek<Q>(&self, key: &Q) -> Option<&V>
272    where
273        K: Borrow<Q>,
274        Q: Hash + Eq + ?Sized,
275    {
276        let hash = self.hash(key);
277        let idx = self
278            .table
279            .find(hash, |&idx| self.slab.get(idx).key.borrow() == key)?;
280        Some(&self.slab.get(*idx).value)
281    }
282
283    /// Inserts a key-value pair into the cache.
284    ///
285    /// If the cache has this key present, the value is updated.
286    #[inline]
287    pub fn insert(&mut self, key: K, value: V) {
288        let hash = self.hash(&key);
289
290        if let Some(&idx) = self
291            .table
292            .find(hash, |&idx| self.slab.get(idx).key.borrow() == &key)
293        {
294            let entry = self.slab.get_mut(idx);
295            entry.value = value;
296            entry.visited = true;
297            return;
298        }
299
300        if !self.has_enough_capacity(1, self.entry_count) {
301            if self.max_capacity == Some(0) {
302                return;
303            }
304
305            self.sieve_evict_one();
306        }
307
308        let slab_entry = SlabEntry {
309            key,
310            value,
311            hash,
312            visited: false,
313            prev: SENTINEL,
314            next: SENTINEL,
315        };
316        let idx = self.slab.allocate(slab_entry);
317
318        let slab = &self.slab;
319        self.table
320            .insert_unique(hash, idx, |&existing_idx| slab.get(existing_idx).hash);
321
322        self.deque.push_back(&mut self.slab, idx);
323        self.entry_count += 1;
324    }
325
326    /// Discards any cached value for the key.
327    ///
328    /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq`
329    /// on the borrowed form _must_ match those for the key type.
330    #[inline]
331    pub fn invalidate<Q>(&mut self, key: &Q)
332    where
333        K: Borrow<Q>,
334        Q: Hash + Eq + ?Sized,
335    {
336        let hash = self.hash(key);
337        let slab = &self.slab;
338        if let Ok(entry) = self
339            .table
340            .find_entry(hash, |&idx| slab.get(idx).key.borrow() == key)
341        {
342            let (idx, _) = entry.remove();
343            self.deque.advance_hand_past(&self.slab, idx);
344            self.deque.unlink(&mut self.slab, idx);
345            self.slab.deallocate(idx);
346            self.entry_count -= 1;
347        }
348    }
349
350    /// Discards any cached value for the key, returning the cached value.
351    ///
352    /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq`
353    /// on the borrowed form _must_ match those for the key type.
354    #[inline]
355    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
356    where
357        K: Borrow<Q>,
358        Q: Hash + Eq + ?Sized,
359    {
360        let hash = self.hash(key);
361        let slab = &self.slab;
362        if let Ok(entry) = self
363            .table
364            .find_entry(hash, |&idx| slab.get(idx).key.borrow() == key)
365        {
366            let (idx, _) = entry.remove();
367            self.deque.advance_hand_past(&self.slab, idx);
368            self.deque.unlink(&mut self.slab, idx);
369            let slab_entry = self.slab.deallocate(idx);
370            self.entry_count -= 1;
371            Some(slab_entry.value)
372        } else {
373            None
374        }
375    }
376
377    /// Discards all cached values.
378    #[cold]
379    #[inline(never)]
380    pub fn invalidate_all(&mut self) {
381        let old_capacity = self.table.capacity();
382        let old_table = std::mem::replace(&mut self.table, HashTable::new());
383        let old_slab = std::mem::replace(&mut self.slab, Slab::new());
384        self.deque.clear();
385        self.entry_count = 0;
386
387        drop(old_table);
388        drop(old_slab);
389
390        self.table.reserve(old_capacity, |&idx| {
391            // This closure is for rehashing during reserve. Since the table is
392            // empty after the swap, this will never be called, but we must
393            // provide it.
394            let _ = idx;
395            0
396        });
397    }
398
399    /// Discards cached values that satisfy a predicate.
400    ///
401    /// `invalidate_entries_if` takes a closure that returns `true` or `false`.
402    /// `invalidate_entries_if` will apply the closure to each cached value,
403    /// and if the closure returns `true`, the value will be invalidated.
404    #[cold]
405    #[inline(never)]
406    pub fn invalidate_entries_if(&mut self, mut predicate: impl FnMut(&K, &V) -> bool) {
407        let indices_to_invalidate: Vec<u32> = self
408            .slab
409            .iter()
410            .filter(|(_, entry)| predicate(&entry.key, &entry.value))
411            .map(|(idx, _)| idx)
412            .collect();
413
414        let mut invalidated = 0u64;
415        for idx in indices_to_invalidate {
416            let hash = self.slab.get(idx).hash;
417            if let Ok(entry) = self.table.find_entry(hash, |&table_idx| table_idx == idx) {
418                entry.remove();
419                self.deque.advance_hand_past(&self.slab, idx);
420                self.deque.unlink(&mut self.slab, idx);
421                self.slab.deallocate(idx);
422                invalidated += 1;
423            }
424        }
425        self.entry_count -= invalidated;
426    }
427
428    /// Creates an iterator visiting all key-value pairs in arbitrary order. The
429    /// iterator element type is `(&K, &V)`.
430    ///
431    /// Unlike the `get` method, visiting entries via an iterator do not mark
432    /// entries as visited for eviction purposes.
433    ///
434    /// # Examples
435    ///
436    /// ```rust
437    /// use micro_moka::unsync::Cache;
438    ///
439    /// let mut cache = Cache::new(100);
440    /// cache.insert("Julia", 14);
441    ///
442    /// let mut iter = cache.iter();
443    /// let (k, v) = iter.next().unwrap(); // (&K, &V)
444    /// assert_eq!(k, &"Julia");
445    /// assert_eq!(v, &14);
446    ///
447    /// assert!(iter.next().is_none());
448    /// ```
449    ///
450    pub fn iter(&self) -> Iter<'_, K, V> {
451        Iter::new(&self.slab.entries)
452    }
453}
454
455//
456// private
457//
458impl<K, V, S> Cache<K, V, S>
459where
460    K: Hash + Eq,
461    S: BuildHasher + Clone,
462{
463    #[inline]
464    fn hash<Q>(&self, key: &Q) -> u64
465    where
466        K: Borrow<Q>,
467        Q: Hash + Eq + ?Sized,
468    {
469        self.build_hasher.hash_one(key)
470    }
471
472    #[inline]
473    fn has_enough_capacity(&self, candidate_weight: u32, ws: u64) -> bool {
474        self.max_capacity
475            .map(|limit| ws + candidate_weight as u64 <= limit)
476            .unwrap_or(true)
477    }
478
479    #[cold]
480    #[inline(never)]
481    fn sieve_evict_one(&mut self) {
482        if let Some(victim_idx) = self.deque.sieve_evict(&mut self.slab) {
483            let victim_hash = self.slab.get(victim_idx).hash;
484            if let Ok(entry) = self
485                .table
486                .find_entry(victim_hash, |&table_idx| table_idx == victim_idx)
487            {
488                entry.remove();
489            }
490            self.slab.deallocate(victim_idx);
491            self.entry_count -= 1;
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::Cache;
499
500    #[test]
501    fn basic_single_thread() {
502        let mut cache = Cache::new(3);
503
504        cache.insert("a", "alice");
505        cache.insert("b", "bob");
506        assert_eq!(cache.get(&"a"), Some(&"alice"));
507        assert!(cache.contains_key(&"a"));
508        assert!(cache.contains_key(&"b"));
509        assert_eq!(cache.get(&"b"), Some(&"bob"));
510
511        cache.insert("c", "cindy");
512        assert_eq!(cache.get(&"c"), Some(&"cindy"));
513        assert!(cache.contains_key(&"c"));
514
515        assert!(cache.contains_key(&"a"));
516        assert_eq!(cache.get(&"a"), Some(&"alice"));
517        assert_eq!(cache.get(&"b"), Some(&"bob"));
518        assert!(cache.contains_key(&"b"));
519
520        // All entries are visited. SIEVE will clear visited bits during sweep
521        // and evict the first unvisited entry it finds.
522        cache.insert("d", "david");
523        assert_eq!(cache.entry_count(), 3);
524        assert!(cache.contains_key(&"d"));
525
526        cache.invalidate(&"b");
527        assert_eq!(cache.get(&"b"), None);
528        assert!(!cache.contains_key(&"b"));
529    }
530
531    #[test]
532    fn sieve_evicts_unvisited_entry() {
533        let mut cache = Cache::new(3);
534
535        cache.insert("a", "alice");
536        cache.insert("b", "bob");
537        cache.insert("c", "cindy");
538
539        // Visit only "b" and "c", leaving "a" unvisited (only inserted, never get'd).
540        cache.get(&"b");
541        cache.get(&"c");
542
543        // Insert "d": SIEVE sweeps from tail backward. "c" is visited (cleared),
544        // "b" is visited (cleared), "a" is unvisited -> evicted.
545        cache.insert("d", "david");
546        assert_eq!(cache.entry_count(), 3);
547        assert_eq!(cache.get(&"a"), None);
548        assert!(cache.contains_key(&"b"));
549        assert!(cache.contains_key(&"c"));
550        assert!(cache.contains_key(&"d"));
551    }
552
553    #[test]
554    fn sieve_visited_entries_get_second_chance() {
555        let mut cache = Cache::new(3);
556
557        cache.insert("a", "alice");
558        cache.insert("b", "bob");
559        cache.insert("c", "cindy");
560
561        // Visit all entries.
562        cache.get(&"a");
563        cache.get(&"b");
564        cache.get(&"c");
565
566        // Insert "d": SIEVE sweeps and clears all visited bits, then wraps
567        // around and evicts the first (now-unvisited) entry from the tail.
568        cache.insert("d", "david");
569        assert_eq!(cache.entry_count(), 3);
570        assert!(cache.contains_key(&"d"));
571    }
572
573    #[test]
574    fn sieve_new_entry_always_admitted() {
575        let mut cache = Cache::new(3);
576
577        cache.insert("a", "alice");
578        cache.insert("b", "bob");
579        cache.insert("c", "cindy");
580
581        // Unlike W-TinyLFU, SIEVE always admits new entries (no frequency check).
582        // Even with all existing entries heavily accessed, the new entry is admitted.
583        for _ in 0..10 {
584            cache.get(&"a");
585            cache.get(&"b");
586            cache.get(&"c");
587        }
588
589        cache.insert("d", "david");
590        assert_eq!(cache.entry_count(), 3);
591        assert!(cache.contains_key(&"d"));
592    }
593
594    #[test]
595    fn invalidate_all() {
596        let mut cache = Cache::new(100);
597
598        cache.insert("a", "alice");
599        cache.insert("b", "bob");
600        cache.insert("c", "cindy");
601        assert_eq!(cache.get(&"a"), Some(&"alice"));
602        assert_eq!(cache.get(&"b"), Some(&"bob"));
603        assert_eq!(cache.get(&"c"), Some(&"cindy"));
604        assert!(cache.contains_key(&"a"));
605        assert!(cache.contains_key(&"b"));
606        assert!(cache.contains_key(&"c"));
607
608        cache.invalidate_all();
609
610        cache.insert("d", "david");
611
612        assert!(cache.get(&"a").is_none());
613        assert!(cache.get(&"b").is_none());
614        assert!(cache.get(&"c").is_none());
615        assert_eq!(cache.get(&"d"), Some(&"david"));
616        assert!(!cache.contains_key(&"a"));
617        assert!(!cache.contains_key(&"b"));
618        assert!(!cache.contains_key(&"c"));
619        assert!(cache.contains_key(&"d"));
620    }
621
622    #[test]
623    fn invalidate_entries_if() {
624        use std::collections::HashSet;
625
626        let mut cache = Cache::new(100);
627
628        cache.insert(0, "alice");
629        cache.insert(1, "bob");
630        cache.insert(2, "alex");
631
632        assert_eq!(cache.get(&0), Some(&"alice"));
633        assert_eq!(cache.get(&1), Some(&"bob"));
634        assert_eq!(cache.get(&2), Some(&"alex"));
635        assert!(cache.contains_key(&0));
636        assert!(cache.contains_key(&1));
637        assert!(cache.contains_key(&2));
638
639        let names = ["alice", "alex"].iter().cloned().collect::<HashSet<_>>();
640        cache.invalidate_entries_if(move |_k, &v| names.contains(v));
641
642        cache.insert(3, "alice");
643
644        assert!(cache.get(&0).is_none());
645        assert!(cache.get(&2).is_none());
646        assert_eq!(cache.get(&1), Some(&"bob"));
647        assert_eq!(cache.get(&3), Some(&"alice"));
648
649        assert!(!cache.contains_key(&0));
650        assert!(cache.contains_key(&1));
651        assert!(!cache.contains_key(&2));
652        assert!(cache.contains_key(&3));
653
654        assert_eq!(cache.table.len(), 2);
655
656        cache.invalidate_entries_if(|_k, &v| v == "alice");
657        cache.invalidate_entries_if(|_k, &v| v == "bob");
658
659        assert!(cache.get(&1).is_none());
660        assert!(cache.get(&3).is_none());
661
662        assert!(!cache.contains_key(&1));
663        assert!(!cache.contains_key(&3));
664
665        assert_eq!(cache.table.len(), 0);
666    }
667
668    #[test]
669    fn remove_decrements_entry_count() {
670        let mut cache = Cache::new(3);
671        cache.insert("a", "alice");
672        cache.insert("b", "bob");
673        assert_eq!(cache.entry_count(), 2);
674
675        let removed = cache.remove(&"a");
676        assert_eq!(removed, Some("alice"));
677        assert_eq!(cache.entry_count(), 1);
678
679        cache.remove(&"nonexistent");
680        assert_eq!(cache.entry_count(), 1);
681
682        cache.remove(&"b");
683        assert_eq!(cache.entry_count(), 0);
684    }
685
686    #[test]
687    fn invalidate_decrements_entry_count() {
688        let mut cache = Cache::new(3);
689        cache.insert("a", "alice");
690        cache.insert("b", "bob");
691        assert_eq!(cache.entry_count(), 2);
692
693        cache.invalidate(&"a");
694        assert_eq!(cache.entry_count(), 1);
695
696        cache.invalidate(&"nonexistent");
697        assert_eq!(cache.entry_count(), 1);
698
699        cache.invalidate(&"b");
700        assert_eq!(cache.entry_count(), 0);
701    }
702
703    #[test]
704    fn insert_after_remove_on_full_cache() {
705        let mut cache = Cache::new(2);
706        cache.insert("a", "alice");
707        cache.insert("b", "bob");
708        assert_eq!(cache.entry_count(), 2);
709
710        cache.remove(&"a");
711        assert_eq!(cache.entry_count(), 1);
712
713        cache.insert("c", "cindy");
714        assert_eq!(cache.entry_count(), 2);
715        assert_eq!(cache.get(&"c"), Some(&"cindy"));
716        assert_eq!(cache.get(&"b"), Some(&"bob"));
717        assert_eq!(cache.get(&"a"), None);
718    }
719
720    #[test]
721    fn insert_after_invalidate_on_full_cache() {
722        let mut cache = Cache::new(2);
723        cache.insert("a", "alice");
724        cache.insert("b", "bob");
725        assert_eq!(cache.entry_count(), 2);
726
727        cache.invalidate(&"a");
728        assert_eq!(cache.entry_count(), 1);
729
730        cache.insert("c", "cindy");
731        assert_eq!(cache.entry_count(), 2);
732        assert_eq!(cache.get(&"c"), Some(&"cindy"));
733        assert_eq!(cache.get(&"b"), Some(&"bob"));
734        assert_eq!(cache.get(&"a"), None);
735    }
736
737    #[test]
738    fn invalidate_all_panic_safety() {
739        use std::panic::catch_unwind;
740        use std::panic::AssertUnwindSafe;
741        use std::sync::atomic::{AtomicU32, Ordering};
742
743        static DROP_COUNT: AtomicU32 = AtomicU32::new(0);
744
745        struct PanicOnDrop {
746            id: u32,
747            should_panic: bool,
748        }
749
750        impl Drop for PanicOnDrop {
751            fn drop(&mut self) {
752                DROP_COUNT.fetch_add(1, Ordering::Relaxed);
753                if self.should_panic {
754                    panic!("intentional panic in drop for id={}", self.id);
755                }
756            }
757        }
758
759        DROP_COUNT.store(0, Ordering::Relaxed);
760        let mut cache = Cache::new(10);
761        cache.insert(
762            1,
763            PanicOnDrop {
764                id: 1,
765                should_panic: false,
766            },
767        );
768        cache.insert(
769            2,
770            PanicOnDrop {
771                id: 2,
772                should_panic: true,
773            },
774        );
775        cache.insert(
776            3,
777            PanicOnDrop {
778                id: 3,
779                should_panic: false,
780            },
781        );
782        assert_eq!(cache.entry_count(), 3);
783
784        let result = catch_unwind(AssertUnwindSafe(|| {
785            cache.invalidate_all();
786        }));
787        assert!(result.is_err());
788
789        assert_eq!(cache.entry_count(), 0);
790        assert_eq!(cache.table.len(), 0);
791
792        cache.insert(
793            4,
794            PanicOnDrop {
795                id: 4,
796                should_panic: false,
797            },
798        );
799        assert_eq!(cache.entry_count(), 1);
800        assert!(cache.contains_key(&4));
801    }
802
803    #[test]
804    fn test_debug_format() {
805        let mut cache = Cache::new(10);
806        cache.insert('a', "alice");
807        cache.insert('b', "bob");
808        cache.insert('c', "cindy");
809
810        let debug_str = format!("{:?}", cache);
811        assert!(debug_str.starts_with('{'));
812        assert!(debug_str.contains(r#"'a': "alice""#));
813        assert!(debug_str.contains(r#"'b': "bob""#));
814        assert!(debug_str.contains(r#"'c': "cindy""#));
815        assert!(debug_str.ends_with('}'));
816    }
817
818    #[test]
819    fn sub_capacity_inserts_skip_eviction() {
820        let mut cache = Cache::new(10);
821        for i in 0u32..5 {
822            cache.insert(i, i * 10);
823        }
824        assert_eq!(cache.entry_count(), 5);
825        for i in 0u32..5 {
826            assert_eq!(cache.get(&i), Some(&(i * 10)));
827        }
828    }
829
830    #[test]
831    fn eviction_triggers_when_over_capacity() {
832        let mut cache = Cache::new(3);
833
834        cache.insert(1, "a");
835        cache.insert(2, "b");
836        cache.insert(3, "c");
837        assert_eq!(cache.entry_count(), 3);
838
839        cache.insert(4, "d");
840        assert!(cache.entry_count() <= 3);
841    }
842
843    #[test]
844    fn warmup_to_full_transition() {
845        let mut cache = Cache::new(4);
846
847        cache.insert(1, "a");
848        cache.insert(2, "b");
849        assert_eq!(cache.entry_count(), 2);
850
851        cache.insert(3, "c");
852        cache.insert(4, "d");
853        assert_eq!(cache.entry_count(), 4);
854
855        for _ in 0..5 {
856            cache.get(&1);
857            cache.get(&2);
858            cache.get(&3);
859            cache.get(&4);
860        }
861
862        cache.insert(5, "e");
863        assert!(cache.entry_count() <= 4);
864    }
865
866    #[test]
867    fn invalidate_and_remove_skip_eviction_below_capacity() {
868        let mut cache = Cache::new(10);
869        cache.insert(1, "a");
870        cache.insert(2, "b");
871        cache.insert(3, "c");
872        assert_eq!(cache.entry_count(), 3);
873
874        cache.invalidate(&1);
875        assert_eq!(cache.entry_count(), 2);
876
877        let val = cache.remove(&2);
878        assert_eq!(val, Some("b"));
879        assert_eq!(cache.entry_count(), 1);
880
881        assert_eq!(cache.get(&3), Some(&"c"));
882    }
883
884    #[test]
885    fn peek_returns_value() {
886        let mut cache = Cache::new(10);
887        cache.insert("a", "alice");
888        cache.insert("b", "bob");
889
890        assert_eq!(cache.peek(&"a"), Some(&"alice"));
891        assert_eq!(cache.peek(&"b"), Some(&"bob"));
892    }
893
894    #[test]
895    fn peek_returns_none_for_missing_key() {
896        let cache = Cache::<&str, &str>::new(10);
897        assert_eq!(cache.peek(&"missing"), None);
898    }
899
900    #[test]
901    fn peek_does_not_set_visited() {
902        let mut cache = Cache::new(3);
903
904        cache.insert("a", "alice");
905        cache.insert("b", "bob");
906        cache.insert("c", "cindy");
907
908        // peek() should NOT set the visited bit.
909        cache.peek(&"a");
910        cache.peek(&"b");
911        cache.peek(&"c");
912
913        // None of the entries are visited, so SIEVE evicts the first unvisited
914        // entry from the tail (insertion order: a, b, c; sweep starts at tail=c).
915        cache.insert("d", "david");
916
917        // One entry was evicted to make room for "d".
918        assert_eq!(cache.entry_count(), 3);
919        assert!(cache.contains_key(&"d"));
920    }
921
922    #[test]
923    fn contains_key_with_shared_reference() {
924        let mut cache = Cache::new(10);
925        cache.insert("a", 1);
926        cache.insert("b", 2);
927        cache.insert("c", 3);
928
929        let cache_ref: &Cache<&str, i32> = &cache;
930        assert!(cache_ref.contains_key(&"a"));
931        assert!(cache_ref.contains_key(&"b"));
932        assert!(cache_ref.contains_key(&"c"));
933        assert!(!cache_ref.contains_key(&"d"));
934    }
935
936    #[test]
937    fn zero_capacity_insert_returns_immediately() {
938        let mut cache = Cache::new(0);
939        cache.insert("a", "alice");
940        assert_eq!(cache.entry_count(), 0);
941        assert!(!cache.contains_key(&"a"));
942        assert_eq!(cache.get(&"a"), None);
943    }
944
945    #[test]
946    fn update_existing_key_sets_visited() {
947        let mut cache = Cache::new(3);
948
949        cache.insert("a", "alice");
950        cache.insert("b", "bob");
951        cache.insert("c", "cindy");
952
953        // Update "a" with a new value. This should set visited=true but NOT
954        // move it to the back (SIEVE maintains insertion order).
955        cache.insert("a", "anna");
956        assert_eq!(cache.get(&"a"), Some(&"anna"));
957        assert_eq!(cache.entry_count(), 3);
958
959        // "b" and "c" are not visited. Insert "d" to trigger eviction.
960        // SIEVE should skip "a" (visited) and evict an unvisited entry.
961        cache.insert("d", "david");
962        assert_eq!(cache.entry_count(), 3);
963        assert!(cache.contains_key(&"a"));
964        assert!(cache.contains_key(&"d"));
965    }
966
967    #[test]
968    fn sieve_hand_advances_across_evictions() {
969        let mut cache = Cache::new(3);
970
971        cache.insert("a", "alice");
972        cache.insert("b", "bob");
973        cache.insert("c", "cindy");
974
975        // No entries visited. First eviction should evict from tail end.
976        cache.insert("d", "david");
977        assert_eq!(cache.entry_count(), 3);
978
979        // Insert another to trigger second eviction. Hand should have advanced.
980        cache.insert("e", "eve");
981        assert_eq!(cache.entry_count(), 3);
982
983        // Insert a third to trigger third eviction.
984        cache.insert("f", "frank");
985        assert_eq!(cache.entry_count(), 3);
986    }
987
988    #[test]
989    fn sieve_multiple_evictions_cycle() {
990        let mut cache = Cache::new(2);
991
992        for i in 0u32..20 {
993            cache.insert(i, i * 10);
994            assert!(cache.entry_count() <= 2);
995        }
996
997        // The last two inserted should be present.
998        assert_eq!(cache.entry_count(), 2);
999        assert!(cache.contains_key(&19));
1000        assert!(cache.contains_key(&18));
1001    }
1002
1003    #[test]
1004    fn update_below_capacity_no_eviction() {
1005        let mut cache = Cache::new(5);
1006
1007        cache.insert("a", "alice");
1008        cache.insert("b", "bob");
1009        cache.insert("c", "cindy");
1010        assert_eq!(cache.entry_count(), 3);
1011
1012        // Updating below capacity should not evict anything.
1013        cache.insert("b", "betty");
1014        assert_eq!(cache.entry_count(), 3);
1015        assert_eq!(cache.get(&"b"), Some(&"betty"));
1016        assert!(cache.contains_key(&"a"));
1017        assert!(cache.contains_key(&"c"));
1018    }
1019}