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