Skip to main content

cqlite_core/storage/cache/
mod.rs

1//! Shared, bytes-bounded, sharded decompressed-chunk cache (issue #1567, Epic B/B1).
2//!
3//! A cache of *decompressed* SSTable compression chunks keyed by authoritative
4//! `(sstable identity, chunk index)`. It is the single biggest lever for
5//! repeated-read latency identified by the July 2026 read-path audit: every
6//! wired read site consults it before reading+decompressing, so a repeat read of
7//! a resident chunk is a refcount bump instead of a disk read + decompress.
8//!
9//! # Design (owner decision #1, LOCKED — see
10//! `openspec/changes/decompressed-chunk-cache/design.md`)
11//!
12//! - **Value = [`bytes::Bytes`]** (D3; substrate migration issue #1940): a hit is a
13//!   `Bytes::clone` (refcount bump), never a chunk-sized memcpy. Insert converts the
14//!   decompressed `Vec<u8>` once via `Bytes::from(vec)`, which is ZERO-COPY — it
15//!   reuses the `Vec`'s existing heap allocation rather than allocating a fresh
16//!   `Arc<[u8]>` backing store and memcpy'ing into it (as `Arc::from(boxed_slice)`
17//!   did). This is what lets the windowed scan reach ≤1 alloc/chunk: the single
18//!   decompress-output allocation flows all the way into the cache untouched, and a
19//!   window fill borrows a refcounted `Bytes` view of it. `Bytes` is `Arc`-backed,
20//!   so the refcount-bump-on-hit contract is preserved verbatim.
21//! - **Bytes-bounded, not entry-count** (spec R1): each entry is weighed by its
22//!   decompressed length; after an insert the owning shard evicts LRU entries
23//!   until it is within its byte budget. A single entry larger than the budget is
24//!   retained (we never evict below one live entry) — the read path must always be
25//!   able to return the chunk it just produced.
26//! - **Hand-sharded `Mutex<LruCache>`** (D2): `shards.len()` is a power of two.
27//!   The hit path locks exactly ONE shard and calls `LruCache::get` (which mutates
28//!   recency — hence a `Mutex`, not an `RwLock`; but sharded so contention is
29//!   `1/N`, never a single process-wide lock). Reuses the tested `lru` crate
30//!   internals; no new external dependency.
31//! - **Poison-tolerant** (D2): every lock is taken with
32//!   `lock().unwrap_or_else(|e| e.into_inner())` so one panicking thread cannot
33//!   turn the cache into a panic-for-everyone. No `unwrap()`/`expect()` here.
34//! - **No-heuristics** (mandate #28): keys are `(u64 sstable id, u64 chunk index)`
35//!   derived from authoritative reader identity + chunk offsets — never inferred
36//!   from decompressed byte content.
37//!
38//! The default is a `Box<[Mutex<Shard>]>` rather than a fixed `[Mutex<Shard>; N]`
39//! array so the shard count is a constructor parameter: production uses
40//! [`DEFAULT_SHARDS`], while unit tests use a single shard for deterministic
41//! eviction ordering. Both remain power-of-two hand-sharded `Mutex<LruCache>` —
42//! the design intent is preserved.
43
44pub mod global_key_offset;
45
46pub(crate) use global_key_offset::GlobalKeyCacheSnapshot;
47pub use global_key_offset::{
48    GenerationIdentity, GlobalKeyOffsetCache, PartitionLoc, DEFAULT_GLOBAL_KEY_CACHE_BYTES,
49    DEFAULT_GLOBAL_KEY_CACHE_SHARDS,
50};
51
52use bytes::Bytes;
53use lru::LruCache;
54use std::hash::{Hash, Hasher};
55use std::sync::atomic::{AtomicU64, Ordering};
56use std::sync::Mutex;
57
58/// Default shard count (power of two). Production readers use this via
59/// [`DecompressedChunkCache::with_budget_bytes`].
60pub const DEFAULT_SHARDS: usize = 16;
61
62/// Default total byte budget when a cache is constructed without an explicit
63/// budget (mirrors `config.memory.block_cache.max_size`'s 256 MiB default).
64pub const DEFAULT_BUDGET_BYTES: usize = 256 * 1024 * 1024;
65
66/// Authoritative cache key: an SSTable identity hash and a chunk discriminator.
67///
68/// `chunk_index` carries whichever authoritative offset the wiring site uses —
69/// a real chunk index (windowed scan, BTI target chunk) or an index-resolved
70/// `block_offset` (BIG point read). Sites keep separate key namespaces by folding
71/// a site salt into `sstable` (design D4), so numerically-overlapping values from
72/// different sites never collide.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub struct ChunkKey {
75    /// Stable hash of the SSTable's `file_path` (+ generation), optionally XORed
76    /// with a per-site namespace salt.
77    pub sstable: u64,
78    /// Authoritative chunk index or index-resolved offset within `sstable`.
79    pub chunk_index: u64,
80    /// Extra discriminant for a size-dependent range read. The chunk-index sites
81    /// (BTI, windowed scan) decompress a whole compression chunk whose bytes are
82    /// fully determined by `chunk_index`, so they use `aux = 0`. The BIG
83    /// point-read path (`get_cached_data`) reads an index-resolved
84    /// `(block_offset, size)` byte range whose decompressed content depends on
85    /// BOTH the offset AND the length: two reads at the same `block_offset` with
86    /// different `size` decompress different input and MUST NOT alias. `aux`
87    /// carries `size` for that path so the key is complete (roborev #1567), and
88    /// because `(u64 offset, u32 size)` cannot be packed into one `u64` without
89    /// loss, `aux` is a first-class key field, not folded into `chunk_index`.
90    pub aux: u64,
91}
92
93impl ChunkKey {
94    /// Construct a key from an sstable identity hash and a chunk discriminator
95    /// (whole-chunk sites; `aux = 0`).
96    #[inline]
97    pub fn new(sstable: u64, chunk_index: u64) -> Self {
98        Self {
99            sstable,
100            chunk_index,
101            aux: 0,
102        }
103    }
104
105    /// Construct a key with an extra discriminant (`aux`) for a size-dependent
106    /// range read (the BIG point-read path keys `aux = size`).
107    #[inline]
108    pub fn with_aux(sstable: u64, chunk_index: u64, aux: u64) -> Self {
109        Self {
110            sstable,
111            chunk_index,
112            aux,
113        }
114    }
115}
116
117/// One shard: an unbounded-by-count `LruCache` plus running byte accounting.
118struct Shard {
119    lru: LruCache<ChunkKey, Bytes>,
120    current_bytes: usize,
121}
122
123impl Shard {
124    fn new() -> Self {
125        Self {
126            // Used UNBOUNDED by count; the byte budget is enforced explicitly in
127            // `insert` via `pop_lru`. `LruCache::unbounded` never evicts on its own.
128            lru: LruCache::unbounded(),
129            current_bytes: 0,
130        }
131    }
132}
133
134/// A shared, bytes-bounded, sharded decompressed-chunk cache.
135pub struct DecompressedChunkCache {
136    shards: Box<[Mutex<Shard>]>,
137    /// `shards.len() - 1`; `shards.len()` is always a power of two.
138    mask: usize,
139    /// Per-shard byte budget (`total_budget / shards.len()`, at least 1).
140    budget_per_shard: usize,
141    /// When `true` this is a genuine no-op cache (issue #1568): `get` never
142    /// stores/returns anything and `insert` never retains, so reads bypass the
143    /// cache entirely. Built by [`disabled`](Self::disabled) when
144    /// `block_cache.enabled == false`. Distinct from a zero-budget cache, which
145    /// would still retain one oversized entry per shard.
146    disabled: bool,
147    hits: AtomicU64,
148    misses: AtomicU64,
149    /// Cumulative count of entries evicted to stay within the byte budget
150    /// (issue #1571, B5). Relaxed atomic — advisory observability, never on a
151    /// fence-bearing path.
152    evictions: AtomicU64,
153}
154
155impl std::fmt::Debug for DecompressedChunkCache {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("DecompressedChunkCache")
158            .field("shards", &self.shards.len())
159            .field("budget_per_shard", &self.budget_per_shard)
160            .field("resident_bytes", &self.resident_bytes())
161            .field("hits", &self.hits.load(Ordering::Relaxed))
162            .field("misses", &self.misses.load(Ordering::Relaxed))
163            .field("evictions", &self.evictions.load(Ordering::Relaxed))
164            .finish()
165    }
166}
167
168impl DecompressedChunkCache {
169    /// Create a cache with `total_budget_bytes` split across [`DEFAULT_SHARDS`]
170    /// shards.
171    pub fn with_budget_bytes(total_budget_bytes: usize) -> Self {
172        Self::with_budget_and_shards(total_budget_bytes, DEFAULT_SHARDS)
173    }
174
175    /// Create a cache with `total_budget_bytes` split across `shard_count` shards.
176    ///
177    /// `shard_count` is rounded UP to the next power of two (min 1) so shard
178    /// selection can mask instead of modulo. Unit tests use `shard_count = 1` for
179    /// deterministic eviction ordering; production uses [`DEFAULT_SHARDS`].
180    pub fn with_budget_and_shards(total_budget_bytes: usize, shard_count: usize) -> Self {
181        let shard_count = shard_count.max(1).next_power_of_two();
182        let budget_per_shard = (total_budget_bytes / shard_count).max(1);
183        let mut shards = Vec::with_capacity(shard_count);
184        for _ in 0..shard_count {
185            shards.push(Mutex::new(Shard::new()));
186        }
187        Self {
188            shards: shards.into_boxed_slice(),
189            mask: shard_count - 1,
190            budget_per_shard,
191            disabled: false,
192            hits: AtomicU64::new(0),
193            misses: AtomicU64::new(0),
194            evictions: AtomicU64::new(0),
195        }
196    }
197
198    /// Create a genuine no-op cache (issue #1568): reads bypass it entirely.
199    ///
200    /// Used when `config.memory.block_cache.enabled == false` so the advertised
201    /// toggle really disables caching instead of being decorative. [`get`](Self::get)
202    /// always returns `None` (no counters touched), [`insert`](Self::insert) returns
203    /// the `Arc` without retaining it, and `resident_bytes()` / `len()` /
204    /// `budget_bytes()` all report `0`. A single dummy shard is allocated only so
205    /// shard indexing stays valid; nothing is ever stored in it.
206    pub fn disabled() -> Self {
207        Self {
208            shards: vec![Mutex::new(Shard::new())].into_boxed_slice(),
209            mask: 0,
210            budget_per_shard: 0,
211            disabled: true,
212            hits: AtomicU64::new(0),
213            misses: AtomicU64::new(0),
214            evictions: AtomicU64::new(0),
215        }
216    }
217
218    /// Poison-tolerant lock: recover the guard if a prior holder panicked, so a
219    /// single panic cannot turn the cache into a panic-for-everyone (design D2).
220    #[inline]
221    fn lock(m: &Mutex<Shard>) -> std::sync::MutexGuard<'_, Shard> {
222        m.lock().unwrap_or_else(|e| e.into_inner())
223    }
224
225    #[inline]
226    fn shard_for(&self, key: &ChunkKey) -> &Mutex<Shard> {
227        let mut h = std::collections::hash_map::DefaultHasher::new();
228        key.hash(&mut h);
229        let idx = (h.finish() as usize) & self.mask;
230        // `idx <= mask < shards.len()`, so this index is always in bounds.
231        &self.shards[idx]
232    }
233
234    /// Look up a resident chunk. On a hit this bumps recency and returns a
235    /// `Bytes::clone` (refcount bump, no chunk-sized allocation). On a miss returns
236    /// `None`.
237    pub fn get(&self, key: &ChunkKey) -> Option<Bytes> {
238        // No-op cache (issue #1568): a disabled cache never holds anything, so
239        // reads bypass it without touching the hit/miss counters.
240        if self.disabled {
241            return None;
242        }
243        let mut guard = Self::lock(self.shard_for(key));
244        match guard.lru.get(key) {
245            Some(v) => {
246                let v = v.clone();
247                drop(guard);
248                self.hits.fetch_add(1, Ordering::Relaxed);
249                Some(v)
250            }
251            None => {
252                drop(guard);
253                self.misses.fetch_add(1, Ordering::Relaxed);
254                None
255            }
256        }
257    }
258
259    /// Insert `data` under `key`, returning the resident [`Bytes`].
260    ///
261    /// The `Vec<u8>` is converted to `Bytes` exactly once here via `Bytes::from`,
262    /// which is ZERO-COPY: it takes ownership of the `Vec`'s existing heap
263    /// allocation rather than allocating a new backing store and copying (issue
264    /// #1940 substrate). The returned handle and any subsequent [`get`](Self::get)
265    /// share that one buffer (refcount-bumped clones). After insertion the owning
266    /// shard evicts LRU entries until it is within its byte budget (never evicting
267    /// the just-inserted entry).
268    pub fn insert(&self, key: ChunkKey, data: Vec<u8>) -> Bytes {
269        let bytes = Bytes::from(data);
270        // No-op cache (issue #1568): return the freshly-produced buffer to the
271        // caller without retaining it, so a disabled cache holds nothing.
272        if self.disabled {
273            return bytes;
274        }
275        let len = bytes.len();
276        let mut guard = Self::lock(self.shard_for(&key));
277
278        // Replacing an existing entry: reclaim its byte weight first.
279        if let Some(old) = guard.lru.put(key, bytes.clone()) {
280            guard.current_bytes = guard.current_bytes.saturating_sub(old.len());
281        }
282        guard.current_bytes = guard.current_bytes.saturating_add(len);
283
284        // Evict LRU entries until within budget. The just-inserted key is now
285        // most-recently-used, so `pop_lru` never targets it while other entries
286        // remain. The `len() > 1` guard keeps the chunk we just produced resident
287        // even if it alone exceeds the budget (documented: single oversized entry).
288        let mut evicted_here: u64 = 0;
289        while guard.current_bytes > self.budget_per_shard && guard.lru.len() > 1 {
290            match guard.lru.pop_lru() {
291                Some((_, evicted)) => {
292                    guard.current_bytes = guard.current_bytes.saturating_sub(evicted.len());
293                    evicted_here += 1;
294                }
295                None => break,
296            }
297        }
298        drop(guard);
299        if evicted_here > 0 {
300            self.evictions.fetch_add(evicted_here, Ordering::Relaxed);
301        }
302        bytes
303    }
304
305    /// Total resident decompressed bytes across all shards.
306    pub fn resident_bytes(&self) -> usize {
307        self.shards
308            .iter()
309            .map(|m| Self::lock(m).current_bytes)
310            .sum()
311    }
312
313    /// Total resident entry count across all shards.
314    pub fn len(&self) -> usize {
315        self.shards.iter().map(|m| Self::lock(m).lru.len()).sum()
316    }
317
318    /// Whether the cache currently holds no entries.
319    pub fn is_empty(&self) -> bool {
320        self.len() == 0
321    }
322
323    /// The configured total byte budget (`budget_per_shard * shard count`).
324    ///
325    /// A [`disabled`](Self::disabled) no-op cache reports `0` (it holds nothing).
326    pub fn budget_bytes(&self) -> usize {
327        self.budget_per_shard * self.shards.len()
328    }
329
330    /// Cumulative cache hits (test/observability instrumentation).
331    pub fn hit_count(&self) -> u64 {
332        self.hits.load(Ordering::Relaxed)
333    }
334
335    /// Cumulative cache misses (test/observability instrumentation).
336    pub fn miss_count(&self) -> u64 {
337        self.misses.load(Ordering::Relaxed)
338    }
339
340    /// Cumulative entries evicted to stay within budget (issue #1571, B5).
341    ///
342    /// Real relaxed-atomic counter; a [`disabled`](Self::disabled) no-op cache
343    /// never evicts and reports `0`.
344    pub fn eviction_count(&self) -> u64 {
345        self.evictions.load(Ordering::Relaxed)
346    }
347}
348
349impl Default for DecompressedChunkCache {
350    fn default() -> Self {
351        Self::with_budget_bytes(DEFAULT_BUDGET_BYTES)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use std::sync::Arc;
359
360    fn chunk(byte: u8, len: usize) -> Vec<u8> {
361        vec![byte; len]
362    }
363
364    /// Task 1.1: eviction order — budget holds exactly 2 equal chunks; access
365    /// A, B, A, then insert C → B is evicted (LRU), A and C survive.
366    #[test]
367    fn eviction_order_lru() {
368        // Single shard for deterministic ordering. Budget = 2 * 100 bytes.
369        let cache = DecompressedChunkCache::with_budget_and_shards(200, 1);
370        let a = ChunkKey::new(1, 0);
371        let b = ChunkKey::new(1, 1);
372        let c = ChunkKey::new(1, 2);
373
374        cache.insert(a, chunk(0xAA, 100));
375        cache.insert(b, chunk(0xBB, 100));
376        // Access A so it becomes more-recently-used than B.
377        assert!(cache.get(&a).is_some());
378        // Insert C: over budget (300 > 200) → evict LRU, which is now B.
379        cache.insert(c, chunk(0xCC, 100));
380
381        assert!(cache.get(&a).is_some(), "A (recently used) must survive");
382        assert!(cache.get(&c).is_some(), "C (just inserted) must survive");
383        assert!(
384            cache.get(&b).is_none(),
385            "B (least recently used) must be evicted"
386        );
387        assert!(cache.resident_bytes() <= 200);
388    }
389
390    /// Task 1.2: byte budget — inserting more distinct chunks than the budget can
391    /// hold keeps resident bytes within budget after every insert.
392    #[test]
393    fn byte_budget_bounded() {
394        let budget = 500usize;
395        let cache = DecompressedChunkCache::with_budget_and_shards(budget, 1);
396        for i in 0..50u64 {
397            cache.insert(ChunkKey::new(7, i), chunk(i as u8, 100));
398            assert!(
399                cache.resident_bytes() <= budget,
400                "resident {} exceeded budget {} after insert {}",
401                cache.resident_bytes(),
402                budget,
403                i
404            );
405        }
406        // At 100 bytes/entry and a 500-byte budget, at most 5 entries survive.
407        assert!(
408            cache.len() <= 5,
409            "entry count must stay bounded (got {})",
410            cache.len()
411        );
412    }
413
414    /// Issue #1571 (B5): the eviction counter reflects the real number of entries
415    /// evicted to stay within budget, and an insert within budget increments it by
416    /// zero (no-fabrication).
417    #[test]
418    fn eviction_count_tracks_real_evictions() {
419        // Budget holds exactly 2 * 100-byte chunks.
420        let cache = DecompressedChunkCache::with_budget_and_shards(200, 1);
421        assert_eq!(cache.eviction_count(), 0, "fresh cache has evicted nothing");
422
423        cache.insert(ChunkKey::new(1, 0), chunk(0x00, 100));
424        cache.insert(ChunkKey::new(1, 1), chunk(0x01, 100));
425        // Still within budget (200 <= 200): no eviction yet.
426        assert_eq!(cache.eviction_count(), 0);
427
428        // Each further distinct insert pushes over budget → exactly one eviction.
429        cache.insert(ChunkKey::new(1, 2), chunk(0x02, 100));
430        assert_eq!(cache.eviction_count(), 1);
431        cache.insert(ChunkKey::new(1, 3), chunk(0x03, 100));
432        assert_eq!(cache.eviction_count(), 2);
433    }
434
435    /// A disabled no-op cache never evicts and reports a real zero.
436    #[test]
437    fn disabled_cache_reports_zero_evictions() {
438        let cache = DecompressedChunkCache::disabled();
439        for i in 0..10u64 {
440            cache.insert(ChunkKey::new(1, i), chunk(i as u8, 4096));
441        }
442        assert_eq!(cache.eviction_count(), 0);
443    }
444
445    /// A single entry larger than the budget is retained (never evict below one
446    /// live entry) — the read path must return the chunk it just produced.
447    #[test]
448    fn single_oversized_entry_retained() {
449        let cache = DecompressedChunkCache::with_budget_and_shards(100, 1);
450        let k = ChunkKey::new(1, 0);
451        cache.insert(k, chunk(0xEE, 4096));
452        assert!(
453            cache.get(&k).is_some(),
454            "oversized entry must remain resident"
455        );
456        assert_eq!(cache.len(), 1);
457    }
458
459    /// Task 1.3: zero-copy hit — insert once, get twice; every handle points at
460    /// the SAME underlying buffer (`Bytes` shares the backing allocation), no
461    /// chunk-sized copy. `Bytes` has no `ptr_eq`, so buffer identity is proven via
462    /// `as_ptr()` (a clone shares the backing store and reports the same data
463    /// pointer; a copy would report a different one).
464    #[test]
465    fn zero_copy_hit_pointer_identity() {
466        let cache = DecompressedChunkCache::with_budget_and_shards(1 << 20, 1);
467        let k = ChunkKey::new(3, 9);
468        let inserted = cache.insert(k, chunk(0x42, 1024));
469
470        let h1 = cache.get(&k).expect("first hit");
471        let h2 = cache.get(&k).expect("second hit");
472
473        // Same allocation: shared backing store, not a value copy.
474        assert_eq!(h1.as_ptr(), inserted.as_ptr());
475        assert_eq!(h2.as_ptr(), inserted.as_ptr());
476        assert_eq!(&*h1, &chunk(0x42, 1024)[..]);
477    }
478
479    /// Task 1.4: concurrency soundness — many threads read overlapping + disjoint
480    /// keys under eviction pressure. Every returned buffer is complete/correct,
481    /// resident bytes stay within budget, no panic.
482    #[test]
483    fn concurrency_soundness() {
484        use std::thread;
485
486        // Small budget vs working set so eviction runs constantly.
487        let cache = Arc::new(DecompressedChunkCache::with_budget_bytes(64 * 1024));
488        let chunk_len = 256usize;
489        let n_keys = 512u64;
490
491        let mut handles = Vec::new();
492        for t in 0..8u64 {
493            let cache = Arc::clone(&cache);
494            handles.push(thread::spawn(move || {
495                for round in 0..2000u64 {
496                    let idx = (t.wrapping_mul(31).wrapping_add(round)) % n_keys;
497                    let key = ChunkKey::new(1, idx);
498                    let expect_byte = idx as u8;
499                    let got = match cache.get(&key) {
500                        Some(v) => v,
501                        None => cache.insert(key, vec![expect_byte; chunk_len]),
502                    };
503                    // Whatever we got must be a complete, correct chunk for `idx`
504                    // (never torn/partial, never another key's bytes).
505                    assert_eq!(got.len(), chunk_len);
506                    assert!(got.iter().all(|&b| b == expect_byte));
507                }
508            }));
509        }
510        for h in handles {
511            h.join().expect("worker thread must not panic");
512        }
513        assert!(
514            cache.resident_bytes() <= cache.budget_bytes(),
515            "resident {} exceeded budget {}",
516            cache.resident_bytes(),
517            cache.budget_bytes()
518        );
519    }
520
521    /// Poison recovery: a panic while holding a shard lock must not wedge the
522    /// cache — subsequent ops recover the guard and continue.
523    #[test]
524    fn poisoned_lock_recovers() {
525        use std::panic::{catch_unwind, AssertUnwindSafe};
526        use std::thread;
527
528        let cache = Arc::new(DecompressedChunkCache::with_budget_and_shards(1 << 20, 1));
529        let k = ChunkKey::new(1, 0);
530        cache.insert(k, chunk(0x55, 16));
531
532        // Poison the single shard's mutex by panicking while holding it.
533        let poison_cache = Arc::clone(&cache);
534        let _ = thread::spawn(move || {
535            let _guard = DecompressedChunkCache::lock(&poison_cache.shards[0]);
536            panic!("intentional poison");
537        })
538        .join();
539
540        // The cache must still serve reads/writes without panicking.
541        let res = catch_unwind(AssertUnwindSafe(|| {
542            let hit = cache.get(&k);
543            cache.insert(ChunkKey::new(1, 1), chunk(0x66, 16));
544            hit.is_some()
545        }));
546        assert_eq!(
547            res.ok(),
548            Some(true),
549            "cache must recover from a poisoned lock"
550        );
551    }
552
553    #[test]
554    fn shard_count_rounds_to_power_of_two() {
555        let cache = DecompressedChunkCache::with_budget_and_shards(1 << 20, 10);
556        assert_eq!(cache.shards.len(), 16);
557        assert_eq!(cache.mask, 15);
558    }
559
560    /// roborev #1567: a size-dependent range read (BIG point-read path) keyed by
561    /// offset alone would return the first-cached range when the same offset is
562    /// later read with a different size. The `aux` discriminant (size) makes the
563    /// key complete, so a same-offset/different-size read MISSES rather than
564    /// aliasing to the wrong bytes.
565    #[test]
566    fn ranged_key_does_not_alias_same_offset_different_size() {
567        let cache = DecompressedChunkCache::with_budget_bytes(1 << 20);
568        let sstable = 0xABCD_u64;
569        let offset = 4096_u64;
570
571        // Cache a 16-byte range at `offset`.
572        let k16 = ChunkKey::with_aux(sstable, offset, 16);
573        let v16 = cache.insert(k16, chunk(0x11, 16));
574
575        // A read at the SAME offset but a DIFFERENT size must not hit `v16`.
576        let k32 = ChunkKey::with_aux(sstable, offset, 32);
577        assert!(
578            cache.get(&k32).is_none(),
579            "same offset with a different size must not alias the cached range"
580        );
581
582        // The original size still hits the original bytes (shared backing store).
583        let again = cache.get(&k16).expect("original ranged key still resident");
584        assert_eq!(
585            v16.as_ptr(),
586            again.as_ptr(),
587            "same (offset,size) key returns the same buffer"
588        );
589
590        // aux == 0 (whole-chunk sites) is a distinct namespace from any sized read.
591        let k0 = ChunkKey::new(sstable, offset);
592        assert!(
593            cache.get(&k0).is_none(),
594            "whole-chunk key (aux=0) must not alias a sized range read"
595        );
596    }
597
598    /// Issue #1568: `disabled()` is a genuine no-op cache. `insert` returns the
599    /// produced buffer without retaining it, `get` always misses, and every
600    /// occupancy/budget/counter accessor reports zero — so a read path wired to
601    /// it truly bypasses caching (used when `block_cache.enabled == false`).
602    #[test]
603    fn disabled_cache_is_a_genuine_no_op() {
604        let cache = DecompressedChunkCache::disabled();
605        assert_eq!(cache.budget_bytes(), 0, "disabled cache has no budget");
606
607        let key = ChunkKey::new(1, 0);
608        let arc = cache.insert(key, chunk(0xEE, 4096));
609        assert_eq!(
610            arc.len(),
611            4096,
612            "insert still hands back the produced buffer"
613        );
614
615        assert!(cache.get(&key).is_none(), "disabled cache never retains");
616        assert_eq!(cache.resident_bytes(), 0);
617        assert_eq!(cache.len(), 0);
618        assert!(cache.is_empty());
619        // Bypass touches no counters: a disabled cache reports a structural zero.
620        assert_eq!(cache.hit_count(), 0);
621        assert_eq!(cache.miss_count(), 0);
622    }
623}