Skip to main content

subetha_cxc/
shared_lru_cache.rs

1//! `SharedLRUCache<K, V>` - cross-process LRU cache.
2//!
3//! Composite primitive demonstrating the layered-composition
4//! thesis at full strength: combines
5//! [`SharedHashMap<K, u32>`](crate::SharedHashMap) for O(1) lookup
6//! with [`SharedLinkedList<(K, V)>`](crate::SharedLinkedList) for
7//! O(1) move-to-front and O(1) eviction.
8//!
9//! # Files (3 per cache, all under a base path)
10//!
11//! - `<base>.map.bin`        - the SharedHashMap<K, u32>
12//! - `<base>.list.<region>`  - SharedLinkedList's underlying region
13//! - (the linked list is single-file; uses one MMF for the region)
14//!
15//! # Concurrency
16//!
17//! - `get` / `contains_key` / `snapshot_*` / `len`: **lock-free
18//!   read paths**. Multi-reader safe at any concurrency. Does NOT
19//!   promote MRU order.
20//! - `touch` / `get_and_touch` / `put` / `remove` / `evict_oldest`:
21//!   **single-writer** operations. Wrap in a SharedSemaphore(1) or
22//!   the application's own coordination for cross-process writer
23//!   serialisation.
24//!
25//! # Why split get vs touch
26//!
27//! Many production caches (tokio::sync MokaCache, Java Caffeine)
28//! separate the "look up the value" path from the "promote to MRU"
29//! path. Read-heavy workloads where LRU ordering is approximate get
30//! the cheap path; strict LRU workloads call `get_and_touch`. This
31//! lets the cache be useful in both regimes.
32//!
33//! # Eviction
34//!
35//! `put(k, v)` always succeeds when the underlying map has room.
36//! If the cache is at capacity AND `k` is not already present, the
37//! LRU entry (back of list) is evicted first via pop_back +
38//! map.remove.
39//!
40//! # Long-running workload limit
41//!
42//! The underlying [`SharedHashMap`] is sized
43//! to 8x the cache capacity to absorb tombstone accumulation from
44//! eviction. After roughly 7x capacity insert-then-evict cycles,
45//! tombstones fill the map and `put` returns `Map(Full)`. For
46//! long-running workloads, either size the cache larger or wait for
47//! the SharedHashMap.compact() reclamation primitive (separate
48//! follow-on). For typical caches that hover near capacity, the
49//! tombstone budget is far more than enough.
50
51use std::marker::PhantomData;
52use std::path::{Path, PathBuf};
53
54use crate::shared_hash_map::{MapError, SharedHashMap};
55use crate::shared_linked_list::{LinkedListError, NodeHandle, SharedLinkedList};
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LRUError {
59    Map(MapError),
60    LinkedList(LinkedListError),
61    LayoutMismatch,
62    IoError(std::io::ErrorKind),
63}
64
65impl From<MapError> for LRUError {
66    fn from(e: MapError) -> Self { Self::Map(e) }
67}
68impl From<LinkedListError> for LRUError {
69    fn from(e: LinkedListError) -> Self { Self::LinkedList(e) }
70}
71impl From<std::io::Error> for LRUError {
72    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
73}
74
75fn map_path(base: &Path) -> PathBuf {
76    let mut p = base.to_path_buf();
77    let stem = p.file_name().unwrap().to_string_lossy().to_string();
78    p.set_file_name(format!("{stem}.map.bin"));
79    p
80}
81fn list_path(base: &Path) -> PathBuf {
82    let mut p = base.to_path_buf();
83    let stem = p.file_name().unwrap().to_string_lossy().to_string();
84    p.set_file_name(format!("{stem}.list.bin"));
85    p
86}
87
88pub struct SharedLRUCache<
89    K: Copy + Eq + Default + 'static,
90    V: Copy + Default + 'static,
91> {
92    map: SharedHashMap<K, u32>,
93    list: SharedLinkedList<(K, V)>,
94    capacity: u32,
95    _phantom: PhantomData<(K, V)>,
96    header_sidecar: subetha_core::HandshakeHeader,
97    ring_sidecar: Box<subetha_core::ObservationRing>,
98}
99
100impl<
101    K: Copy + Eq + Default + Send + Sync + 'static,
102    V: Copy + Default + Send + Sync + 'static,
103> subetha_sidecar::AdaptiveInstance for SharedLRUCache<K, V> {
104    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
105    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
106    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
107        Box::new(subetha_sidecar::NoMigrationPolicy)
108    }
109}
110
111impl<
112    K: Copy + Eq + Default + 'static,
113    V: Copy + Default + 'static,
114> SharedLRUCache<K, V> {
115    /// Create a new LRU cache with `capacity` entries.
116    ///
117    /// SIZING: the underlying SharedHashMap is sized to 8x capacity
118    /// to absorb tombstone accumulation (open-addressing leaves a
119    /// tombstone on every remove; LRU caches do many removes via
120    /// eviction). The linked list region is sized to capacity + 2
121    /// (one sentinel head + capacity nodes + 1 spare for the
122    /// pop-then-push transition during update).
123    pub fn create(
124        base_path: impl AsRef<Path>, capacity: u32,
125    ) -> Result<Self, LRUError> {
126        assert!(capacity >= 1);
127        let base = base_path.as_ref();
128        let map = SharedHashMap::<K, u32>::create(
129            map_path(base), (capacity as usize * 8).max(32),
130        )?;
131        let list = SharedLinkedList::<(K, V)>::create(
132            list_path(base), capacity as usize + 2,
133        )?;
134        Ok(Self {
135            map, list, capacity,
136            _phantom: PhantomData,
137            header_sidecar: subetha_core::HandshakeHeader::new(),
138            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
139        })
140    }
141
142    pub fn open(
143        base_path: impl AsRef<Path>, capacity: u32,
144    ) -> Result<Self, LRUError> {
145        let base = base_path.as_ref();
146        let map = SharedHashMap::<K, u32>::open(
147            map_path(base), (capacity as usize * 8).max(32),
148        )?;
149        let list = SharedLinkedList::<(K, V)>::open(
150            list_path(base), capacity as usize + 2,
151        )?;
152        Ok(Self {
153            map, list, capacity,
154            _phantom: PhantomData,
155            header_sidecar: subetha_core::HandshakeHeader::new(),
156            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
157        })
158    }
159
160    #[inline]
161    pub fn capacity(&self) -> u32 { self.capacity }
162
163    /// Current number of entries.
164    pub fn len(&self) -> usize {
165        self.list.len()
166    }
167
168    pub fn is_empty(&self) -> bool { self.len() == 0 }
169
170    /// Lock-free lookup. Does NOT promote `k` to MRU position.
171    /// Use [`get_and_touch`](Self::get_and_touch) or
172    /// [`touch`](Self::touch) for strict LRU semantics.
173    pub fn get(&self, key: &K) -> Option<V> {
174        let r = (|| {
175            let idx = self.map.get(key)?;
176            let (k, v) = self.list.get(NodeHandle::new(idx))?;
177            // Sanity: the list slot we looked up via the map MUST hold
178            // the same key. If not, the cache is corrupted (shouldn't
179            // happen since map and list are updated together in writer
180            // ops). Return None defensively rather than asserting.
181            if k != *key { return None; }
182            Some(v)
183        })();
184        self.ring_sidecar.push_op(
185            crate::sidecar_ops::lru_cache::OP_GET,
186            if r.is_none() { 2 } else { 0 },
187        );
188        r
189    }
190
191    /// True if key is present (lock-free).
192    pub fn contains_key(&self, key: &K) -> bool {
193        self.map.contains_key(key)
194    }
195
196    /// Promote `key` to MRU position. Writer-side. Returns true if
197    /// the key was present and was promoted.
198    pub fn touch(&self, key: &K) -> bool {
199        let promoted = (|| {
200            let idx = self.map.get(key)?;
201            let (k, v) = self.list.remove(NodeHandle::new(idx))?;
202            // Push to front; get the new handle; update the map.
203            match self.list.push_front((k, v)) {
204                Ok(new_handle) => {
205                    // Map insert may collide; map and list updates are
206                    // separate writes so the lock-free LRU pattern
207                    // already tolerates ordering anomalies here.
208                    self.map.insert(k, new_handle.index).ok();
209                    Some(true)
210                }
211                Err(_) => {
212                    // Shouldn't happen since we just freed a slot, but
213                    // be defensive: re-insert at the back.
214                    self.list.push_back((k, v)).ok();
215                    Some(false)
216                }
217            }
218        })().unwrap_or(false);
219        self.ring_sidecar.push_op(
220            crate::sidecar_ops::lru_cache::OP_TOUCH,
221            if promoted { 0 } else { 2 }, // 2 = key absent
222        );
223        promoted
224    }
225
226    /// Look up and promote in one call. Writer-side.
227    pub fn get_and_touch(&self, key: &K) -> Option<V> {
228        let v = self.get(key)?;
229        self.touch(key);
230        Some(v)
231    }
232
233    /// Insert / update. Writer-side. If the cache is at capacity
234    /// AND `key` is new, evicts the LRU entry first. Returns the
235    /// previous value if `key` was present.
236    pub fn put(&self, key: K, value: V) -> Result<Option<V>, LRUError> {
237        let r = self.put_inner(key, value);
238        self.ring_sidecar.push_op(
239            crate::sidecar_ops::lru_cache::OP_PUT,
240            if r.is_err() { 1 } else { 0 },
241        );
242        r
243    }
244
245    fn put_inner(&self, key: K, value: V) -> Result<Option<V>, LRUError> {
246        // Existing key: update in place + promote.
247        if let Some(idx) = self.map.get(&key) {
248            let old = self.list.remove(NodeHandle::new(idx))
249                .map(|(_, v)| v);
250            let new_handle = self.list.push_front((key, value))?;
251            self.map.insert(key, new_handle.index)?;
252            return Ok(old);
253        }
254        // New key: maybe evict.
255        if self.len() >= self.capacity as usize {
256            self.evict_oldest();
257        }
258        let new_handle = self.list.push_front((key, value))?;
259        self.map.insert(key, new_handle.index)?;
260        Ok(None)
261    }
262
263    /// Remove a key. Writer-side. Returns the value if present.
264    pub fn remove(&self, key: &K) -> Option<V> {
265        let r = (|| {
266            let idx = self.map.remove(key)?;
267            let (_, v) = self.list.remove(NodeHandle::new(idx))?;
268            Some(v)
269        })();
270        self.ring_sidecar.push_op(
271            crate::sidecar_ops::lru_cache::OP_REMOVE,
272            if r.is_none() { 2 } else { 0 },
273        );
274        r
275    }
276
277    /// Evict the LRU (least-recently-used) entry. Writer-side.
278    /// Returns (key, value) of the evicted entry, or None if empty.
279    pub fn evict_oldest(&self) -> Option<(K, V)> {
280        // Pop_back gives the LRU entry.
281        let r = (|| {
282            let (k, v) = self.list.pop_back()?;
283            // Map may already be missing the key under concurrent races;
284            // we just want the eviction to commit either way.
285            let _removed = self.map.remove(&k);
286            Some((k, v))
287        })();
288        self.ring_sidecar.push_op(
289            crate::sidecar_ops::lru_cache::OP_EVICT,
290            if r.is_none() { 2 } else { 0 },
291        );
292        r
293    }
294
295    /// Snapshot all entries from MRU (front) to LRU (back).
296    /// Lock-free; not stable under concurrent writers.
297    pub fn snapshot_mru_first(&self) -> Vec<(K, V)> {
298        self.list.iter_forward()
299    }
300
301    /// Snapshot all entries from LRU (back) to MRU (front).
302    pub fn snapshot_lru_first(&self) -> Vec<(K, V)> {
303        self.list.iter_backward()
304    }
305
306    pub fn flush(&self) -> Result<(), LRUError> {
307        self.map.flush()?;
308        self.list.flush()?;
309        Ok(())
310    }
311
312    pub fn flush_async(&self) -> Result<(), LRUError> {
313        self.map.flush_async()?;
314        self.list.flush_async()?;
315        Ok(())
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn tmp_base(name: &str) -> PathBuf {
324        let mut p = std::env::temp_dir();
325        let pid = std::process::id();
326        p.push(format!("subetha-lru-{name}-{pid}"));
327        p
328    }
329
330    fn cleanup(base: &Path) {
331        std::fs::remove_file(map_path(base)).ok();
332        std::fs::remove_file(list_path(base)).ok();
333    }
334
335    #[test]
336    fn create_initial_state_is_empty() {
337        let base = tmp_base("init");
338        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
339        assert_eq!(c.capacity(), 16);
340        assert_eq!(c.len(), 0);
341        assert!(c.is_empty());
342        assert_eq!(c.get(&1), None);
343        cleanup(&base);
344    }
345
346    #[test]
347    fn put_get_round_trip() {
348        let base = tmp_base("rt");
349        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
350        c.put(1, 100).unwrap();
351        c.put(2, 200).unwrap();
352        c.put(3, 300).unwrap();
353        assert_eq!(c.get(&1), Some(100));
354        assert_eq!(c.get(&2), Some(200));
355        assert_eq!(c.get(&3), Some(300));
356        assert_eq!(c.get(&999), None);
357        assert_eq!(c.len(), 3);
358        cleanup(&base);
359    }
360
361    #[test]
362    fn put_existing_key_updates_and_promotes() {
363        let base = tmp_base("update");
364        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
365        c.put(1, 100).unwrap();
366        c.put(2, 200).unwrap();
367        c.put(3, 300).unwrap();
368        // Update key 1.
369        let prev = c.put(1, 111).unwrap();
370        assert_eq!(prev, Some(100));
371        assert_eq!(c.get(&1), Some(111));
372        assert_eq!(c.len(), 3);
373        // Key 1 should now be at the front (MRU).
374        let snap = c.snapshot_mru_first();
375        assert_eq!(snap[0], (1, 111));
376        cleanup(&base);
377    }
378
379    #[test]
380    fn get_does_not_promote() {
381        let base = tmp_base("get-no-promote");
382        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
383        c.put(1, 100).unwrap();
384        c.put(2, 200).unwrap();
385        c.put(3, 300).unwrap();
386        // Snapshot order is push-front-order, so MRU = 3.
387        let before = c.snapshot_mru_first();
388        assert_eq!(before, vec![(3, 300), (2, 200), (1, 100)]);
389        // Plain get on key 1 should NOT promote.
390        c.get(&1).unwrap();
391        let after = c.snapshot_mru_first();
392        assert_eq!(after, vec![(3, 300), (2, 200), (1, 100)],
393            "plain get must not change order");
394        cleanup(&base);
395    }
396
397    #[test]
398    fn touch_promotes_to_front() {
399        let base = tmp_base("touch");
400        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
401        c.put(1, 100).unwrap();
402        c.put(2, 200).unwrap();
403        c.put(3, 300).unwrap();
404        // Touch key 1: should move it to front.
405        assert!(c.touch(&1));
406        let snap = c.snapshot_mru_first();
407        assert_eq!(snap, vec![(1, 100), (3, 300), (2, 200)]);
408        cleanup(&base);
409    }
410
411    #[test]
412    fn touch_nonexistent_returns_false() {
413        let base = tmp_base("touch-none");
414        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
415        c.put(1, 100).unwrap();
416        assert!(!c.touch(&999));
417        cleanup(&base);
418    }
419
420    #[test]
421    fn get_and_touch_combines_both() {
422        let base = tmp_base("get-touch");
423        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
424        c.put(1, 100).unwrap();
425        c.put(2, 200).unwrap();
426        c.put(3, 300).unwrap();
427        let v = c.get_and_touch(&1).unwrap();
428        assert_eq!(v, 100);
429        let snap = c.snapshot_mru_first();
430        assert_eq!(snap[0], (1, 100));
431        cleanup(&base);
432    }
433
434    #[test]
435    fn eviction_at_capacity_drops_oldest() {
436        let base = tmp_base("evict");
437        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 3).unwrap();
438        c.put(1, 10).unwrap();
439        c.put(2, 20).unwrap();
440        c.put(3, 30).unwrap();
441        // Now full. Insert 4 -> should evict key 1 (LRU).
442        let prev = c.put(4, 40).unwrap();
443        assert_eq!(prev, None);
444        assert_eq!(c.len(), 3);
445        assert_eq!(c.get(&1), None, "key 1 should have been evicted");
446        assert_eq!(c.get(&2), Some(20));
447        assert_eq!(c.get(&3), Some(30));
448        assert_eq!(c.get(&4), Some(40));
449        cleanup(&base);
450    }
451
452    #[test]
453    fn touch_prevents_eviction_of_recently_used() {
454        let base = tmp_base("touch-evict");
455        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 3).unwrap();
456        c.put(1, 10).unwrap();
457        c.put(2, 20).unwrap();
458        c.put(3, 30).unwrap();
459        // Touch key 1 to make it MRU.
460        c.touch(&1);
461        // Insert 4 -> should evict key 2 (now LRU since 1 was touched).
462        c.put(4, 40).unwrap();
463        assert_eq!(c.get(&1), Some(10), "touched key 1 should survive");
464        assert_eq!(c.get(&2), None, "untouched key 2 should be evicted");
465        assert_eq!(c.get(&3), Some(30));
466        assert_eq!(c.get(&4), Some(40));
467        cleanup(&base);
468    }
469
470    #[test]
471    fn remove_cleans_both_map_and_list() {
472        let base = tmp_base("rm");
473        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
474        c.put(1, 10).unwrap();
475        c.put(2, 20).unwrap();
476        c.put(3, 30).unwrap();
477        let v = c.remove(&2).unwrap();
478        assert_eq!(v, 20);
479        assert_eq!(c.len(), 2);
480        assert_eq!(c.get(&2), None);
481        // Other keys still present.
482        assert_eq!(c.get(&1), Some(10));
483        assert_eq!(c.get(&3), Some(30));
484        // snapshot shouldn't contain key 2.
485        let snap = c.snapshot_mru_first();
486        let keys: Vec<u32> = snap.iter().map(|(k, _)| *k).collect();
487        assert!(!keys.contains(&2));
488        cleanup(&base);
489    }
490
491    #[test]
492    fn evict_oldest_returns_lru() {
493        let base = tmp_base("evict-oldest");
494        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
495        c.put(1, 10).unwrap();
496        c.put(2, 20).unwrap();
497        c.put(3, 30).unwrap();
498        let evicted = c.evict_oldest().unwrap();
499        assert_eq!(evicted, (1, 10));
500        assert_eq!(c.len(), 2);
501        assert_eq!(c.get(&1), None);
502        cleanup(&base);
503    }
504
505    #[test]
506    fn evict_oldest_on_empty_returns_none() {
507        let base = tmp_base("evict-empty");
508        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 4).unwrap();
509        assert_eq!(c.evict_oldest(), None);
510        cleanup(&base);
511    }
512
513    #[test]
514    fn snapshot_mru_and_lru_first_are_reverses() {
515        let base = tmp_base("snap");
516        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
517        for (k, v) in [(1, 10), (2, 20), (3, 30)] {
518            c.put(k, v).unwrap();
519        }
520        let mru = c.snapshot_mru_first();
521        let mut lru = c.snapshot_lru_first();
522        lru.reverse();
523        assert_eq!(mru, lru);
524        cleanup(&base);
525    }
526
527    #[test]
528    fn cross_handle_visibility() {
529        let base = tmp_base("cross-handle");
530        let writer: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
531        let reader: SharedLRUCache<u32, u32> = SharedLRUCache::open(&base, 8).unwrap();
532        writer.put(42, 4242).unwrap();
533        writer.put(7, 77).unwrap();
534        assert_eq!(reader.get(&42), Some(4242));
535        assert_eq!(reader.get(&7), Some(77));
536        // reader.touch() also works (it's writer-side, but touch
537        // is acceptable if the application coordinates writes).
538        writer.evict_oldest();
539        assert_eq!(reader.get(&42), None);  // 42 was LRU
540        cleanup(&base);
541    }
542
543    #[test]
544    fn struct_key_value_round_trip() {
545        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
546        #[repr(C)]
547        struct UserKey { realm: u32, user: u32 }
548        #[derive(Clone, Copy, Debug, PartialEq, Default)]
549        #[repr(C)]
550        struct Session { token: u64, expires_us: u64 }
551        let base = tmp_base("struct");
552        let c: SharedLRUCache<UserKey, Session> = SharedLRUCache::create(&base, 8).unwrap();
553        let k = UserKey { realm: 1, user: 42 };
554        let v = Session { token: 0xDEAD_BEEF, expires_us: 9_999_999_999 };
555        c.put(k, v).unwrap();
556        assert_eq!(c.get(&k), Some(v));
557        cleanup(&base);
558    }
559
560    #[test]
561    fn disk_persistence_survives_reopen() {
562        let base = tmp_base("disk");
563        {
564            let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
565            c.put(1, 100).unwrap();
566            c.put(2, 200).unwrap();
567            c.put(3, 300).unwrap();
568            c.flush().unwrap();
569        }
570        let c2: SharedLRUCache<u32, u32> = SharedLRUCache::open(&base, 8).unwrap();
571        assert_eq!(c2.len(), 3);
572        assert_eq!(c2.get(&1), Some(100));
573        assert_eq!(c2.get(&2), Some(200));
574        assert_eq!(c2.get(&3), Some(300));
575        cleanup(&base);
576    }
577
578    #[test]
579    fn many_evictions_maintain_mru_correctness() {
580        // Stress within the tombstone budget (8x capacity).
581        // capacity=10 -> 80 map slots. 50 puts = 10 active + 40
582        // tombstones = 50/80 = 62% load, within probe limit.
583        let base = tmp_base("many-evict");
584        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 10).unwrap();
585        for k in 0..50u32 {
586            c.put(k, k * 10).unwrap();
587        }
588        // After 50 puts into a 10-slot cache, exactly keys 40..50
589        // should be present (the most-recently-inserted 10).
590        assert_eq!(c.len(), 10);
591        for k in 0..40u32 {
592            assert_eq!(c.get(&k), None, "key {k} should have been evicted");
593        }
594        for k in 40..50u32 {
595            assert_eq!(c.get(&k), Some(k * 10), "key {k} should be present");
596        }
597        cleanup(&base);
598    }
599
600    #[test]
601    fn touched_keys_survive_subsequent_evictions() {
602        // Insert N keys, touch a specific subset, then insert N more.
603        // The touched keys should survive; the un-touched original
604        // keys should be evicted.
605        let base = tmp_base("touch-survival");
606        let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 5).unwrap();
607        // Fill: keys 0..5 with 4 = MRU (push_front order).
608        for k in 0..5u32 { c.put(k, k * 10).unwrap(); }
609        // Touch keys 0 and 1 to make them MRU; order is now 1, 0,
610        // then the remaining un-touched ones below.
611        c.touch(&0);
612        c.touch(&1);
613        // Insert 3 new keys; LRU is now whatever wasn't touched.
614        c.put(100, 1000).unwrap();
615        c.put(101, 1010).unwrap();
616        c.put(102, 1020).unwrap();
617        // Touched keys 0 and 1 should survive.
618        assert_eq!(c.get(&0), Some(0));
619        assert_eq!(c.get(&1), Some(10));
620        // New keys present.
621        assert_eq!(c.get(&100), Some(1000));
622        assert_eq!(c.get(&101), Some(1010));
623        assert_eq!(c.get(&102), Some(1020));
624        cleanup(&base);
625    }
626}