Skip to main content

lsm_tree/
cache.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5#[cfg(feature = "zstd")]
6use crate::UserKey;
7use crate::sharded_cache::{Priority, ShardedCache, Weighter};
8use crate::table::block::{BlockType, Header};
9use crate::table::{Block, BlockOffset};
10use crate::value::InternalValue;
11use crate::{GlobalTableId, UserValue};
12
13const TAG_BLOCK: u8 = 0;
14const TAG_BLOB: u8 = 1;
15#[cfg(feature = "zstd")]
16const TAG_PARTIAL_BLOCK: u8 = 2;
17/// Row-cache tag: a fully resolved point-read result (`InternalValue`) keyed by
18/// the owning SST's id + the user key's hash, so a repeat point read returns the
19/// decoded value without re-loading and re-decoding its data block.
20const TAG_ROW: u8 = 3;
21/// Decoded-directory tag: a columnar row group's page directory, decoded and
22/// checked once, keyed by the group's offset.
23#[cfg(feature = "columnar")]
24const TAG_DIRECTORY: u8 = 4;
25
26#[derive(Clone)]
27enum Item {
28    Block(Block),
29    /// A separated value, carrying everything about the handle that the cache
30    /// key does not: the user key it belongs to and the record's on-disk size.
31    ///
32    /// The cache key is the blob file id plus the offset, which identifies a
33    /// POSITION, not a record. A corrupt index entry can name that position
34    /// with a different key, or with the right key and the wrong size. A direct
35    /// read rejects both (the reader compares the key it was asked for AND the
36    /// declared size against the record header), so the cached path keeps the
37    /// same two fields and compares them, rather than trusting the position.
38    Blob(crate::UserKey, u32, UserValue),
39    /// A resolved point-read result for one user key in one (immutable) SST: the
40    /// newest version found there. The full key is carried in `key.user_key` so a
41    /// hash collision on the cache key is caught (verified on lookup) rather than
42    /// returning a wrong value.
43    Row(InternalValue),
44    /// The adaptive partial-tier entry for a cold zstd block: the decompressed
45    /// prefix + resume snapshot (so a later read extends it without re-decoding
46    /// from block 0) plus the access stats driving promotion to a full resident
47    /// block. The served data block is synthesized on demand from the prefix, so
48    /// only the touched fraction stays resident. See [`Cache::peek_partial_block`].
49    #[cfg(feature = "zstd")]
50    PartialBlock(PartialBlockEntry),
51    /// A columnar row group's page directory, decoded and checked, with the
52    /// directory block's on-disk length. Every read of a group starts from its
53    /// directory, and a directory of many row pages is dozens to hundreds of
54    /// entries whose decode sorts them to prove the page grid: caching it
55    /// decoded makes every later read of the group skip both, where caching
56    /// the raw block would repeat them per read.
57    #[cfg(feature = "columnar")]
58    Directory(
59        alloc::sync::Arc<crate::table::column_page::PageDirectory>,
60        u32,
61    ),
62}
63
64/// A cached partial-tier entry: the resumable decode state for a cold block plus
65/// the access stats the promotion heuristic reads. When the block is read often
66/// enough or its decoded fraction passes the promotion threshold, the reader
67/// decodes it fully and caches the whole block instead, evicting this entry.
68#[cfg(feature = "zstd")]
69#[doc(hidden)]
70#[derive(Clone)]
71pub struct PartialBlockEntry {
72    /// Resumable decode state: the decompressed prefix (`window_prime`), the
73    /// entropy/repcode snapshot, the inner-block count, and the compressed
74    /// cursor. The served data block is synthesized from `window_prime`; growth
75    /// resumes from the snapshot.
76    pub resume: crate::table::lazy_block::PartialResume,
77    /// Highest user key the decoded prefix covers (its last complete entry).
78    pub covered_upper: UserKey,
79    /// Total inner zstd blocks in the full data block.
80    pub total_blocks: u32,
81    /// Number of times this partial entry has served a read (promotion input).
82    pub hits: u32,
83}
84
85#[derive(Clone, Copy, Eq, core::hash::Hash, PartialEq)]
86struct CacheKey(u8, u64, u64, u64);
87
88impl From<(u8, u64, u64, u64)> for CacheKey {
89    fn from((tag, root_id, table_id, offset): (u8, u64, u64, u64)) -> Self {
90        Self(tag, root_id, table_id, offset)
91    }
92}
93
94/// What a cached row costs the cache: key bytes + value bytes + a fixed term for
95/// the `InternalKey` scalars (seqno + `value_type`) and the entry's own
96/// bookkeeping.
97///
98/// Shared by the weigher and by `insert_row`'s admission pre-check so the two
99/// cannot drift: a pre-check that charged less than the weigher would copy a
100/// value the cache then refuses, and one that charged more would skip a row that
101/// would have fit.
102const fn row_weight(key_len: usize, value_len: usize) -> u64 {
103    key_len as u64 + value_len as u64 + 16
104}
105
106/// What a block weighs in the cache: its header and its decoded payload.
107pub fn block_weight(block: &crate::table::Block) -> u64 {
108    (Header::header_len(block.header.block_type) as u64)
109        + u64::from(block.header.uncompressed_length)
110}
111
112#[derive(Clone)]
113struct BlockWeighter;
114
115impl Weighter<CacheKey, Item> for BlockWeighter {
116    fn weight(&self, _: &CacheKey, item: &Item) -> u64 {
117        use Item::{Blob, Block};
118
119        match item {
120            Block(b) => block_weight(b),
121            // Key + value; the size field is an inline scalar. The prefetch's
122            // admission budget charges itself the same way, so the two agree.
123            Blob(key, _, b) => (key.len() + b.len()) as u64,
124            Item::Row(iv) => row_weight(iv.key.user_key.len(), iv.value.len()),
125            // Weighed by the resident decompressed prefix + covered key; the
126            // shared `Arc<ResumeState>` scratch is approximated by a small fixed
127            // term rather than counted per entry.
128            #[cfg(feature = "zstd")]
129            Item::PartialBlock(entry) => {
130                entry.resume.window_prime.len() as u64 + entry.covered_upper.len() as u64 + 64
131            }
132            #[cfg(feature = "columnar")]
133            Item::Directory(directory, _) => directory.heap_size() as u64 + 64,
134        }
135    }
136}
137
138/// Cache, in which blocks or blobs are cached in-memory
139/// after being retrieved from disk
140///
141/// This speeds up consecutive queries to nearby data, improving
142/// read performance for hot data.
143///
144/// # Examples
145///
146/// Sharing cache between multiple trees
147///
148/// ```
149/// # use lsm_tree::{Tree, Config, Cache};
150/// # use std::sync::Arc;
151/// #
152/// // Provide 64 MB of cache capacity
153/// let cache = Arc::new(Cache::with_capacity_bytes(64 * 1_000 * 1_000));
154///
155/// # let folder = tempfile::tempdir()?;
156/// let tree1 = Config::new(folder, Default::default(), Default::default()).use_cache(cache.clone()).open()?;
157/// # let folder = tempfile::tempdir()?;
158/// let tree2 = Config::new(folder, Default::default(), Default::default()).use_cache(cache.clone()).open()?;
159/// #
160/// # Ok::<(), lsm_tree::Error>(())
161/// ```
162pub struct Cache {
163    // NOTE: rustc_hash performed best: https://fjall-rs.github.io/post/fjall-2-1
164    /// In-tree sharded S3-FIFO cache (byte-weighted).
165    data: ShardedCache<CacheKey, Item, BlockWeighter, rustc_hash::FxBuildHasher>,
166    /// When false, the row cache (decoded point-read results) is off, so
167    /// `get_row` always misses and `insert_row` is a no-op. Blocks / blobs are
168    /// cached regardless.
169    ///
170    /// On by default. Rows share the block cache's byte capacity, so the worry
171    /// was that a workload without key reuse would spend capacity on rows at the
172    /// blocks' expense. Measurement says it does not: scan-heavy and
173    /// larger-than-cache arms are unchanged or better, while a repeat point read
174    /// skips the index walk and the data-block decode outright. Turn it off for
175    /// a workload measured to be one of the exceptions.
176    row_cache_enabled: bool,
177    /// When true (default), index / filter / range-tombstone blocks are admitted
178    /// at [`Priority::High`] so heavy data-block churn (working set >> cache)
179    /// cannot evict the metadata blocks every seek touches, sparing a re-read +
180    /// re-decode on the next index descent. Disable to put every block on equal
181    /// footing (the pre-priority behaviour), e.g. for A/B measurement.
182    metadata_priority: bool,
183    /// The cold loads in flight, keyed like the entries they will fill, so
184    /// readers that miss the same block or blob at once share one read and
185    /// one decode. Only a miss consults it.
186    loads: Loads,
187}
188
189/// The registry of cold loads in flight.
190type Loads =
191    crate::inflight::InflightLoads<CacheKey, Item, LoadIdentity, rustc_hash::FxBuildHasher>;
192
193/// The lead of one cold load, borrowing the cache's registry.
194type LoadLead<'a> =
195    crate::inflight::Lead<'a, CacheKey, Item, LoadIdentity, rustc_hash::FxBuildHasher>;
196
197/// What a cold load reads, beyond its position: a waiter takes a load's
198/// result only when it asked for the same thing, so a request with another
199/// extent, role or codec never receives bytes interpreted for a different one.
200#[derive(Clone, Copy, PartialEq, Eq)]
201pub enum LoadIdentity {
202    /// A table block: its role, on-disk length, codec (dictionary included)
203    /// and whether it is encrypted.
204    Block {
205        block_type: BlockType,
206        size: u32,
207        compression: crate::CompressionType,
208        encrypted: bool,
209    },
210    /// A separated value: its record's on-disk size. The user key is checked
211    /// against the value the load returns, as a cache hit checks it.
212    Blob { on_disk_size: u32 },
213    /// Growth of a block's partially decoded prefix. Waiters take no value:
214    /// once the growth ends they look at the cached prefix again.
215    #[cfg(feature = "zstd")]
216    PartialGrowth,
217}
218
219/// How a read that needs a block's partial prefix grown proceeds.
220#[cfg(feature = "zstd")]
221pub enum GrowthClaim<'a> {
222    /// No growth of the block runs: grow it, holding the lead until the
223    /// grown prefix is cached.
224    Lead(GrowthLead<'a>),
225    /// Another read grew the block while this one waited: look at the cached
226    /// prefix again before growing anything.
227    Waited,
228    /// Grow without coordination: the registry is full.
229    Alone,
230}
231
232/// The lead of a partial prefix's growth. Dropping it, after the grown prefix
233/// is cached or on any failure, lets the reads waiting on it look again.
234#[cfg(feature = "zstd")]
235pub struct GrowthLead<'a>(
236    #[expect(dead_code, reason = "held for its drop, which releases the waiters")] LoadLead<'a>,
237);
238
239/// How a block miss proceeds: lead the load, take a concurrent one's block,
240/// or load alone.
241pub enum BlockClaim<'a> {
242    /// No load of the block is in flight: read it, then [`BlockLead::publish`].
243    Lead(BlockLead<'a>),
244    /// A load of the block finished while this request waited for it.
245    Shared(Block),
246    /// Read the block without coordination: the registry is full, an
247    /// incompatible load of the position is in flight, or the load this
248    /// request waited for failed.
249    Alone,
250}
251
252/// The lead of a block load. Dropping it unpublished (an error or an unwind)
253/// releases its waiters to read on their own. A batched read's lead also
254/// keeps its thread from waiting on other loads while it lives.
255pub struct BlockLead<'a>(LoadLead<'a>, Option<crate::inflight::BatchToken>);
256
257impl BlockLead<'_> {
258    /// Hands the loaded block to the waiters, if any; insert it into the
259    /// cache first, so a miss after this sees it there.
260    pub(crate) fn publish(self, block: &Block) {
261        let Self(lead, _batch) = self;
262        lead.publish_with(|| Item::Block(block.clone()));
263    }
264}
265
266/// How a miss of a row group's decoded directory proceeds, as [`BlockClaim`]
267/// for a block.
268#[cfg(feature = "columnar")]
269pub enum DirectoryClaim<'a> {
270    /// Read the directory, then [`DirectoryLead::publish`] it.
271    Lead(DirectoryLead<'a>),
272    /// A concurrent read of the group's directory finished: the directory
273    /// and its block's on-disk length.
274    Shared(
275        alloc::sync::Arc<crate::table::column_page::PageDirectory>,
276        u32,
277    ),
278    /// Read the directory without coordination.
279    Alone,
280}
281
282/// The lead of a directory load, as [`BlockLead`] for a block.
283#[cfg(feature = "columnar")]
284pub struct DirectoryLead<'a>(LoadLead<'a>);
285
286#[cfg(feature = "columnar")]
287impl DirectoryLead<'_> {
288    /// Hands the decoded directory and its block's on-disk length to the
289    /// waiters; cache it first, so a miss after this sees it there.
290    pub(crate) fn publish(
291        self,
292        directory: &alloc::sync::Arc<crate::table::column_page::PageDirectory>,
293        len: u32,
294    ) {
295        self.0
296            .publish_with(|| Item::Directory(alloc::sync::Arc::clone(directory), len));
297    }
298}
299
300/// How a blob miss proceeds, as [`BlockClaim`] for a block.
301pub enum BlobClaim<'a> {
302    /// Read the value, then [`BlobLead::publish`].
303    Lead(BlobLead<'a>),
304    /// A concurrent read of the same record, for the same key, finished.
305    Shared(UserValue),
306    /// Read the value without coordination.
307    Alone,
308}
309
310/// The lead of a blob load, as [`BlockLead`] for a block.
311pub struct BlobLead<'a>(LoadLead<'a>);
312
313impl BlobLead<'_> {
314    /// Hands the value read for `user_key` to the waiters, caching it first
315    /// when `fill` says so. Returns the bytes copied to own the key, which the
316    /// cache entry and the waiters share: none when the value is neither
317    /// cached nor awaited.
318    pub(crate) fn publish(
319        self,
320        cache: &Cache,
321        vlog_id: crate::TreeId,
322        vhandle: &crate::vlog::ValueHandle,
323        user_key: &[u8],
324        value: UserValue,
325        fill: bool,
326    ) -> usize {
327        if !fill && !self.0.has_waiters() {
328            // Dropped unpublished: nothing to share, and the key is not
329            // copied for nobody.
330            return 0;
331        }
332        let item = Item::Blob(user_key.into(), vhandle.on_disk_size, value);
333        if fill {
334            cache.data.insert(
335                (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into(),
336                item.clone(),
337            );
338        }
339        self.0.publish(item);
340        user_key.len()
341    }
342}
343
344/// Shards of the load registry: as many as the cache, so two loads that
345/// would contend on one cache shard are the ones that share a registry shard.
346const LOAD_SHARDS: usize = BLOCK_CACHE_SHARDS;
347/// Loads one registry shard coordinates at once, so the registry holds at
348/// most `LOAD_SHARDS * LOADS_PER_SHARD` tickets; a miss past that loads on
349/// its own.
350const LOADS_PER_SHARD: usize = 64;
351/// How long cold loads must take before concurrent misses of one object wait
352/// for a single load instead of each loading it (see
353/// [`Cache::with_load_coalescing_after`]). Loads served from the page cache
354/// take a few microseconds for a block and tens for a whole column row group,
355/// and waiting on another reader there loses to loading alone; this sits
356/// above them and below a read that reaches a device or the network.
357const DEFAULT_LOAD_COALESCING_AFTER: core::time::Duration = core::time::Duration::from_micros(64);
358
359/// Number of shards in the block cache. 64 keeps per-shard write contention low
360/// on many-core hosts while the lock array stays small; reads take a shared lock
361/// and don't contend regardless of shard count.
362const BLOCK_CACHE_SHARDS: usize = 64;
363/// Seeds the per-shard ghost-queue sizing (S3-FIFO remembers recently-evicted
364/// fingerprints to fast-track re-admission). Matches the previous
365/// `estimated_items_capacity`.
366const BLOCK_CACHE_EST_ITEMS: usize = 10_000;
367
368impl Cache {
369    /// Creates a new block cache with roughly `n` bytes of capacity.
370    #[must_use]
371    pub fn with_capacity_bytes(bytes: u64) -> Self {
372        Self {
373            data: ShardedCache::with_weighter(
374                bytes,
375                BLOCK_CACHE_SHARDS,
376                BLOCK_CACHE_EST_ITEMS,
377                BlockWeighter,
378                rustc_hash::FxBuildHasher,
379            ),
380            row_cache_enabled: true,
381            metadata_priority: true,
382            loads: crate::inflight::InflightLoads::new(
383                LOAD_SHARDS,
384                LOADS_PER_SHARD,
385                DEFAULT_LOAD_COALESCING_AFTER,
386                rustc_hash::FxBuildHasher,
387            ),
388        }
389    }
390
391    /// Claims the cold load of the block at `offset` after a cache miss.
392    ///
393    /// Blocks while a compatible load of the block runs on another thread,
394    /// returning its block. A [`BlockClaim::Lead`] holder checks the cache
395    /// once more before reading: a load that finished between its miss and
396    /// this claim left the block there.
397    ///
398    /// A thread that holds a batched read's lead never waits (see
399    /// [`crate::inflight::may_wait`]): it takes a block another read already
400    /// published and reads one still being read again on its own.
401    pub(crate) fn claim_block(
402        &self,
403        id: GlobalTableId,
404        offset: BlockOffset,
405        identity: LoadIdentity,
406    ) -> BlockClaim<'_> {
407        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
408        match self.loads.claim(key, identity) {
409            crate::inflight::Claim::Lead(lead) => BlockClaim::Lead(BlockLead(lead, None)),
410            crate::inflight::Claim::Join(join) => {
411                let shared = if crate::inflight::may_wait() {
412                    join.wait()
413                } else {
414                    join.try_take()
415                };
416                match shared {
417                    Some(Item::Block(block)) => BlockClaim::Shared(block),
418                    _ => BlockClaim::Alone,
419                }
420            }
421            crate::inflight::Claim::Alone => BlockClaim::Alone,
422        }
423    }
424
425    /// Claims the lead of the load of the block at `offset` for a batched
426    /// read about to read it along with others; `None` when the read reads
427    /// it alone. Never waits, since the read goes on to lead more blocks: a
428    /// block another read is loading is read again alone. Nor does it take
429    /// one: the registry holds only loads not yet published (a lead leaves
430    /// it before it settles), and a published one is in the cache the caller
431    /// just missed.
432    ///
433    /// `lead` says whether the read may lead the load (see
434    /// [`crate::inflight::batches_may_lead`]); a lead it takes counts on this
435    /// thread until it is published or dropped, so the thread waits on
436    /// nothing meanwhile.
437    pub(crate) fn claim_batched_block(
438        &self,
439        id: GlobalTableId,
440        offset: BlockOffset,
441        identity: LoadIdentity,
442        lead: bool,
443    ) -> Option<BlockLead<'_>> {
444        if !(lead && crate::inflight::batches_may_lead()) {
445            return None;
446        }
447        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
448        match self.loads.claim(key, identity) {
449            crate::inflight::Claim::Lead(lead) => {
450                Some(BlockLead(lead, Some(crate::inflight::BatchToken::new())))
451            }
452            crate::inflight::Claim::Join(_) | crate::inflight::Claim::Alone => None,
453        }
454    }
455
456    /// Claims the load of a row group's page at `offset` for a read about to
457    /// fetch it along with the group's other pages. Waits for a compatible
458    /// load another read runs only while this thread holds no lead of a
459    /// batched read, so until this read leads a page of its own: from then
460    /// on others may be waiting on it, and a page another read is loading is
461    /// read again alone. A lead taken here counts as a batched one.
462    ///
463    /// `with_directory` says the load of the group's directory is
464    /// coordinated, by this read or one it took the directory from: its pages
465    /// are then coordinated too, whatever their own loads measured, since a
466    /// page read along with the directory is never timed on its own.
467    #[cfg(feature = "columnar")]
468    pub(crate) fn claim_page(
469        &self,
470        id: GlobalTableId,
471        offset: BlockOffset,
472        identity: LoadIdentity,
473        with_directory: bool,
474    ) -> BlockClaim<'_> {
475        if !crate::inflight::batches_may_lead() {
476            return BlockClaim::Alone;
477        }
478        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
479        match self.loads.claim_as(key, identity, with_directory) {
480            crate::inflight::Claim::Lead(lead) => {
481                BlockClaim::Lead(BlockLead(lead, Some(crate::inflight::BatchToken::new())))
482            }
483            crate::inflight::Claim::Join(join) if crate::inflight::may_wait() => {
484                match join.wait() {
485                    Some(Item::Block(block)) => BlockClaim::Shared(block),
486                    _ => BlockClaim::Alone,
487                }
488            }
489            crate::inflight::Claim::Join(_) | crate::inflight::Claim::Alone => BlockClaim::Alone,
490        }
491    }
492
493    /// Claims the cold read of the decoded directory of the row group at
494    /// `offset`, as [`Self::claim_block`] does for a block. `identity` names
495    /// the group's extent, so a read of another extent at the same offset
496    /// never takes this directory.
497    #[cfg(feature = "columnar")]
498    pub(crate) fn claim_directory(
499        &self,
500        id: GlobalTableId,
501        offset: BlockOffset,
502        identity: LoadIdentity,
503    ) -> DirectoryClaim<'_> {
504        let key: CacheKey = (TAG_DIRECTORY, id.tree_id(), id.table_id(), *offset).into();
505        match self.loads.claim(key, identity) {
506            crate::inflight::Claim::Lead(lead) => DirectoryClaim::Lead(DirectoryLead(lead)),
507            crate::inflight::Claim::Join(join) => {
508                let shared = if crate::inflight::may_wait() {
509                    join.wait()
510                } else {
511                    join.try_take()
512                };
513                match shared {
514                    Some(Item::Directory(directory, len)) => DirectoryClaim::Shared(directory, len),
515                    _ => DirectoryClaim::Alone,
516                }
517            }
518            crate::inflight::Claim::Alone => DirectoryClaim::Alone,
519        }
520    }
521
522    /// How many requests wait on cold loads in flight, for tests.
523    #[cfg(test)]
524    pub(crate) fn load_waiters(&self) -> usize {
525        self.loads.waiters()
526    }
527
528    /// How many cold loads are in flight, for tests.
529    #[cfg(test)]
530    pub(crate) fn loads_in_flight(&self) -> usize {
531        self.loads.len()
532    }
533
534    /// The load-time estimates of every registry shard summed, for tests.
535    #[cfg(test)]
536    pub(crate) fn load_estimates_total(&self) -> u64 {
537        self.loads.estimates_total()
538    }
539
540    /// The load-time estimate of the shard that the partial-prefix growth of
541    /// the block at `offset` coordinates in, for tests.
542    #[cfg(all(test, feature = "zstd"))]
543    pub(crate) fn partial_growth_estimate(&self, id: GlobalTableId, offset: BlockOffset) -> u64 {
544        self.loads
545            .estimate(&(TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into())
546    }
547
548    /// Claims the cold read of the separated value `vhandle` names for
549    /// `user_key`, as [`Self::claim_block`] does for a block. A shared value
550    /// is returned only when the read it came from was for the same key and
551    /// size: the position alone names a record, not whose it is.
552    pub(crate) fn claim_blob(
553        &self,
554        vlog_id: crate::TreeId,
555        vhandle: &crate::vlog::ValueHandle,
556        user_key: &[u8],
557    ) -> BlobClaim<'_> {
558        let key: CacheKey = (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into();
559        let identity = LoadIdentity::Blob {
560            on_disk_size: vhandle.on_disk_size,
561        };
562        match self.loads.claim(key, identity) {
563            crate::inflight::Claim::Lead(lead) => BlobClaim::Lead(BlobLead(lead)),
564            crate::inflight::Claim::Join(join) => {
565                let shared = if crate::inflight::may_wait() {
566                    join.wait()
567                } else {
568                    join.try_take()
569                };
570                match shared {
571                    Some(Item::Blob(read_key, size, value))
572                        if &*read_key == user_key && size == vhandle.on_disk_size =>
573                    {
574                        BlobClaim::Shared(value)
575                    }
576                    _ => BlobClaim::Alone,
577                }
578            }
579            crate::inflight::Claim::Alone => BlobClaim::Alone,
580        }
581    }
582
583    /// Enables or disables the row cache (decoded point-read results), returning
584    /// the cache for builder-style configuration. On by default; rows share the
585    /// block cache's byte capacity.
586    #[must_use]
587    pub fn with_row_cache(mut self, enabled: bool) -> Self {
588        self.row_cache_enabled = enabled;
589        self
590    }
591
592    /// Whether the row cache is enabled (see [`Cache::with_row_cache`]).
593    #[must_use]
594    pub fn row_cache_enabled(&self) -> bool {
595        self.row_cache_enabled
596    }
597
598    /// Enables or disables high-priority pinning of index / filter /
599    /// range-tombstone blocks (see [`Cache::metadata_priority`] field docs),
600    /// returning the cache for builder-style configuration. On by default.
601    #[must_use]
602    pub fn with_metadata_priority(mut self, enabled: bool) -> Self {
603        self.metadata_priority = enabled;
604        self
605    }
606
607    /// Whether metadata-block priority pinning is enabled (see
608    /// [`Cache::with_metadata_priority`]).
609    #[must_use]
610    pub fn metadata_priority(&self) -> bool {
611        self.metadata_priority
612    }
613
614    /// Sets how long cold loads must take before readers that miss the same
615    /// block, blob or column directory at once share one read and decode
616    /// instead of each doing it, returning the cache for builder-style
617    /// configuration.
618    ///
619    /// The cache times a sample of its cold loads. While they finish quicker
620    /// than `after` (blocks served from the page cache, with cores to spare),
621    /// a reader loads on its own: waiting for another reader's load would
622    /// cost it more than its own. Once loads take longer (slow or remote
623    /// storage, or cores saturated), concurrent misses of one object wait for
624    /// a single load. `Duration::ZERO` shares every load; `Duration::MAX`
625    /// none. Without the `std` feature loads are not timed, so only
626    /// `Duration::ZERO` shares there.
627    ///
628    /// # Examples
629    ///
630    /// ```
631    /// use core::time::Duration;
632    /// use lsm_tree::Cache;
633    ///
634    /// let cache = Cache::with_capacity_bytes(64 << 20)
635    ///     .with_load_coalescing_after(Duration::from_micros(50));
636    /// assert_eq!(cache.load_coalescing_after(), Duration::from_micros(50));
637    /// ```
638    #[must_use]
639    pub fn with_load_coalescing_after(mut self, after: core::time::Duration) -> Self {
640        self.loads.set_coalesce_after(after);
641        self
642    }
643
644    /// How long cold loads must take before concurrent misses share them
645    /// (see [`Cache::with_load_coalescing_after`]).
646    #[must_use]
647    pub fn load_coalescing_after(&self) -> core::time::Duration {
648        self.loads.coalesce_after()
649    }
650
651    /// Starts timing a cold load about to run on the calling thread, for the
652    /// estimate that decides whether misses share loads.
653    pub(crate) fn time_load() -> crate::inflight::LoadTimer {
654        crate::inflight::LoadTimer::start()
655    }
656
657    /// Records the cold load of the block at `offset` that `timer` timed.
658    pub(crate) fn record_block_load(
659        &self,
660        id: GlobalTableId,
661        offset: BlockOffset,
662        timer: crate::inflight::LoadTimer,
663    ) {
664        self.loads.record_load(
665            &(TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into(),
666            timer,
667        );
668    }
669
670    /// Records a read of the blocks at `offsets` that `timer` timed, issued
671    /// together: each takes an even share of it.
672    #[cfg(feature = "columnar")]
673    pub(crate) fn record_block_loads(
674        &self,
675        id: GlobalTableId,
676        offsets: impl Iterator<Item = BlockOffset> + Clone,
677        timer: crate::inflight::LoadTimer,
678    ) {
679        self.loads.record_loads(
680            offsets.map(|offset| (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into()),
681            timer,
682        );
683    }
684
685    /// Records the growth of the partially decoded prefix of the block at
686    /// `offset` that `timer` timed.
687    #[cfg(feature = "zstd")]
688    pub(crate) fn record_partial_growth(
689        &self,
690        id: GlobalTableId,
691        offset: BlockOffset,
692        timer: crate::inflight::LoadTimer,
693    ) {
694        self.loads.record_load(
695            &(TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into(),
696            timer,
697        );
698    }
699
700    /// Whether a full load of the block at `offset` is in flight, which a
701    /// read that would decode only a prefix of it can take instead.
702    #[cfg(feature = "zstd")]
703    pub(crate) fn block_load_in_flight(&self, id: GlobalTableId, offset: BlockOffset) -> bool {
704        self.loads
705            .in_flight(&(TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into())
706    }
707
708    /// Records a wait of a batched read that `timer` timed, which handed back
709    /// `reads` block reads.
710    pub(crate) fn record_batch_wait(&self, timer: crate::inflight::LoadTimer, reads: usize) {
711        self.loads.record_batch_wait(timer, reads);
712    }
713
714    /// Records the cold read of the separated value `vhandle` names that
715    /// `timer` timed.
716    pub(crate) fn record_blob_load(
717        &self,
718        vlog_id: crate::TreeId,
719        vhandle: &crate::vlog::ValueHandle,
720        timer: crate::inflight::LoadTimer,
721    ) {
722        self.loads.record_load(
723            &(TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into(),
724            timer,
725        );
726    }
727
728    /// Records the cold read of the row group at `offset` that `timer` timed.
729    #[cfg(feature = "columnar")]
730    pub(crate) fn record_directory_load(
731        &self,
732        id: GlobalTableId,
733        offset: BlockOffset,
734        timer: crate::inflight::LoadTimer,
735    ) {
736        self.loads.record_load(
737            &(TAG_DIRECTORY, id.tree_id(), id.table_id(), *offset).into(),
738            timer,
739        );
740    }
741
742    /// Returns the amount of cached bytes.
743    #[must_use]
744    pub fn size(&self) -> u64 {
745        self.data.weight()
746    }
747
748    /// Returns the cache capacity in bytes.
749    #[must_use]
750    pub fn capacity(&self) -> u64 {
751        self.data.capacity()
752    }
753
754    /// The heaviest entry this cache can actually keep resident: one shard's
755    /// capacity. Heavier inserts are refused by the cache; callers that would
756    /// build such an entry (e.g. the scan read-ahead) can skip the work.
757    #[must_use]
758    pub(crate) fn max_entry_weight(&self) -> u64 {
759        self.data.max_entry_weight()
760    }
761
762    #[doc(hidden)]
763    #[must_use]
764    pub fn get_block(&self, id: GlobalTableId, offset: BlockOffset) -> Option<Block> {
765        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
766
767        Some(match self.data.get(&key)? {
768            Item::Block(block) => block,
769            Item::Blob(..) | Item::Row(_) => unreachable!("invalid cache item"),
770            #[cfg(feature = "zstd")]
771            Item::PartialBlock(_) => unreachable!("invalid cache item"),
772            #[cfg(feature = "columnar")]
773            Item::Directory(..) => unreachable!("invalid cache item"),
774        })
775    }
776
777    /// [`Self::get_block`] that leaves the entry's recency and the hit
778    /// statistics as they were, for a reader that must not change what a later
779    /// read finds cached.
780    #[must_use]
781    pub(crate) fn peek_block(&self, id: GlobalTableId, offset: BlockOffset) -> Option<Block> {
782        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
783
784        Some(match self.data.peek(&key)? {
785            Item::Block(block) => block,
786            Item::Blob(..) | Item::Row(_) => unreachable!("invalid cache item"),
787            #[cfg(feature = "zstd")]
788            Item::PartialBlock(_) => unreachable!("invalid cache item"),
789            #[cfg(feature = "columnar")]
790            Item::Directory(..) => unreachable!("invalid cache item"),
791        })
792    }
793
794    /// The decoded directory of the row group at `offset`, with its block's
795    /// on-disk length, when it is cached. `touch` says whether the lookup may
796    /// promote the entry and count as a hit, as [`Self::get_block`] does, or
797    /// must leave both as they were, as [`Self::peek_block`] does.
798    #[cfg(feature = "columnar")]
799    #[must_use]
800    pub(crate) fn get_directory(
801        &self,
802        id: GlobalTableId,
803        offset: BlockOffset,
804        touch: bool,
805    ) -> Option<(
806        alloc::sync::Arc<crate::table::column_page::PageDirectory>,
807        u32,
808    )> {
809        let key: CacheKey = (TAG_DIRECTORY, id.tree_id(), id.table_id(), *offset).into();
810        let item = if touch {
811            self.data.get(&key)
812        } else {
813            self.data.peek(&key)
814        };
815        match item? {
816            Item::Directory(directory, len) => Some((directory, len)),
817            _ => unreachable!("invalid cache item"),
818        }
819    }
820
821    /// Caches the decoded directory of the row group at `offset`, with its
822    /// block's on-disk length. A directory is read before any page of its
823    /// group, like an index block before its data blocks, so it is pinned at
824    /// the same priority.
825    #[cfg(feature = "columnar")]
826    pub(crate) fn insert_directory(
827        &self,
828        id: GlobalTableId,
829        offset: BlockOffset,
830        directory: alloc::sync::Arc<crate::table::column_page::PageDirectory>,
831        len: u32,
832    ) {
833        let priority = if self.metadata_priority {
834            Priority::High
835        } else {
836            Priority::Normal
837        };
838        self.data.insert_with_priority(
839            (TAG_DIRECTORY, id.tree_id(), id.table_id(), *offset).into(),
840            Item::Directory(directory, len),
841            priority,
842        );
843    }
844
845    /// Whether a full (non-partial) data block is already resident for `offset`.
846    ///
847    /// The partial-tier reader uses this to bail out (let the normal cached path
848    /// serve) once a block has been promoted to a full resident block, and the
849    /// batched prewarm to tell a cold block from one it would re-read for
850    /// nothing. Neither wants the block itself, so this answers without cloning
851    /// it out and without counting a hit for a read that never happens.
852    #[doc(hidden)]
853    #[must_use]
854    pub fn has_block(&self, id: GlobalTableId, offset: BlockOffset) -> bool {
855        let key: CacheKey = (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into();
856        self.data.contains(&key)
857    }
858
859    /// Reads the cached partial-tier entry for `offset` (resume state + access
860    /// stats), without mutating it. The caller checks coverage against its query,
861    /// applies the promotion heuristic, then re-inserts with bumped stats, grows
862    /// the extent, or promotes to a full block.
863    #[cfg(feature = "zstd")]
864    #[doc(hidden)]
865    #[must_use]
866    pub fn peek_partial_block(
867        &self,
868        id: GlobalTableId,
869        offset: BlockOffset,
870    ) -> Option<PartialBlockEntry> {
871        let key: CacheKey = (TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into();
872        match self.data.peek(&key) {
873            Some(Item::PartialBlock(entry)) => Some(entry),
874            _ => None,
875        }
876    }
877
878    /// Records the partial-tier entry for `offset`, never letting its decoded
879    /// prefix shrink: when the resident entry already decoded more inner
880    /// blocks (a concurrent read grew it further), that one stays, with the
881    /// higher of the two hit counts. A read that served from an older copy of
882    /// the entry, or grew it less, can therefore not undo another read's
883    /// growth.
884    #[cfg(feature = "zstd")]
885    #[doc(hidden)]
886    pub fn insert_partial_block(
887        &self,
888        id: GlobalTableId,
889        offset: BlockOffset,
890        entry: PartialBlockEntry,
891    ) {
892        self.data.insert_merged(
893            (TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into(),
894            |resident| match resident {
895                Some(Item::PartialBlock(resident))
896                    if resident.resume.decoded_blocks > entry.resume.decoded_blocks =>
897                {
898                    Some(Item::PartialBlock(PartialBlockEntry {
899                        hits: resident.hits.max(entry.hits),
900                        ..resident.clone()
901                    }))
902                }
903                Some(Item::PartialBlock(resident)) => Some(Item::PartialBlock(PartialBlockEntry {
904                    hits: resident.hits.max(entry.hits),
905                    ..entry
906                })),
907                _ => Some(Item::PartialBlock(entry)),
908            },
909        );
910    }
911
912    /// Claims the growth of the partially decoded prefix of the block at
913    /// `offset`, so two reads that both need it longer decode the new inner
914    /// blocks once: the second waits for the first, then finds the longer
915    /// prefix cached.
916    #[cfg(feature = "zstd")]
917    pub(crate) fn claim_partial_growth(
918        &self,
919        id: GlobalTableId,
920        offset: BlockOffset,
921    ) -> GrowthClaim<'_> {
922        let key: CacheKey = (TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into();
923        match self.loads.claim(key, LoadIdentity::PartialGrowth) {
924            crate::inflight::Claim::Lead(lead) => GrowthClaim::Lead(GrowthLead(lead)),
925            crate::inflight::Claim::Join(join) if crate::inflight::may_wait() => {
926                let _ = join.wait();
927                GrowthClaim::Waited
928            }
929            crate::inflight::Claim::Join(_) | crate::inflight::Claim::Alone => GrowthClaim::Alone,
930        }
931    }
932
933    /// Drops the partial-tier entry for `offset` (used on promotion to a full
934    /// resident block, so the stale partial does not linger).
935    #[cfg(feature = "zstd")]
936    #[doc(hidden)]
937    pub fn evict_partial_block(&self, id: GlobalTableId, offset: BlockOffset) {
938        let key: CacheKey = (TAG_PARTIAL_BLOCK, id.tree_id(), id.table_id(), *offset).into();
939        self.data.remove(&key);
940    }
941
942    #[doc(hidden)]
943    pub fn insert_block(&self, id: GlobalTableId, offset: BlockOffset, block: Block) {
944        // Pin index / filter / range-tombstone blocks: they are touched on every
945        // seek (index descent + bloom check), so under data-block churn (working
946        // set >> cache) they must outlive the data blocks that would otherwise
947        // evict them and force a metadata re-read + re-decode on the next seek.
948        let priority = if self.metadata_priority
949            && matches!(
950                block.header.block_type,
951                BlockType::Index | BlockType::Filter | BlockType::RangeTombstone
952            ) {
953            Priority::High
954        } else {
955            Priority::Normal
956        };
957        self.data.insert_with_priority(
958            (TAG_BLOCK, id.tree_id(), id.table_id(), *offset).into(),
959            Item::Block(block),
960            priority,
961        );
962    }
963
964    /// Looks up the cached point-read result for `user_key` in SST `id`. The
965    /// stored key is verified against `user_key` so a hash collision on the
966    /// cache slot is rejected (returns `None`) rather than serving a wrong value.
967    /// `key_hash` is the same hash the bloom filter uses, so the caller passes
968    /// the value it already computed.
969    #[doc(hidden)]
970    #[must_use]
971    pub fn get_row(
972        &self,
973        id: GlobalTableId,
974        key_hash: u64,
975        user_key: &[u8],
976    ) -> Option<InternalValue> {
977        if !self.row_cache_enabled {
978            return None;
979        }
980        let key: CacheKey = (TAG_ROW, id.tree_id(), id.table_id(), key_hash).into();
981        match self.data.get(&key)? {
982            Item::Row(iv) if crate::comparator::same_user_key(&iv.key.user_key, user_key) => {
983                Some(iv)
984            }
985            // Hash collision (a different key hashed to this slot) or a foreign
986            // item kind: treat as a miss so the caller does the real lookup.
987            _ => None,
988        }
989    }
990
991    /// Caches the resolved point-read result (`user_key`, `seqno`,
992    /// `value_type`, `value`) for SST `id`, keyed by `key_hash`, and returns
993    /// the bytes it copied to own the row (zero when the row cache is off or
994    /// the row is too heavy to admit). Only a newest-version result (from a
995    /// latest-version read) should be inserted, so the seqno-visibility check
996    /// on lookup stays correct. SSTs are immutable, so an entry stays valid
997    /// until its SST is compacted away (after which its `table_id` is never
998    /// read again and the entry ages out of the cache).
999    #[doc(hidden)]
1000    #[must_use = "the bytes copied belong to the reading caller's gather count"]
1001    pub fn insert_row(
1002        &self,
1003        id: GlobalTableId,
1004        key_hash: u64,
1005        user_key: &[u8],
1006        seqno: crate::SeqNo,
1007        value_type: crate::ValueType,
1008        value: &[u8],
1009    ) -> usize {
1010        if !self.row_cache_enabled {
1011            return 0;
1012        }
1013        // The point-read path hands over a key and value that may be SUBSLICES
1014        // of the decoded data block, and a subslice keeps the whole block
1015        // allocation alive. The weigher charges a row its own key and value
1016        // bytes only, so a 100-byte row viewing a 4 KiB block would be
1017        // accounted as 100 bytes while holding 4096 — and a workload touching
1018        // one key per block, the one a row cache helps least, would overrun the
1019        // requested capacity by that ratio. Copying both out costs small
1020        // allocations per MISS, on a path that has just walked the index and
1021        // decoded a block, and it makes the charge equal to what is actually
1022        // retained.
1023        //
1024        // Unless the row cannot be admitted at all. A row heavier than one shard
1025        // is refused by the insert below, and a value that big is re-read as
1026        // often as any other, so copying it first would burn an allocation and a
1027        // memcpy on every one of those reads to produce something immediately
1028        // discarded. The copy does not change the weight, so projecting it here
1029        // is exact.
1030        if row_weight(user_key.len(), value.len()) > self.data.max_entry_weight() {
1031            return 0;
1032        }
1033        let iv = InternalValue {
1034            key: crate::key::InternalKey::new(crate::UserKey::from(user_key), seqno, value_type),
1035            value: crate::UserValue::from(value),
1036        };
1037        self.data.insert(
1038            (TAG_ROW, id.tree_id(), id.table_id(), key_hash).into(),
1039            Item::Row(iv),
1040        );
1041        user_key.len() + value.len()
1042    }
1043
1044    /// Caches a separated value under its position, together with the two
1045    /// fields the position does not carry (`user_key` and the handle's declared
1046    /// on-disk size) so a lookup can prove the entry belongs to the handle it
1047    /// is asked about. Returns the bytes copied to own the entry's key.
1048    #[doc(hidden)]
1049    #[must_use = "the bytes copied belong to the reading caller's gather count"]
1050    pub fn insert_blob(
1051        &self,
1052        vlog_id: crate::TreeId,
1053        vhandle: &crate::vlog::ValueHandle,
1054        user_key: &[u8],
1055        value: UserValue,
1056    ) -> usize {
1057        self.data.insert(
1058            (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into(),
1059            Item::Blob(user_key.into(), vhandle.on_disk_size, value),
1060        );
1061        user_key.len()
1062    }
1063
1064    /// Whether a separated value is already cached.
1065    ///
1066    /// For a caller deciding whether it has to read, not what the value is:
1067    /// [`get_blob`](Self::get_blob) would clone the value out (an atomic bump
1068    /// on the refcounted payload) and count a cache hit for a read that never
1069    /// happens, skewing the eviction policy toward entries nobody consumed.
1070    #[doc(hidden)]
1071    #[must_use]
1072    pub fn contains_blob(
1073        &self,
1074        vlog_id: crate::TreeId,
1075        vhandle: &crate::vlog::ValueHandle,
1076    ) -> bool {
1077        let key: CacheKey = (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into();
1078        self.data.contains(&key)
1079    }
1080
1081    /// The cached value for `vhandle`, but only if it belongs to that exact
1082    /// handle: same `user_key` and same declared on-disk size.
1083    ///
1084    /// The cache key is a POSITION in a blob file, and a corrupt index entry
1085    /// can name that position with a different key, or with the right key and
1086    /// the wrong size. The reader rejects both on a direct read, comparing what
1087    /// it was asked for against the record header; these comparisons are how
1088    /// the cached path reaches the same verdict instead of serving whatever
1089    /// sits at the offset. A mismatch reads as a miss, so the caller does the
1090    /// real read and gets the real error.
1091    #[doc(hidden)]
1092    #[must_use]
1093    pub fn get_blob(
1094        &self,
1095        vlog_id: crate::TreeId,
1096        vhandle: &crate::vlog::ValueHandle,
1097        user_key: &[u8],
1098    ) -> Option<UserValue> {
1099        let key: CacheKey = (TAG_BLOB, vlog_id, vhandle.blob_file_id, vhandle.offset).into();
1100
1101        match self.data.get(&key)? {
1102            Item::Blob(cached_key, cached_size, blob)
1103                if &*cached_key == user_key && cached_size == vhandle.on_disk_size =>
1104            {
1105                Some(blob)
1106            }
1107            Item::Blob(..) => None,
1108            Item::Block(_) | Item::Row(_) => unreachable!("invalid cache item"),
1109            #[cfg(feature = "zstd")]
1110            Item::PartialBlock(_) => unreachable!("invalid cache item"),
1111            #[cfg(feature = "columnar")]
1112            Item::Directory(..) => unreachable!("invalid cache item"),
1113        }
1114    }
1115}
1116
1117#[cfg(test)]
1118#[expect(clippy::expect_used, reason = "test code")]
1119mod tests;
1120
1121#[cfg(test)]
1122#[expect(clippy::expect_used, reason = "test code")]
1123mod coalesce_tests;