Skip to main content

commonware_runtime/utils/buffer/paged/
cache.rs

1//! A page cache for caching _logical_ pages of [Blob] data in memory. The cache is unaware of the
2//! physical page format used by the blob, which is left to the blob implementation.
3
4use super::{CHECKSUM_SIZE, STORAGE_PAGE_SIZE, get_page_from_blob};
5use crate::{Blob, BufferPool, BufferPooler, Error, IoBuf, IoBufMut, ReadOptions};
6use ahash::AHashMap;
7use commonware_utils::{Widen, cache::Clock, sync::RwLock};
8use futures::{FutureExt, future::Shared};
9use std::{
10    collections::hash_map::Entry,
11    future::Future,
12    num::{NonZeroU16, NonZeroUsize},
13    pin::Pin,
14    sync::{
15        Arc,
16        atomic::{AtomicU64, Ordering},
17    },
18};
19use tracing::{debug, error, trace};
20
21/// Shared future for one logical page fetch. The output uses `Arc<Error>` because `Shared`
22/// requires cloneable results. The `IoBuf` contains only the logical, validated page bytes.
23type PageFetchFuture = Shared<Pin<Box<dyn Future<Output = Result<IoBuf, Arc<Error>>> + Send>>>;
24
25/// Shared handle to one in-flight fetch generation. The cache keeps one copy in `page_fetches`,
26/// and each waiter clones the `Arc` while it is still interested in the result.
27type PageFetch = Arc<PageFetchFuture>;
28
29/// One in-flight fetch generation for a single `(blob_id, page_num)`.
30///
31/// `fetch` is shared by every waiter that joined this generation. `waiters` counts the still
32/// armed waiters whose drop path may need to remove this entry if they become the last
33/// unresolved waiter. If `page_fetches[key]` is later replaced by a newer generation, stale
34/// waiters from the old generation must ignore it and rely on `Arc::ptr_eq` against their saved
35/// `fetch`.
36struct PageFetchEntry {
37    /// Shared page fetch future that reads and validates the logical page exactly once.
38    fetch: PageFetch,
39    /// Count of waiters that still need cancellation cleanup for this fetch generation.
40    waiters: usize,
41}
42
43/// Removes a stale in-flight page fetch when the last unresolved waiter is dropped.
44struct PageFetchGuard {
45    cache: Arc<RwLock<Cache>>,
46    key: (u64, u64),
47    fetch: PageFetch,
48    armed: bool,
49}
50
51impl PageFetchGuard {
52    const fn new(cache: Arc<RwLock<Cache>>, key: (u64, u64), fetch: PageFetch) -> Self {
53        Self {
54            cache,
55            key,
56            fetch,
57            armed: true,
58        }
59    }
60
61    const fn disarm(&mut self) {
62        self.armed = false;
63    }
64}
65
66impl Drop for PageFetchGuard {
67    fn drop(&mut self) {
68        if !self.armed {
69            return;
70        }
71
72        // A resolved fetch removes `page_fetches[key]` before waiters resume and disarm their
73        // guards. If that fetch failed, the page remains uncached, so a new reader can install a
74        // new fetch for the same key before an old waiter is cancelled. Ignore drops from stale
75        // waiters so they cannot decrement or remove a newer generation. A surviving waiter keeps
76        // the current generation installed, which lets the shared future finish and cache the page
77        // on success.
78        let mut cache = self.cache.write();
79        let Entry::Occupied(mut current) = cache.page_fetches.entry(self.key) else {
80            return;
81        };
82        if !Arc::ptr_eq(&current.get().fetch, &self.fetch) {
83            return;
84        }
85        if current.get().waiters == 1 {
86            current.remove();
87        } else {
88            current.get_mut().waiters -= 1;
89        }
90    }
91}
92
93/// A [Cache] caches pages of [Blob] data in memory after verifying the integrity of each.
94///
95/// A single page cache can be used to cache data from multiple blobs by assigning a unique id to
96/// each.
97///
98/// Eviction is delegated to a [Clock], which uses the Clock (second-chance) replacement
99/// policy, a lightweight approximation of LRU. All page buffers are pre-allocated from `pool` at
100/// construction (via [Clock::prefill]) and reused in place, so caching never allocates after
101/// construction.
102///
103/// Reads first resolve pages through `hints`, a fixed-size direct-mapped array from
104/// [Self::hint_index] to the [Clock] slot the page was last cached in: a lookup is one array
105/// load instead of a hash-table probe chain, which the out-of-order core cannot overlap across
106/// items. Hints are best-effort, never truth: [Clock::get_at] only resolves a slot that still
107/// holds the page's key live, so entries staled by eviction, invalidation, or hint collisions
108/// read as misses and fall back to the [Clock]'s own lookup. Hints need no maintenance on
109/// eviction or invalidation, and their memory is fixed at construction, so no blob offset can
110/// grow them.
111struct Cache {
112    /// Maps each (blob id, page number) to its logical page buffer.
113    cache: Clock<(u64, u64), IoBufMut>,
114
115    /// Direct-mapped [Clock] slot hints, indexed by [Self::hint_index]. Initialized
116    /// out-of-range so untouched entries read as misses. The length is a power of two so
117    /// [Self::hint_index] can wrap with a mask instead of a division, and at least twice the
118    /// cache capacity: a full cache has one live page per `capacity`, so sizing at capacity
119    /// makes hint collisions (and their slower fallback lookups) common.
120    hints: Vec<usize>,
121
122    /// Logical size of each page in bytes (the payload stored per page, excluding the CRC
123    /// record appended on disk).
124    page_size: NonZeroU16,
125
126    /// Pool the page buffers were allocated from.
127    pool: BufferPool,
128
129    /// A map of currently executing page fetches to ensure only one task at a time is trying to
130    /// fetch a specific page.
131    page_fetches: AHashMap<(u64, u64), PageFetchEntry>,
132}
133
134/// A reference to a page cache that can be shared across threads via cloning, along with the page
135/// size that will be used with it. Provides the API for interacting with the page cache in a
136/// thread-safe manner.
137#[derive(Clone)]
138pub struct CacheRef {
139    /// The logical size of each page in the underlying blobs managed by this page cache. A page
140    /// occupies `page_size + CHECKSUM_SIZE` bytes on disk (its physical size).
141    ///
142    /// # Warning
143    ///
144    /// You cannot change the page size once data has been written without invalidating it. Reads on
145    /// blobs written with a different page size fail their integrity check, and reopening such a
146    /// blob for writing treats the mismatched pages as invalid trailing data and silently truncates
147    /// the blob (potentially to empty). Changing an existing store's page size is a destructive
148    /// format migration, not a configuration change.
149    page_size: NonZeroU16,
150
151    /// The next id to assign to a blob that will be managed by this cache.
152    next_id: Arc<AtomicU64>,
153
154    /// Shareable reference to the page cache.
155    cache: Arc<RwLock<Cache>>,
156
157    /// Pool used for page-cache and associated buffer allocations.
158    pool: BufferPool,
159}
160
161impl CacheRef {
162    /// Create a shared page-cache handle backed by `pool`.
163    ///
164    /// The cache stores at most `capacity` pages, each exactly `page_size` bytes (see
165    /// [Self::page_size] for how this relates to a page's physical size on disk).
166    /// Initialization eagerly allocates and zeroes all cache slots from `pool`.
167    ///
168    /// Any `page_size` is accepted, but physical pages that do not align with storage pages (see
169    /// the module docs) amplify cold random reads. Use [super::page_size] to pick an aligned value.
170    /// Cache misses request [ReadOptions::DONT_CACHE] because the fetched page is retained here.
171    pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
172        let page_size_u64: u64 = page_size.widen();
173        let physical_page_size = page_size_u64 + CHECKSUM_SIZE;
174        if !physical_page_size.is_multiple_of(STORAGE_PAGE_SIZE)
175            && !STORAGE_PAGE_SIZE.is_multiple_of(physical_page_size)
176        {
177            debug!(
178                page_size = page_size.get(),
179                physical_page_size, "physical pages do not align with storage pages"
180            );
181        }
182
183        Self {
184            page_size,
185            next_id: Arc::new(AtomicU64::new(0)),
186            cache: Arc::new(RwLock::new(Cache::new(pool.clone(), page_size, capacity))),
187            pool,
188        }
189    }
190
191    /// Create a shared page-cache handle, extracting the storage [BufferPool] from a
192    /// [BufferPooler]. Cache misses request [ReadOptions::DONT_CACHE].
193    pub fn from_pooler(
194        pooler: &impl BufferPooler,
195        page_size: NonZeroU16,
196        capacity: NonZeroUsize,
197    ) -> Self {
198        Self::new(pooler.storage_buffer_pool().clone(), page_size, capacity)
199    }
200
201    /// The page size used by this page cache: the logical payload bytes stored per page. Each
202    /// page occupies `page_size() + CHECKSUM_SIZE` bytes on disk (its physical size).
203    #[inline]
204    pub const fn page_size(&self) -> NonZeroU16 {
205        self.page_size
206    }
207
208    /// Returns the storage buffer pool associated with this cache.
209    #[inline]
210    pub const fn pool(&self) -> &BufferPool {
211        &self.pool
212    }
213
214    /// Returns a unique id for the next blob that will use this page cache.
215    pub fn next_id(&self) -> u64 {
216        self.next_id.fetch_add(1, Ordering::Relaxed)
217    }
218
219    /// Convert a logical offset into the number of the page it belongs to and the offset within
220    /// that page.
221    pub fn offset_to_page(&self, offset: u64) -> (u64, u64) {
222        Cache::offset_to_page(self.page_size, offset)
223    }
224
225    /// Try to read the specified bytes from the page cache only. Returns the number of bytes
226    /// successfully read from cache and copied to `buf` before a page fault, if any.
227    pub(super) fn read_cached(
228        &self,
229        blob_id: u64,
230        mut buf: &mut [u8],
231        mut logical_offset: u64,
232    ) -> usize {
233        let original_len = buf.len();
234        let page_cache = self.cache.read();
235        while !buf.is_empty() {
236            let count = page_cache.read_at(blob_id, buf, logical_offset);
237            if count == 0 {
238                // Cache miss - return how many bytes we successfully read
239                break;
240            }
241            logical_offset += count as u64;
242            buf = &mut buf[count..];
243        }
244        original_len - buf.len()
245    }
246
247    /// Read multiple disjoint byte ranges from the page cache in a single lock acquisition.
248    ///
249    /// Each element of `ranges` is `(dest_slice, logical_offset)`. Fully-cached ranges have
250    /// their data written to the destination slice and are removed from `ranges`. Entries left
251    /// in `ranges` correspond to cache misses that the caller must read from the underlying
252    /// blob.
253    pub(super) fn read_cached_many(&self, blob_id: u64, ranges: &mut Vec<(&mut [u8], u64)>) {
254        let page_cache = self.cache.read();
255        let page_size = page_cache.page_size;
256
257        // Resolve every range's first page before copying any data. The lookups are
258        // independent, so batching them lets the core overlap their memory latency instead of
259        // stalling each lookup behind the previous range's copy.
260        let mut srcs: Vec<Option<&[u8]>> = Vec::with_capacity(ranges.len());
261        for (buf, offset) in ranges.iter() {
262            let (page_num, offset_in_page, remaining) = Cache::locate(page_size, *offset);
263            let seg = std::cmp::min(buf.len(), remaining);
264            srcs.push(
265                page_cache
266                    .get_page(blob_id, page_num)
267                    .map(|page| &page.as_ref()[offset_in_page..offset_in_page + seg]),
268            );
269        }
270
271        // Copy resolved pages, dropping fully-cached ranges and keeping misses. A range whose
272        // first page missed is kept untouched, and one that continues past its first page reads
273        // the rest page by page, staying a miss if any later page faults.
274        let mut next = 0;
275        ranges.retain_mut(|(buf, offset)| {
276            let src = srcs[next];
277            next += 1;
278            if buf.is_empty() {
279                return false;
280            }
281            let Some(src) = src else {
282                return true;
283            };
284            buf[..src.len()].copy_from_slice(src);
285            let mut done = src.len();
286            while done < buf.len() {
287                let count = page_cache.read_at(blob_id, &mut buf[done..], *offset + done as u64);
288                if count == 0 {
289                    return true;
290                }
291                done += count;
292            }
293            false
294        });
295    }
296
297    /// Read the specified bytes, preferentially from the page cache. Bytes not found in the cache
298    /// will be read from the provided `blob` and cached for future reads.
299    pub(super) async fn read<B: Blob>(
300        &self,
301        blob: &B,
302        blob_id: u64,
303        mut buf: &mut [u8],
304        mut offset: u64,
305    ) -> Result<(), Error> {
306        // Read up to a page worth of data at a time from either the page cache or the `blob`,
307        // until the requested data is fully read.
308        while !buf.is_empty() {
309            // Read lock the page cache and see if we can get (some of) the data from it.
310            {
311                let page_cache = self.cache.read();
312                let count = page_cache.read_at(blob_id, buf, offset);
313                if count != 0 {
314                    offset += count as u64;
315                    buf = &mut buf[count..];
316                    continue;
317                }
318            }
319
320            // Handle page fault.
321            let count = self
322                .read_after_page_fault(blob, blob_id, buf, offset)
323                .await?;
324            offset += count as u64;
325            buf = &mut buf[count..];
326        }
327
328        Ok(())
329    }
330
331    /// Fetch the requested page after encountering a page fault, which may involve retrieving it
332    /// from `blob` & caching the result in the page cache. Returns the number of bytes read, which
333    /// should always be non-zero.
334    pub(super) async fn read_after_page_fault<B: Blob>(
335        &self,
336        blob: &B,
337        blob_id: u64,
338        buf: &mut [u8],
339        offset: u64,
340    ) -> Result<usize, Error> {
341        assert!(!buf.is_empty());
342
343        let (page_num, offset_in_page, _) = Cache::locate(self.page_size, offset);
344        trace!(page_num, blob_id, "page fault");
345
346        // Create or clone a future that retrieves the desired page from the underlying blob. This
347        // requires a write lock on the page cache since we may need to modify `page_fetches` if
348        // this task is the first fetcher.
349        let (fetch_future, mut fetch_guard) = {
350            let mut cache = self.cache.write();
351
352            // There's a (small) chance the page was fetched & buffered by another task before we
353            // were able to acquire the write lock, so check the cache before doing anything else.
354            let count = cache.read_at(blob_id, buf, offset);
355            if count != 0 {
356                return Ok(count);
357            }
358
359            let key = (blob_id, page_num);
360            match cache.page_fetches.entry(key) {
361                Entry::Occupied(o) => {
362                    // Another thread is already fetching this page, so clone its existing future.
363                    let entry = o.into_mut();
364                    entry.waiters += 1;
365                    let fetch_future = entry.fetch.as_ref().clone();
366                    let fetch = Arc::clone(&entry.fetch);
367                    (
368                        fetch_future,
369                        PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
370                    )
371                }
372                Entry::Vacant(v) => {
373                    // Nobody is currently fetching this page, so create a future that will do the
374                    // work. get_page_from_blob handles CRC validation and returns only logical bytes.
375                    let blob = blob.clone();
376                    let cache = Arc::clone(&self.cache);
377                    let page_size = self.page_size;
378                    let future = async move {
379                        let result = fetch_cacheable_page(&blob, page_num, page_size).await;
380                        if let Err(err) = &result {
381                            error!(page_num, ?err, "Page fetch failed");
382                        }
383
384                        // This shared future still owns `page_fetches[key]`. As long as at least
385                        // one waiter remains armed, that entry pins this generation in place, so a
386                        // replacement fetch for the same page cannot be inserted before we cache
387                        // the successful result below. Only when every waiter cancels can the last
388                        // guard remove the entry and let a later reader start a new generation.
389                        let mut cache = cache.write();
390                        if let Ok(page) = &result {
391                            cache.cache(blob_id, page.as_ref(), page_num);
392                        }
393                        let _ = cache.page_fetches.remove(&key);
394                        result
395                    };
396
397                    // Make the future shareable and insert it into the map.
398                    let fetch_future = future.boxed().shared();
399                    let fetch = Arc::new(fetch_future.clone());
400                    v.insert(PageFetchEntry {
401                        fetch: Arc::clone(&fetch),
402                        waiters: 1,
403                    });
404
405                    (
406                        fetch_future,
407                        PageFetchGuard::new(Arc::clone(&self.cache), key, fetch),
408                    )
409                }
410            }
411        };
412
413        // Await the shared fetch. The future itself logs failures, caches the resolved page, and
414        // removes the in-flight marker before it returns, so waiters only need cancellation
415        // cleanup while the fetch is still unresolved.
416        let fetch_result = fetch_future.await;
417        fetch_guard.disarm();
418        let page_buf = match fetch_result {
419            Ok(page_buf) => page_buf,
420            Err(err) => return Err(err.as_ref().clone()),
421        };
422
423        // Copy the requested portion of the page into the buffer.
424        let bytes_to_copy = std::cmp::min(buf.len(), page_buf.len() - offset_in_page);
425        buf[..bytes_to_copy]
426            .copy_from_slice(&page_buf.as_ref()[offset_in_page..offset_in_page + bytes_to_copy]);
427
428        Ok(bytes_to_copy)
429    }
430
431    /// Cache the provided pages of data in the page cache, returning the remaining bytes that
432    /// didn't fill a whole page. `offset` must be page aligned.
433    ///
434    /// # Panics
435    ///
436    /// - Panics if `offset` is not page aligned.
437    /// - If the buffer is not the size of a page.
438    pub fn cache(&self, blob_id: u64, mut buf: &[u8], offset: u64) -> usize {
439        let (mut page_num, offset_in_page) = self.offset_to_page(offset);
440        assert_eq!(offset_in_page, 0);
441        {
442            // Write lock the page cache.
443            let page_size: usize = self.page_size.widen();
444            let mut page_cache = self.cache.write();
445            while buf.len() >= page_size {
446                page_cache.cache(blob_id, &buf[..page_size], page_num);
447                buf = &buf[page_size..];
448                page_num = match page_num.checked_add(1) {
449                    Some(next) => next,
450                    None => break,
451                };
452            }
453        }
454
455        buf.len()
456    }
457
458    /// Drop all cached pages while retaining the backing page buffers for reuse.
459    ///
460    /// Call only when no reads are in flight for this cache.
461    #[cfg(any(test, feature = "test-utils"))]
462    pub fn clear(&self) {
463        self.cache.write().clear();
464    }
465
466    /// Drop any cached pages for `blob_id` at `page_num >= start_page`. Used after a blob is
467    /// truncated so subsequent reads can't observe pre-truncation bytes in a page that the tip
468    /// buffer (or future writes) now owns.
469    pub(super) fn invalidate_from(&self, blob_id: u64, start_page: u64) {
470        self.cache.write().invalidate_from(blob_id, start_page);
471    }
472}
473
474impl Cache {
475    /// Return a new empty page cache with a max cache capacity of `capacity` pages, each of size
476    /// `page_size` bytes.
477    pub fn new(pool: BufferPool, page_size: NonZeroU16, capacity: NonZeroUsize) -> Self {
478        let slot_size: usize = page_size.widen();
479        let mut cache = Clock::new(capacity);
480        cache.prefill(|| pool.alloc_zeroed(slot_size));
481        let hints = capacity.get().saturating_mul(2).next_power_of_two();
482        Self {
483            cache,
484            hints: vec![usize::MAX; hints],
485            page_size,
486            pool,
487            page_fetches: AHashMap::new(),
488        }
489    }
490
491    /// Convert a logical offset into the number of the page it belongs to and the offset within
492    /// that page.
493    fn offset_to_page(page_size: NonZeroU16, offset: u64) -> (u64, u64) {
494        let page_size: u64 = page_size.widen();
495        (offset / page_size, offset % page_size)
496    }
497
498    /// Locate `offset` within its page: the page number, the offset inside that page, and the
499    /// bytes remaining in the page at that offset.
500    fn locate(page_size: NonZeroU16, offset: u64) -> (u64, usize, usize) {
501        let (page_num, offset_in_page) = Self::offset_to_page(page_size, offset);
502        let offset_in_page = offset_in_page as usize;
503        let width: usize = page_size.widen();
504        let remaining = width - offset_in_page;
505        (page_num, offset_in_page, remaining)
506    }
507
508    /// Attempt to fetch blob data starting at `offset` from the page cache. Returns the number of
509    /// bytes read, which could be 0 if the first page in the requested range isn't buffered, and is
510    /// never more than `self.page_size` or the length of `buf`. The returned bytes won't cross a
511    /// page boundary, so multiple reads may be required even if all data in the desired range is
512    /// buffered.
513    fn read_at(&self, blob_id: u64, buf: &mut [u8], logical_offset: u64) -> usize {
514        let (page_num, offset_in_page, remaining) = Self::locate(self.page_size, logical_offset);
515        let Some(page) = self.get_page(blob_id, page_num) else {
516            return 0;
517        };
518        let page = page.as_ref();
519
520        let bytes_to_copy = std::cmp::min(buf.len(), remaining);
521        buf[..bytes_to_copy].copy_from_slice(&page[offset_in_page..offset_in_page + bytes_to_copy]);
522
523        bytes_to_copy
524    }
525
526    /// Put the given `page` into the page cache and record its slot hint.
527    fn cache(&mut self, blob_id: u64, page: &[u8], page_num: u64) {
528        let page_size: usize = self.page_size.widen();
529        assert_eq!(page.len(), page_size);
530        let pool = &self.pool;
531        let (slot, buf) = self
532            .cache
533            .get_or_insert_mut((blob_id, page_num), || pool.alloc_zeroed(page_size));
534        buf.as_mut().copy_from_slice(page);
535        let hint = self.hint_index(blob_id, page_num);
536        self.hints[hint] = slot;
537    }
538
539    /// The hint slot for `(blob_id, page_num)`: the page number offset by a per-blob salt,
540    /// wrapped to the array.
541    ///
542    /// Adding (rather than hashing in) the page number keeps consecutive pages in consecutive
543    /// hint entries, so the sorted batches issued by [CacheRef::read_cached_many] walk the
544    /// array sequentially instead of taking a cache miss per lookup. The salt spreads blobs'
545    /// ranges apart; two blobs whose ranges still overlap only evict each other's hints, which
546    /// [Self::get_page] repairs through the fallback lookup.
547    #[inline]
548    const fn hint_index(&self, blob_id: u64, page_num: u64) -> usize {
549        let salted = page_num.wrapping_add(blob_id.wrapping_mul(commonware_utils::GOLDEN_RATIO));
550        (salted & (self.hints.len() as u64 - 1)) as usize
551    }
552
553    /// Look up a page, preferring its direct-mapped slot hint over the [Clock]'s own lookup.
554    #[inline]
555    fn get_page(&self, blob_id: u64, page_num: u64) -> Option<&IoBufMut> {
556        let key = (blob_id, page_num);
557        let slot = self.hints[self.hint_index(blob_id, page_num)];
558        if let Some(page) = self.cache.get_at(slot, &key) {
559            return Some(page);
560        }
561        self.cache.get(&key)
562    }
563
564    /// Drop any cached pages for `blob_id` at `page_num >= start_page`.
565    fn invalidate_from(&mut self, blob_id: u64, start_page: u64) {
566        self.cache
567            .retain(|&(bid, page_num), _| bid != blob_id || page_num < start_page);
568    }
569
570    /// Drop all cached pages while retaining backing page buffers for reuse.
571    #[cfg(any(test, feature = "test-utils"))]
572    fn clear(&mut self) {
573        self.cache.retain(|_, _| false);
574        self.page_fetches.clear();
575    }
576}
577
578/// Fetch one logical page for insertion into the page cache, rejecting partial pages because cache
579/// entries must always contain a full logical page.
580async fn fetch_cacheable_page(
581    blob: &impl Blob,
582    page_num: u64,
583    page_size: NonZeroU16,
584) -> Result<IoBuf, Arc<Error>> {
585    // CacheRef retains the page, so the source page need not remain in the OS page cache.
586    let width: u64 = page_size.widen();
587    let page = get_page_from_blob(blob, page_num, width, ReadOptions::DONT_CACHE)
588        .await
589        .map_err(Arc::new)?;
590
591    // We should never be fetching partial pages through the page cache. This can happen if a
592    // non-last page is corrupted and falls back to a partial CRC.
593    let len = page.len();
594    let expected: usize = page_size.widen();
595    if len != expected {
596        error!(
597            page_num,
598            expected = page_size,
599            actual = len,
600            "attempted to fetch partial page from blob"
601        );
602        return Err(Arc::new(Error::InvalidChecksum));
603    }
604
605    Ok(page)
606}
607
608#[cfg(test)]
609mod tests {
610    use super::{super::Checksum, *};
611    use crate::{
612        BufferPool, BufferPoolConfig, Clock as _, Handle, IoBufMut, IoBufs, IoBufsMut, Runner as _,
613        Spawner as _, Storage as _, Supervisor as _, WriteOptions, buffer::paged::CHECKSUM_SIZE,
614        deterministic, telemetry::metrics::Registry,
615    };
616    use commonware_cryptography::Crc32;
617    use commonware_macros::test_traced;
618    use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
619    use futures::future::pending;
620    use rstest::rstest;
621    use std::{
622        num::NonZeroU16,
623        sync::{
624            Arc,
625            atomic::{AtomicUsize, Ordering},
626        },
627        time::Duration,
628    };
629
630    fn test_pool() -> BufferPool {
631        let mut registry = Registry::default();
632        BufferPool::new(BufferPoolConfig::for_storage(), &mut registry)
633    }
634
635    // Logical page size (what CacheRef uses and what gets cached).
636    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
637    const PAGE_SIZE_U64: u64 = PAGE_SIZE.get() as u64;
638
639    fn expected_cached_bytes(logical_offset: u64, len: usize) -> Vec<u8> {
640        (0..len)
641            .map(|i| {
642                let page = (logical_offset + i as u64) / PAGE_SIZE_U64;
643                page as u8 + 1
644            })
645            .collect()
646    }
647
648    /// A blob that signals once a read starts and then never returns.
649    #[derive(Clone)]
650    struct BlockingBlob {
651        started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
652    }
653
654    impl Blob for BlockingBlob {
655        async fn read_at(
656            &self,
657            offset: u64,
658            len: usize,
659            options: ReadOptions,
660        ) -> Result<IoBufsMut, Error> {
661            self.read_at_buf(offset, len, IoBufMut::with_capacity(len), options)
662                .await
663        }
664
665        async fn read_at_buf(
666            &self,
667            _offset: u64,
668            _len: usize,
669            _bufs: impl Into<IoBufsMut> + Send,
670            _options: ReadOptions,
671        ) -> Result<IoBufsMut, Error> {
672            let sender = self
673                .started
674                .lock()
675                .take()
676                .expect("blocking blob read started more than once");
677            let _ = sender.send(());
678            pending::<()>().await;
679            unreachable!()
680        }
681
682        async fn write_at(
683            &self,
684            _offset: u64,
685            _bufs: impl Into<crate::IoBufs> + Send,
686            _options: WriteOptions,
687        ) -> Result<(), Error> {
688            Ok(())
689        }
690
691        async fn resize(&self, _len: u64) -> Result<(), Error> {
692            Ok(())
693        }
694
695        async fn sync(&self) -> Result<(), Error> {
696            Ok(())
697        }
698
699        async fn start_sync(&self) -> Handle<()> {
700            Handle::ready(self.sync().await)
701        }
702    }
703
704    #[derive(Clone)]
705    enum ControlledBlobResult {
706        Success(Arc<Vec<u8>>),
707        Error,
708    }
709
710    /// A blob that blocks its first physical page read until released and counts total reads.
711    #[derive(Clone)]
712    struct ControlledBlob {
713        started: Arc<Mutex<Option<oneshot::Sender<()>>>>,
714        release: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
715        reads: Arc<AtomicUsize>,
716        result: ControlledBlobResult,
717    }
718
719    impl Blob for ControlledBlob {
720        async fn read_at(
721            &self,
722            offset: u64,
723            len: usize,
724            options: ReadOptions,
725        ) -> Result<IoBufsMut, Error> {
726            self.read_at_buf(offset, len, IoBufMut::with_capacity(len), options)
727                .await
728        }
729
730        async fn read_at_buf(
731            &self,
732            _offset: u64,
733            _len: usize,
734            _bufs: impl Into<IoBufsMut> + Send,
735            _options: ReadOptions,
736        ) -> Result<IoBufsMut, Error> {
737            if self.reads.fetch_add(1, Ordering::Relaxed) == 0 {
738                let sender = self
739                    .started
740                    .lock()
741                    .take()
742                    .expect("controlled blob start signal consumed more than once");
743                let _ = sender.send(());
744
745                let release = self
746                    .release
747                    .lock()
748                    .take()
749                    .expect("controlled blob release receiver consumed more than once");
750                release.await.expect("release signal dropped");
751            }
752
753            match &self.result {
754                ControlledBlobResult::Success(page) => Ok(IoBufsMut::from(page.as_ref().clone())),
755                ControlledBlobResult::Error => Err(Error::ReadFailed),
756            }
757        }
758
759        async fn write_at(
760            &self,
761            _offset: u64,
762            _bufs: impl Into<crate::IoBufs> + Send,
763            _options: WriteOptions,
764        ) -> Result<(), Error> {
765            Ok(())
766        }
767
768        async fn resize(&self, _len: u64) -> Result<(), Error> {
769            Ok(())
770        }
771
772        async fn sync(&self) -> Result<(), Error> {
773            Ok(())
774        }
775
776        async fn start_sync(&self) -> Handle<()> {
777            Handle::ready(self.sync().await)
778        }
779    }
780
781    #[test_traced]
782    fn test_cache_basic() {
783        let pool = test_pool();
784        let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(10));
785
786        // Cache stores logical-sized pages.
787        let mut buf = vec![0; PAGE_SIZE.get() as usize];
788        let bytes_read = cache.read_at(0, &mut buf, 0);
789        assert_eq!(bytes_read, 0);
790
791        cache.cache(0, &[1; PAGE_SIZE.get() as usize], 0);
792        let bytes_read = cache.read_at(0, &mut buf, 0);
793        assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
794        assert_eq!(buf, [1; PAGE_SIZE.get() as usize]);
795
796        // Test replacement -- re-caching the same page overwrites it in place.
797        cache.cache(0, &[2; PAGE_SIZE.get() as usize], 0);
798        let bytes_read = cache.read_at(0, &mut buf, 0);
799        assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
800        assert_eq!(buf, [2; PAGE_SIZE.get() as usize]);
801
802        // Test exceeding the cache capacity.
803        for i in 0u64..11 {
804            cache.cache(0, &[i as u8; PAGE_SIZE.get() as usize], i);
805        }
806        // Page 0 should have been evicted.
807        let bytes_read = cache.read_at(0, &mut buf, 0);
808        assert_eq!(bytes_read, 0);
809        // Page 1-10 should be in the cache.
810        for i in 1u64..11 {
811            let bytes_read = cache.read_at(0, &mut buf, i * PAGE_SIZE_U64);
812            assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
813            assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
814        }
815
816        // Test reading from an unaligned offset by adding 2 to an aligned offset. The read
817        // should be 2 bytes short of a full logical page.
818        let mut buf = vec![0; PAGE_SIZE.get() as usize];
819        let bytes_read = cache.read_at(0, &mut buf, PAGE_SIZE_U64 + 2);
820        assert_eq!(bytes_read, PAGE_SIZE.get() as usize - 2);
821        assert_eq!(
822            &buf[..PAGE_SIZE.get() as usize - 2],
823            [1; PAGE_SIZE.get() as usize - 2]
824        );
825    }
826
827    #[test_traced]
828    fn test_invalidate_from_does_not_orphan_re_cached_page() {
829        // Invalidating pages, re-caching one, then forcing an eviction must keep every live page
830        // readable. Freed slots are reused cleanly, so an invalidated-then-re-cached page is never
831        // orphaned by a later eviction.
832        let mut registry = Registry::default();
833        let pool = BufferPool::new(BufferPoolConfig::for_storage(), &mut registry);
834        let mut cache: Cache = Cache::new(pool, PAGE_SIZE, NZUsize!(2));
835        let blob_id = 0u64;
836        let page_size = PAGE_SIZE.get() as usize;
837
838        // Fill both slots, then invalidate them so both slots are freed for reuse.
839        cache.cache(blob_id, &vec![0xAA; page_size], 0);
840        cache.cache(blob_id, &vec![0xBB; page_size], 1);
841        cache.invalidate_from(blob_id, 0);
842
843        // Re-cache page 1 into a reused slot.
844        cache.cache(blob_id, &vec![0xCC; page_size], 1);
845        let mut buf = vec![0u8; page_size];
846        assert_eq!(
847            cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
848            page_size,
849            "page 1 should be readable after re-cache"
850        );
851        assert_eq!(buf, vec![0xCC; page_size]);
852
853        // Cache a new page, which reuses the other freed slot rather than evicting live page 1.
854        cache.cache(blob_id, &vec![0xDD; page_size], 2);
855
856        // Slot 0 must still be reachable via its live index entry.
857        let mut buf = vec![0u8; page_size];
858        assert_eq!(
859            cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64),
860            page_size,
861            "live page 1 was orphaned by stale-slot eviction"
862        );
863        assert_eq!(buf, vec![0xCC; page_size]);
864
865        // And the newly cached page 2 is also reachable.
866        let mut buf = vec![0u8; page_size];
867        assert_eq!(
868            cache.read_at(blob_id, &mut buf, PAGE_SIZE_U64 * 2),
869            page_size
870        );
871        assert_eq!(buf, vec![0xDD; page_size]);
872    }
873
874    #[test_traced]
875    fn test_cache_read_with_blob() {
876        // Initialize the deterministic context
877        let executor = deterministic::Runner::default();
878        // Start the test within the executor
879        executor.start(|context| async move {
880            // Physical page size = logical + CRC record.
881            let physical_page_size = PAGE_SIZE_U64 + CHECKSUM_SIZE;
882
883            // Populate a blob with 11 consecutive pages of CRC-protected data.
884            let (blob, size) = context
885                .open("test", "blob".as_bytes())
886                .await
887                .expect("Failed to open blob");
888            assert_eq!(size, 0);
889            for i in 0..11 {
890                // Write logical data followed by Checksum.
891                let logical_data = vec![i as u8; PAGE_SIZE.get() as usize];
892                let crc = Crc32::checksum(&logical_data);
893                let record = Checksum::new(PAGE_SIZE.get(), crc);
894                let mut page_data = logical_data;
895                page_data.extend_from_slice(&record.to_bytes());
896                blob.write_at(i * physical_page_size, page_data, WriteOptions::default())
897                    .await
898                    .unwrap();
899            }
900
901            // Fill the page cache with the blob's data via CacheRef::read.
902            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
903            assert_eq!(cache_ref.next_id(), 0);
904            assert_eq!(cache_ref.next_id(), 1);
905            for i in 0..11 {
906                // Read expects logical bytes only (CRCs are stripped).
907                let mut buf = vec![0; PAGE_SIZE.get() as usize];
908                cache_ref
909                    .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
910                    .await
911                    .unwrap();
912                assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
913            }
914
915            // Repeat the read to exercise reading from the page cache. Must start at 1 because
916            // page 0 should be evicted.
917            for i in 1..11 {
918                let mut buf = vec![0; PAGE_SIZE.get() as usize];
919                cache_ref
920                    .read(&blob, 0, &mut buf, i * PAGE_SIZE_U64)
921                    .await
922                    .unwrap();
923                assert_eq!(buf, [i as u8; PAGE_SIZE.get() as usize]);
924            }
925
926            // Cleanup.
927            blob.sync().await.unwrap();
928        });
929    }
930
931    #[test_traced]
932    fn test_cache_clear_forces_uncached_blob_read() {
933        #[derive(Clone)]
934        struct CountingBlob {
935            reads: Arc<AtomicUsize>,
936            read_options: Arc<Mutex<Vec<ReadOptions>>>,
937            page: Arc<Vec<u8>>,
938        }
939
940        impl Blob for CountingBlob {
941            async fn read_at(
942                &self,
943                offset: u64,
944                len: usize,
945                options: ReadOptions,
946            ) -> Result<IoBufsMut, Error> {
947                self.read_at_buf(offset, len, IoBufsMut::default(), options)
948                    .await
949            }
950
951            async fn read_at_buf(
952                &self,
953                _offset: u64,
954                _len: usize,
955                _bufs: impl Into<IoBufsMut> + Send,
956                options: ReadOptions,
957            ) -> Result<IoBufsMut, Error> {
958                self.reads.fetch_add(1, Ordering::Relaxed);
959                self.read_options.lock().push(options);
960                Ok(IoBufsMut::from(self.page.as_ref().clone()))
961            }
962
963            async fn write_at(
964                &self,
965                _offset: u64,
966                _bufs: impl Into<IoBufs> + Send,
967                _options: WriteOptions,
968            ) -> Result<(), Error> {
969                Ok(())
970            }
971
972            async fn resize(&self, _len: u64) -> Result<(), Error> {
973                Ok(())
974            }
975
976            async fn sync(&self) -> Result<(), Error> {
977                Ok(())
978            }
979
980            async fn start_sync(&self) -> Handle<()> {
981                Handle::ready(self.sync().await)
982            }
983        }
984
985        let executor = deterministic::Runner::default();
986        executor.start(|context| async move {
987            let page = vec![7u8; PAGE_SIZE.get() as usize];
988            let crc = Crc32::checksum(&page);
989            let record = Checksum::new(PAGE_SIZE.get(), crc);
990            let mut physical_page = page.clone();
991            physical_page.extend_from_slice(&record.to_bytes());
992            let physical_page = Arc::new(physical_page);
993            let blob = CountingBlob {
994                reads: Arc::new(AtomicUsize::new(0)),
995                read_options: Arc::new(Mutex::new(Vec::new())),
996                page: physical_page,
997            };
998            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
999
1000            let mut buf = vec![0u8; page.len()];
1001            cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1002            assert_eq!(buf, page);
1003            assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
1004
1005            let mut buf = vec![0u8; page.len()];
1006            cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1007            assert_eq!(buf, page);
1008            assert_eq!(blob.reads.load(Ordering::Relaxed), 1);
1009
1010            cache_ref.clear();
1011
1012            let mut buf = vec![0u8; page.len()];
1013            cache_ref.read(&blob, 0, &mut buf, 0).await.unwrap();
1014            assert_eq!(buf, page);
1015            assert_eq!(blob.reads.load(Ordering::Relaxed), 2);
1016            assert_eq!(
1017                *blob.read_options.lock(),
1018                vec![ReadOptions::DONT_CACHE, ReadOptions::DONT_CACHE]
1019            );
1020        });
1021    }
1022
1023    #[test_traced]
1024    fn test_cache_max_page() {
1025        let executor = deterministic::Runner::default();
1026        executor.start(|context| async move {
1027            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(2));
1028
1029            // Use the largest page-aligned offset representable for the configured PAGE_SIZE.
1030            let aligned_max_offset = u64::MAX - (u64::MAX % PAGE_SIZE_U64);
1031
1032            // CacheRef::cache expects only logical bytes (no CRC).
1033            let logical_data = vec![42u8; PAGE_SIZE.get() as usize];
1034
1035            // Caching exactly one page at the maximum offset should succeed.
1036            let remaining = cache_ref.cache(0, logical_data.as_slice(), aligned_max_offset);
1037            assert_eq!(remaining, 0);
1038
1039            // Reading from the cache should return the logical bytes.
1040            let mut buf = vec![0u8; PAGE_SIZE.get() as usize];
1041            let page_cache = cache_ref.cache.read();
1042            let bytes_read = page_cache.read_at(0, &mut buf, aligned_max_offset);
1043            assert_eq!(bytes_read, PAGE_SIZE.get() as usize);
1044            assert!(buf.iter().all(|b| *b == 42));
1045        });
1046    }
1047
1048    #[test_traced]
1049    fn test_cache_at_high_offset() {
1050        let executor = deterministic::Runner::default();
1051        executor.start(|context| async move {
1052            // Use the minimum page size (CHECKSUM_SIZE + 1 = 13) with high offset.
1053            const MIN_PAGE_SIZE: u64 = CHECKSUM_SIZE + 1;
1054            let cache_ref =
1055                CacheRef::from_pooler(&context, NZU16!(MIN_PAGE_SIZE as u16), NZUsize!(2));
1056
1057            // Create two pages worth of logical data (no CRCs - CacheRef::cache expects logical
1058            // only).
1059            let data = vec![1u8; MIN_PAGE_SIZE as usize * 2];
1060
1061            // Cache pages at a high (but not max) aligned offset so we can verify both pages.
1062            // Use an offset that's a few pages below max to avoid overflow when verifying.
1063            let aligned_max_offset = u64::MAX - (u64::MAX % MIN_PAGE_SIZE);
1064            let high_offset = aligned_max_offset - (MIN_PAGE_SIZE * 2);
1065            let remaining = cache_ref.cache(0, &data, high_offset);
1066            // Both pages should be cached.
1067            assert_eq!(remaining, 0);
1068
1069            // Verify the first page was cached correctly.
1070            let mut buf = vec![0u8; MIN_PAGE_SIZE as usize];
1071            let page_cache = cache_ref.cache.read();
1072            assert_eq!(
1073                page_cache.read_at(0, &mut buf, high_offset),
1074                MIN_PAGE_SIZE as usize
1075            );
1076            assert!(buf.iter().all(|b| *b == 1));
1077
1078            // Verify the second page was cached correctly.
1079            assert_eq!(
1080                page_cache.read_at(0, &mut buf, high_offset + MIN_PAGE_SIZE),
1081                MIN_PAGE_SIZE as usize
1082            );
1083            assert!(buf.iter().all(|b| *b == 1));
1084        });
1085    }
1086
1087    #[test_traced]
1088    fn test_page_fetches_entry_removed_when_first_fetcher_cancelled() {
1089        let executor = deterministic::Runner::default();
1090        executor.start(|context| async move {
1091            // Set up a small cache and a blob whose read never completes once started.
1092            let blob_id = 0;
1093            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1094            let (started_tx, started_rx) = oneshot::channel();
1095            let blob = BlockingBlob {
1096                started: Arc::new(Mutex::new(Some(started_tx))),
1097            };
1098            let mut read_buf = vec![0u8; PAGE_SIZE.get() as usize];
1099
1100            // Spawn the first fetcher. It will insert into `page_fetches` and then block forever.
1101            let cache_ref_for_task = cache_ref.clone();
1102            let blob_for_task = blob.clone();
1103            let handle = context.spawn(move |_| async move {
1104                let _ = cache_ref_for_task
1105                    .read(&blob_for_task, blob_id, &mut read_buf, 0)
1106                    .await;
1107            });
1108
1109            // Wait until the underlying read has started, ensuring the in-flight marker exists.
1110            started_rx.await.expect("blocking read never started");
1111            {
1112                let page_cache = cache_ref.cache.read();
1113                assert!(page_cache.page_fetches.contains_key(&(blob_id, 0)));
1114            }
1115
1116            // Cancel the first fetcher before it reaches explicit cleanup.
1117            handle.abort();
1118            assert!(matches!(handle.await, Err(Error::Closed)));
1119
1120            // The guard drop path should have removed the stale in-flight entry.
1121            let page_cache = cache_ref.cache.read();
1122            assert!(
1123                !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1124                "cancelled first fetcher should not leave stale page_fetches entry"
1125            );
1126        });
1127    }
1128
1129    #[test_traced]
1130    fn test_followers_keep_single_flight_after_first_fetcher_cancellation() {
1131        let executor = deterministic::Runner::default();
1132        executor.start(|context| async move {
1133            let blob_id = 0;
1134            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1135
1136            // Return one valid full page, but hold the underlying read until the test releases it.
1137            let logical_page = vec![7u8; PAGE_SIZE.get() as usize];
1138            let crc = Crc32::checksum(&logical_page);
1139            let mut physical_page = logical_page.clone();
1140            physical_page.extend_from_slice(&Checksum::new(PAGE_SIZE.get(), crc).to_bytes());
1141            let (started_tx, started_rx) = oneshot::channel();
1142            let (release_tx, release_rx) = oneshot::channel();
1143            let reads = Arc::new(AtomicUsize::new(0));
1144            let blob = ControlledBlob {
1145                started: Arc::new(Mutex::new(Some(started_tx))),
1146                release: Arc::new(Mutex::new(Some(release_rx))),
1147                reads: reads.clone(),
1148                result: ControlledBlobResult::Success(Arc::new(physical_page)),
1149            };
1150
1151            // Start the fetch that installs the shared in-flight entry.
1152            let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1153            let cache_ref_for_first = cache_ref.clone();
1154            let blob_for_first = blob.clone();
1155            let first = context.child("first").spawn(move |_| async move {
1156                let _ = cache_ref_for_first
1157                    .read(&blob_for_first, blob_id, &mut first_buf, 0)
1158                    .await;
1159            });
1160            started_rx.await.expect("first read never started");
1161
1162            // Join as a follower while the first fetch is still blocked in the blob.
1163            let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1164            let cache_ref_for_second = cache_ref.clone();
1165            let blob_for_second = blob.clone();
1166            let second = context.child("second").spawn(move |_| async move {
1167                cache_ref_for_second
1168                    .read(&blob_for_second, blob_id, &mut second_buf, 0)
1169                    .await
1170                    .expect("second read failed");
1171                second_buf
1172            });
1173
1174            // Wait until both tasks are registered against the same in-flight fetch.
1175            loop {
1176                let joined = {
1177                    let page_cache = cache_ref.cache.read();
1178                    page_cache
1179                        .page_fetches
1180                        .get(&(blob_id, 0))
1181                        .map(|fetch| fetch.waiters == 2)
1182                        .unwrap_or(false)
1183                };
1184                if joined {
1185                    break;
1186                }
1187                context.sleep(Duration::from_millis(1)).await;
1188            }
1189
1190            // Cancel the original fetcher; the follower should keep the generation alive.
1191            first.abort();
1192            assert!(matches!(first.await, Err(Error::Closed)));
1193
1194            // A later reader should still join the existing in-flight fetch instead of starting a
1195            // second blob read.
1196            let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1197            let cache_ref_for_third = cache_ref.clone();
1198            let blob_for_third = blob.clone();
1199            let third = context.child("third").spawn(move |_| async move {
1200                cache_ref_for_third
1201                    .read(&blob_for_third, blob_id, &mut third_buf, 0)
1202                    .await
1203                    .expect("third read failed");
1204                third_buf
1205            });
1206
1207            // Either the third reader bumps the waiter count back to 2, or a bug starts a second
1208            // blob read.
1209            loop {
1210                let third_entered = {
1211                    let page_cache = cache_ref.cache.read();
1212                    reads.load(Ordering::Relaxed) > 1
1213                        || page_cache
1214                            .page_fetches
1215                            .get(&(blob_id, 0))
1216                            .map(|fetch| fetch.waiters == 2)
1217                            .unwrap_or(false)
1218                };
1219                if third_entered {
1220                    break;
1221                }
1222                context.sleep(Duration::from_millis(1)).await;
1223            }
1224
1225            // Let the single underlying fetch complete and satisfy both surviving waiters.
1226            let _ = release_tx.send(());
1227            let second_buf = second.await.expect("second task failed");
1228            let third_buf = third.await.expect("third task failed");
1229            assert_eq!(second_buf, logical_page);
1230            assert_eq!(third_buf, logical_page);
1231
1232            // All waiters should have shared the same blob read.
1233            assert_eq!(reads.load(Ordering::Relaxed), 1);
1234
1235            // The successful fetch should populate the cache for later readers.
1236            let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1237            assert_eq!(
1238                cache_ref.read_cached(blob_id, &mut cached, 0),
1239                PAGE_SIZE.get() as usize
1240            );
1241            assert_eq!(cached, logical_page);
1242
1243            // A later read should hit the cached page and avoid touching the blob again.
1244            let mut fourth_buf = vec![0u8; PAGE_SIZE.get() as usize];
1245            cache_ref
1246                .read(&blob, blob_id, &mut fourth_buf, 0)
1247                .await
1248                .unwrap();
1249            assert_eq!(fourth_buf, logical_page);
1250            assert_eq!(reads.load(Ordering::Relaxed), 1);
1251
1252            let page_cache = cache_ref.cache.read();
1253            assert!(
1254                !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1255                "completed fetch should leave no stale page_fetches entry"
1256            );
1257        });
1258    }
1259
1260    #[test_traced]
1261    fn test_page_fetch_error_removes_entry_for_all_waiters() {
1262        let executor = deterministic::Runner::default();
1263        executor.start(|context| async move {
1264            let blob_id = 0;
1265            let cache_ref = CacheRef::from_pooler(&context, PAGE_SIZE, NZUsize!(10));
1266
1267            // Hold one shared fetch in flight, then make the underlying read fail.
1268            let (started_tx, started_rx) = oneshot::channel();
1269            let (release_tx, release_rx) = oneshot::channel();
1270            let reads = Arc::new(AtomicUsize::new(0));
1271            let blob = ControlledBlob {
1272                started: Arc::new(Mutex::new(Some(started_tx))),
1273                release: Arc::new(Mutex::new(Some(release_rx))),
1274                reads: reads.clone(),
1275                result: ControlledBlobResult::Error,
1276            };
1277
1278            // Start the fetch that creates the in-flight entry.
1279            let mut first_buf = vec![0u8; PAGE_SIZE.get() as usize];
1280            let cache_ref_for_first = cache_ref.clone();
1281            let blob_for_first = blob.clone();
1282            let first = context.child("first").spawn(move |_| async move {
1283                cache_ref_for_first
1284                    .read(&blob_for_first, blob_id, &mut first_buf, 0)
1285                    .await
1286            });
1287            started_rx.await.expect("first erroring read never started");
1288
1289            // Join with a second waiter that should observe the same failure.
1290            let mut second_buf = vec![0u8; PAGE_SIZE.get() as usize];
1291            let cache_ref_for_second = cache_ref.clone();
1292            let blob_for_second = blob.clone();
1293            let second = context.child("second").spawn(move |_| async move {
1294                cache_ref_for_second
1295                    .read(&blob_for_second, blob_id, &mut second_buf, 0)
1296                    .await
1297            });
1298
1299            // Wait until both tasks share the same in-flight fetch entry.
1300            loop {
1301                let joined = {
1302                    let page_cache = cache_ref.cache.read();
1303                    page_cache
1304                        .page_fetches
1305                        .get(&(blob_id, 0))
1306                        .map(|fetch| fetch.waiters == 2)
1307                        .unwrap_or(false)
1308                };
1309                if joined {
1310                    break;
1311                }
1312                context.sleep(Duration::from_millis(1)).await;
1313            }
1314
1315            // Release the blocked read so the shared fetch resolves with an error.
1316            let _ = release_tx.send(());
1317
1318            assert!(matches!(first.await, Ok(Err(Error::ReadFailed))));
1319            assert!(matches!(second.await, Ok(Err(Error::ReadFailed))));
1320            // Both waiters should still have shared a single blob read.
1321            assert_eq!(reads.load(Ordering::Relaxed), 1);
1322
1323            // The failed generation must remove its in-flight entry and avoid caching data.
1324            {
1325                let page_cache = cache_ref.cache.read();
1326                assert!(
1327                    !page_cache.page_fetches.contains_key(&(blob_id, 0)),
1328                    "erroring fetch should leave no stale page_fetches entry"
1329                );
1330            }
1331            let mut cached = vec![0u8; PAGE_SIZE.get() as usize];
1332            assert_eq!(cache_ref.read_cached(blob_id, &mut cached, 0), 0);
1333
1334            // A later read should start a fresh fetch rather than reusing stale error state.
1335            let mut third_buf = vec![0u8; PAGE_SIZE.get() as usize];
1336            assert!(matches!(
1337                cache_ref.read(&blob, blob_id, &mut third_buf, 0).await,
1338                Err(Error::ReadFailed)
1339            ));
1340            assert_eq!(reads.load(Ordering::Relaxed), 2);
1341        });
1342    }
1343
1344    #[test_traced]
1345    fn test_read_cached_many_all_cached() {
1346        let pool = test_pool();
1347        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1348        let blob_id = cache_ref.next_id();
1349        let page0 = vec![0xAA; PAGE_SIZE.get() as usize];
1350        let page1 = vec![0xBB; PAGE_SIZE.get() as usize];
1351
1352        // Populate two pages with distinct data.
1353        {
1354            let mut cache = cache_ref.cache.write();
1355            cache.cache(blob_id, &page0, 0);
1356            cache.cache(blob_id, &page1, 1);
1357        }
1358
1359        let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1360        let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1361        let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1362
1363        cache_ref.read_cached_many(blob_id, &mut ranges);
1364
1365        // All ranges served from cache, so the vec is now empty.
1366        assert!(ranges.is_empty());
1367        drop(ranges);
1368
1369        // Buffers should contain the cached page data.
1370        assert!(buf0 == page0);
1371        assert!(buf1 == page1);
1372    }
1373
1374    #[test_traced]
1375    fn test_read_cached_many_none_cached() {
1376        let pool = test_pool();
1377        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1378        let blob_id = cache_ref.next_id();
1379
1380        let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1381        let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1382        let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf0, 0), (&mut buf1, PAGE_SIZE_U64)];
1383
1384        // Empty cache: both ranges should miss and remain in the vec unchanged.
1385        cache_ref.read_cached_many(blob_id, &mut ranges);
1386        assert_eq!(ranges.len(), 2);
1387        assert_eq!(ranges[0].1, 0);
1388        assert_eq!(ranges[1].1, PAGE_SIZE_U64);
1389    }
1390
1391    #[test_traced]
1392    fn test_read_cached_many_scattered_misses() {
1393        // Verify that read_cached_many checks ALL ranges, not just up to the
1394        // first miss. Pages 0 and 2 are cached, page 1 is not.
1395        let pool = test_pool();
1396        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1397        let blob_id = cache_ref.next_id();
1398
1399        let page0 = vec![0x11; PAGE_SIZE.get() as usize];
1400        let page2 = vec![0x33; PAGE_SIZE.get() as usize];
1401        {
1402            let mut cache = cache_ref.cache.write();
1403            cache.cache(blob_id, &page0, 0);
1404            // page 1 deliberately not cached
1405            cache.cache(blob_id, &page2, 2);
1406        }
1407
1408        let mut buf0 = vec![0u8; PAGE_SIZE_U64 as usize];
1409        let mut buf1 = vec![0u8; PAGE_SIZE_U64 as usize];
1410        let mut buf2 = vec![0u8; PAGE_SIZE_U64 as usize];
1411        let mut ranges: Vec<(&mut [u8], u64)> = vec![
1412            (&mut buf0, 0),
1413            (&mut buf1, PAGE_SIZE_U64),
1414            (&mut buf2, PAGE_SIZE_U64 * 2),
1415        ];
1416
1417        cache_ref.read_cached_many(blob_id, &mut ranges);
1418
1419        // Only the page 1 miss should remain (page 2 is still processed despite
1420        // the earlier miss).
1421        assert_eq!(ranges.len(), 1);
1422        assert_eq!(ranges[0].1, PAGE_SIZE_U64);
1423        drop(ranges);
1424
1425        // Cached pages should have their data written to the buffers.
1426        assert!(buf0 == page0);
1427        assert!(buf2 == page2);
1428        // Missed page's buffer should be untouched (still zeroed).
1429        assert!(buf1.iter().all(|b| *b == 0));
1430    }
1431
1432    #[test_traced]
1433    fn test_read_cached_many_stale_hint_after_eviction() {
1434        // Insert one page past capacity so the CLOCK evicts page 0 and reuses its slot for
1435        // page 2. Page 0's hint now points at a slot holding page 2's key, so the batched
1436        // read must report page 0 as a miss (never page 2's bytes) while still serving the
1437        // live pages.
1438        let pool = test_pool();
1439        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1440        let blob_id = cache_ref.next_id();
1441        let page_size = PAGE_SIZE.get() as usize;
1442        {
1443            let mut cache = cache_ref.cache.write();
1444            for page in 0u64..3 {
1445                cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1446            }
1447        }
1448
1449        let mut bufs: Vec<Vec<u8>> = (0..3).map(|_| vec![0u8; page_size]).collect();
1450        let mut iter = bufs.iter_mut();
1451        let mut ranges: Vec<(&mut [u8], u64)> = (0..3u64)
1452            .map(|page| (iter.next().unwrap().as_mut_slice(), page * PAGE_SIZE_U64))
1453            .collect();
1454        cache_ref.read_cached_many(blob_id, &mut ranges);
1455
1456        // Page 0 was evicted: it must be the one remaining miss, untouched.
1457        assert_eq!(ranges.len(), 1);
1458        assert_eq!(ranges[0].1, 0);
1459        drop(ranges);
1460        assert!(bufs[0].iter().all(|b| *b == 0));
1461        assert_eq!(bufs[1], vec![2u8; page_size]);
1462        assert_eq!(bufs[2], vec![3u8; page_size]);
1463    }
1464
1465    #[test_traced]
1466    fn test_read_cached_many_cross_blob_hint_collision() {
1467        // Two blobs whose salted ranges overlap share a hint entry, and the later insert
1468        // overwrites the earlier blob's hint. The hint only proposes a slot: [Clock::get_at]
1469        // validates the full (blob, page) key, so each blob reads back its own bytes (the
1470        // clobbered one through the fallback lookup), never the other's.
1471        let pool = test_pool();
1472        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1473        let blob_a = cache_ref.next_id();
1474        let blob_b = cache_ref.next_id();
1475        let page_size = PAGE_SIZE.get() as usize;
1476        let page_a = 5u64;
1477        let page_b = {
1478            let mut cache = cache_ref.cache.write();
1479
1480            // Solve hint_index(blob_b, page_b) == hint_index(blob_a, page_a) for page_b.
1481            let mask = cache.hints.len() as u64 - 1;
1482            let page_b = page_a
1483                .wrapping_add(blob_a.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1484                .wrapping_sub(blob_b.wrapping_mul(commonware_utils::GOLDEN_RATIO))
1485                & mask;
1486            assert_eq!(
1487                cache.hint_index(blob_a, page_a),
1488                cache.hint_index(blob_b, page_b)
1489            );
1490            cache.cache(blob_a, &vec![0xAA; page_size], page_a);
1491            cache.cache(blob_b, &vec![0xBB; page_size], page_b);
1492            page_b
1493        };
1494
1495        for (blob, page, byte) in [(blob_a, page_a, 0xAAu8), (blob_b, page_b, 0xBB)] {
1496            let mut buf = vec![0u8; page_size];
1497            let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1498            cache_ref.read_cached_many(blob, &mut ranges);
1499            assert!(
1500                ranges.is_empty(),
1501                "blob {blob} page {page} should be cached"
1502            );
1503            drop(ranges);
1504            assert_eq!(buf, vec![byte; page_size]);
1505        }
1506    }
1507
1508    #[test_traced]
1509    fn test_read_cached_many_sparse_page_number_keeps_hints_fixed() {
1510        // Hint memory is fixed at construction: caching at an extreme page number must not
1511        // grow any structure, and the page is still served through the hint path.
1512        let pool = test_pool();
1513        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(2));
1514        let blob_id = cache_ref.next_id();
1515        let page_size = PAGE_SIZE.get() as usize;
1516        let page_num = u64::MAX / PAGE_SIZE_U64 - 1;
1517        {
1518            let mut cache = cache_ref.cache.write();
1519            let hints = cache.hints.len();
1520            cache.cache(blob_id, &vec![0x5A; page_size], page_num);
1521            assert_eq!(cache.hints.len(), hints);
1522        }
1523
1524        let mut buf = vec![0u8; page_size];
1525        let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page_num * PAGE_SIZE_U64)];
1526        cache_ref.read_cached_many(blob_id, &mut ranges);
1527        assert!(ranges.is_empty());
1528        drop(ranges);
1529        assert_eq!(buf, vec![0x5A; page_size]);
1530    }
1531
1532    #[test_traced]
1533    fn test_read_cached_many_invalidated_page_is_a_miss() {
1534        // Invalidated pages free their slots but keep their keys. Their hints need no
1535        // cleanup: a freed slot is not live, so the dropped page reads as a miss until
1536        // re-cached.
1537        let pool = test_pool();
1538        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(4));
1539        let blob_id = cache_ref.next_id();
1540        let page_size = PAGE_SIZE.get() as usize;
1541        {
1542            let mut cache = cache_ref.cache.write();
1543            for page in 0u64..4 {
1544                cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1545            }
1546        }
1547        cache_ref.invalidate_from(blob_id, 2);
1548
1549        let read_page = |page: u64| {
1550            let mut buf = vec![0u8; page_size];
1551            let mut ranges: Vec<(&mut [u8], u64)> = vec![(&mut buf, page * PAGE_SIZE_U64)];
1552            cache_ref.read_cached_many(blob_id, &mut ranges);
1553            let hit = ranges.is_empty();
1554            drop(ranges);
1555            hit.then_some(buf)
1556        };
1557        assert_eq!(read_page(0), Some(vec![1u8; page_size]));
1558        assert_eq!(read_page(1), Some(vec![2u8; page_size]));
1559        assert_eq!(read_page(2), None);
1560        assert_eq!(read_page(3), None);
1561
1562        // Re-caching a dropped page restores it through the hint path.
1563        {
1564            let mut cache = cache_ref.cache.write();
1565            cache.cache(blob_id, &vec![0xCC; page_size], 2);
1566        }
1567        assert_eq!(read_page(2), Some(vec![0xCC; page_size]));
1568    }
1569
1570    #[rstest]
1571    #[case::empty_read(vec![], 0, 0, 0)]
1572    #[case::single_cached_page(vec![0], 3, 5, 5)]
1573    #[case::cached_range_can_cross_pages(vec![0, 1], PAGE_SIZE_U64 - 2, 4, 4)]
1574    #[case::missing_first_page_reads_nothing(vec![1], 0, 4, 0)]
1575    #[case::missing_later_page_truncates_read(vec![0], PAGE_SIZE_U64 - 2, 4, 2)]
1576    fn test_read_cached(
1577        #[case] cached_pages: Vec<u64>,
1578        #[case] logical_offset: u64,
1579        #[case] len: usize,
1580        #[case] expected_count: usize,
1581    ) {
1582        let pool = test_pool();
1583        let cache_ref = CacheRef::new(pool, PAGE_SIZE, NZUsize!(10));
1584        let blob_id = cache_ref.next_id();
1585        let sentinel = 0xEE;
1586        let page_size = PAGE_SIZE.get() as usize;
1587
1588        {
1589            let mut cache = cache_ref.cache.write();
1590            for page in cached_pages {
1591                // Use a distinct byte per page so cross-page reads prove both halves were copied.
1592                cache.cache(blob_id, &vec![page as u8 + 1; page_size], page);
1593            }
1594        }
1595
1596        let mut buf = vec![sentinel; len];
1597        let count = cache_ref.read_cached(blob_id, &mut buf, logical_offset);
1598        assert_eq!(count, expected_count);
1599
1600        // The satisfied prefix holds cached bytes; everything past the first fault is untouched.
1601        assert_eq!(buf[..count], expected_cached_bytes(logical_offset, count));
1602        assert!(buf[count..].iter().all(|b| *b == sentinel));
1603    }
1604}