Skip to main content

lsm_tree/memtable/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Dmitry Prudnikov
4
5pub(crate) mod arena;
6pub(crate) mod interval_tree;
7pub(crate) mod own_line;
8use own_line::OwnLine;
9pub(crate) mod skiplist;
10pub(crate) mod value_store;
11
12use crate::comparator::SharedComparator;
13use crate::key::InternalKey;
14use crate::range_tombstone::RangeTombstone;
15use crate::{
16    UserKey,
17    value::{InternalValue, SeqNo},
18};
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21use core::ops::RangeBounds;
22use core::sync::atomic::AtomicBool;
23use portable_atomic::AtomicU64;
24// `parking_lot::RwLock` (std: small, userspace fast-path, no poisoning) /
25// `spin::RwLock` (no_std). Neither poisons on a panicked holder, so the read/
26// write guards are taken without a `LockResult` unwrap.
27#[cfg(feature = "std")]
28use parking_lot::RwLock;
29#[cfg(not(feature = "std"))]
30use spin::RwLock;
31
32pub use crate::tree::inner::MemtableId;
33
34/// The distinct keys among `keys`, in the table's key order, and the distinct
35/// hashes a table filter built over them holds: one per key and, with
36/// `prefixes`, one per prefix, as a full filter deduplicates them before it
37/// builds. Versions of a key add none.
38///
39/// Counted over all of `keys` at once: a flush rotating into several tables
40/// hashes a prefix spanning two of them in each, so the count runs short by
41/// at most one prefix per rotation.
42pub(crate) fn filter_count(
43    keys: impl Iterator<Item = crate::UserKey>,
44    prefixes: Option<&dyn crate::PrefixExtractor>,
45) -> crate::filter_budget::FilterCount {
46    let mut distinct = 0u64;
47    let mut previous: Option<crate::UserKey> = None;
48    // Without an extractor a filter holds one hash a key, and needs no list.
49    let mut hashes: Vec<u64> = Vec::new();
50    let mut previous_prefixes: Vec<u64> = Vec::new();
51    for key in keys {
52        if previous
53            .as_ref()
54            .is_some_and(|previous| crate::comparator::same_user_key(previous, &key))
55        {
56            continue;
57        }
58        distinct += 1;
59        if let Some(extractor) = prefixes {
60            hashes.push(crate::hash::hash64(&key));
61            // A prefix the key before had at the same position is a repeat,
62            // dropped here to keep the list short, as the writer does.
63            for (position, prefix) in extractor.prefixes(&key).enumerate() {
64                let hash = crate::hash::hash64(prefix);
65                match previous_prefixes.get_mut(position) {
66                    Some(previous) if *previous == hash => {}
67                    Some(previous) => {
68                        *previous = hash;
69                        hashes.push(hash);
70                    }
71                    None => {
72                        previous_prefixes.push(hash);
73                        hashes.push(hash);
74                    }
75                }
76            }
77        }
78        previous = Some(key);
79    }
80    let hashes = if prefixes.is_some() {
81        // Tokens equal to a key, or repeated out of position, the writer
82        // drops when it sorts its hashes.
83        hashes.sort_unstable();
84        hashes.dedup();
85        u64::try_from(hashes.len()).unwrap_or(u64::MAX)
86    } else {
87        distinct
88    };
89    crate::filter_budget::FilterCount {
90        keys: distinct,
91        hashes,
92    }
93}
94
95/// The memtable serves as an intermediary, ephemeral, sorted storage for new items
96///
97/// When the Memtable exceeds some size, it should be flushed to a table.
98pub struct Memtable {
99    #[doc(hidden)]
100    pub id: MemtableId,
101
102    /// The user key comparator used for ordering entries.
103    pub(crate) comparator: SharedComparator,
104
105    /// The actual content, stored in an arena-based skiplist with lock-free traversal.
106    ///
107    /// Nodes are allocated from a contiguous byte arena for cache locality
108    /// and O(1) bulk deallocation when the memtable is dropped.  Traversal of
109    /// the skiplist index uses atomic loads and CAS for inserts.
110    pub(crate) items: skiplist::SkipMap,
111
112    /// Range tombstones stored in an interval tree.
113    ///
114    /// Protected by `RwLock` — read-heavy suppression queries (`query_suppression`,
115    /// `range_tombstones_sorted`) take a shared read lock, while `insert_range_tombstone`
116    /// takes an exclusive write lock. After a rotation has been requested via
117    /// `requested_rotation`, the interval tree is treated as read-only by convention,
118    /// and only readers are expected to access this field (the `RwLock` is still used
119    /// for synchronization, but there should be no further writes).
120    ///
121    /// `std::sync::RwLock` may be reader-biased on some platforms, but writer
122    /// starvation is not a concern here: range deletes are rare, the write-side
123    /// critical section is O(log n) with n typically small, and the memtable
124    /// rotates (becoming read-only) well before contention could accumulate.
125    pub(crate) range_tombstones: RwLock<interval_tree::IntervalTree>,
126
127    /// Approximate active memtable size.
128    ///
129    /// If this grows too large, a flush is triggered. Updated with `Relaxed`
130    /// RMWs: it is a rotation heuristic, and nothing derives the visibility
131    /// of other memory from it (entry visibility comes from the skiplist's
132    /// release/acquire tower stores; `highest_seqno` keeps `AcqRel` so a
133    /// reader that observes a seqno also observes its entry).
134    ///
135    /// Each counter every insert writes sits on a line of its own, apart from
136    /// the fields reads and inserts only load.
137    pub(crate) approximate_size: OwnLine<AtomicU64>,
138
139    /// Highest encountered sequence number.
140    ///
141    /// This is used so that `get_highest_seqno` has O(1) complexity.
142    pub(crate) highest_seqno: OwnLine<AtomicU64>,
143
144    pub(crate) requested_rotation: AtomicBool,
145
146    /// Whether any insert-time per-KV digest (`KvChecksumComputePoint::AtInsert`)
147    /// has been stored in this memtable. Set on the first digest-bearing insert
148    /// and read once at flush by [`Self::verify_kv_residence`] to skip walking
149    /// the nodes entirely when there is nothing to verify (the default `Off` /
150    /// `AtBlockCompile` path). The per-node digest carries its own algorithm, so
151    /// no memtable-wide algorithm is tracked here.
152    has_at_insert_digests: AtomicBool,
153
154    /// Blob files the cell rows written here reference. Until this memtable
155    /// is flushed, no table links them for these rows, so a blob file listed
156    /// here is neither removed nor punched.
157    blob_refs: RwLock<crate::HashSet<crate::vlog::BlobFileId>>,
158}
159
160impl Memtable {
161    /// Returns the memtable ID.
162    pub fn id(&self) -> MemtableId {
163        self.id
164    }
165
166    /// Returns `true` if the memtable was already flagged for rotation.
167    pub fn is_flagged_for_rotation(&self) -> bool {
168        self.requested_rotation
169            .load(core::sync::atomic::Ordering::Relaxed)
170    }
171
172    /// Flags the memtable as requested for rotation.
173    pub fn flag_rotated(&self) {
174        self.requested_rotation
175            .store(true, core::sync::atomic::Ordering::Relaxed);
176    }
177
178    // `pub` + `#[doc(hidden)]`: used by the host crate (fjall) to construct
179    // ephemeral memtables. Not part of the semver-stable API.
180    // Keep the comparator by-value for hidden-public API compatibility while
181    // still requiring callers to pass the tree comparator explicitly.
182    #[doc(hidden)]
183    #[expect(
184        clippy::needless_pass_by_value,
185        reason = "hidden-public constructor keeps the preexisting by-value signature for compatibility"
186    )]
187    #[must_use]
188    pub fn new(id: MemtableId, comparator: SharedComparator) -> Self {
189        Self {
190            id,
191            items: skiplist::SkipMap::new(comparator.clone()),
192            comparator: comparator.clone(),
193            range_tombstones: RwLock::new(interval_tree::IntervalTree::new_with_comparator(
194                comparator.clone(),
195            )),
196            approximate_size: OwnLine(AtomicU64::default()),
197            highest_seqno: OwnLine(AtomicU64::default()),
198            requested_rotation: AtomicBool::default(),
199            has_at_insert_digests: AtomicBool::default(),
200            blob_refs: RwLock::new(crate::HashSet::default()),
201        }
202    }
203
204    /// Records the blob files `item` references when it is a cell row.
205    ///
206    /// Every insert path calls this, so a row replayed from an external log
207    /// registers its files like a row written through
208    /// [`BlobTree::insert_cells`](crate::BlobTree::insert_cells). A row that
209    /// does not decode registers nothing: it fails at flush or read, which
210    /// decode it again.
211    fn note_blob_refs(&self, item: &InternalValue) {
212        if !item.key.value_type.is_cell_row() {
213            return;
214        }
215        let Ok(refs) = crate::blob_tree::field_row::row_refs(&item.value) else {
216            return;
217        };
218        if refs.is_empty() {
219            return;
220        }
221        let mut set = self.blob_refs.write();
222        for (indirection, _) in refs {
223            set.insert(indirection.vhandle.blob_file_id);
224        }
225    }
226
227    /// Whether a cell row in this memtable references `blob_file_id`.
228    pub(crate) fn references_blob_file(&self, blob_file_id: crate::vlog::BlobFileId) -> bool {
229        self.blob_refs.read().contains(&blob_file_id)
230    }
231
232    /// Adds the blob files cell rows in this memtable reference to `files`.
233    pub(crate) fn collect_blob_files(&self, files: &mut crate::HashSet<crate::vlog::BlobFileId>) {
234        files.extend(self.blob_refs.read().iter().copied());
235    }
236
237    /// Creates an iterator over all items.
238    pub fn iter(&self) -> impl DoubleEndedIterator<Item = InternalValue> + '_ {
239        self.items.iter().map(|entry| InternalValue {
240            key: entry.key(),
241            value: entry.value(),
242        })
243    }
244
245    /// Creates an iterator over a range of items.
246    ///
247    /// Accepts `InternalKey`-based bounds.
248    pub(crate) fn range_internal<'a, R: RangeBounds<InternalKey> + 'a>(
249        &'a self,
250        range: R,
251    ) -> impl DoubleEndedIterator<Item = InternalValue> + 'a {
252        self.items.range(range).map(|entry| InternalValue {
253            key: entry.key(),
254            value: entry.value(),
255        })
256    }
257
258    /// Returns the item by key if it exists.
259    ///
260    /// Returns the version with the highest seqno that is strictly less than
261    /// the given `seqno`.  Pass [`MAX_SEQNO`](crate::MAX_SEQNO) to retrieve the latest version.
262    #[doc(hidden)]
263    pub fn get(&self, key: &[u8], seqno: SeqNo) -> Option<InternalValue> {
264        if seqno == 0 {
265            return None;
266        }
267
268        // NOTE: This range start deserves some explanation...
269        // InternalKeys are multi-sorted by 2 categories: user_key and Reverse(seqno). (tombstone doesn't really matter)
270        // We search for the lowest entry that is greater or equal the user's prefix key
271        // and has the seqno (or lower) we want (because the seqno is stored in reverse order)
272        //
273        // Example: We search for "abc"
274        //
275        // key -> seqno
276        //
277        // a   -> 7
278        // abc -> 5 <<< This is the lowest key (highest seqno) that matches the key with seqno=MAX
279        // abc -> 4
280        // abc -> 3 <<< If searching for abc and seqno=4, we would get this
281        // abcdef -> 6
282        // abcdef -> 5
283        //
284        // Borrowed-parts point lookup: no `InternalKey` bound is built, so the
285        // probe key is never copied (the old `range(lower_bound..)` path
286        // heap-copied `key` into a fresh bound per probed memtable).
287        self.items.point_get(key, seqno - 1)
288    }
289
290    /// Gets approximate size of memtable in bytes.
291    pub fn size(&self) -> u64 {
292        self.approximate_size
293            .load(core::sync::atomic::Ordering::Acquire)
294    }
295
296    /// Counts the number of items in the memtable.
297    pub fn len(&self) -> usize {
298        self.items.len()
299    }
300
301    /// How often an insert had to retry linking an item because a concurrent
302    /// insert changed its neighbour first: the contention writers meet in
303    /// this memtable, counted once per retried level of the skiplist.
304    #[cfg(feature = "metrics")]
305    #[must_use]
306    pub fn insert_cas_retries(&self) -> usize {
307        self.items.cas_retries()
308    }
309
310    /// Returns `true` if the memtable has no KV items and no range tombstones.
311    #[must_use]
312    pub fn is_empty(&self) -> bool {
313        self.items.is_empty() && self.range_tombstone_count() == 0
314    }
315
316    /// Inserts multiple items into the memtable in bulk.
317    ///
318    /// More efficient than calling [`Memtable::insert`] in a loop because it
319    /// performs a single `fetch_add` for the total size and a single
320    /// `fetch_max` for the highest seqno.
321    ///
322    /// Returns `(total_bytes_added, new_memtable_size)`.
323    #[doc(hidden)]
324    pub fn insert_batch(&self, items: Vec<InternalValue>) -> (u64, u64) {
325        self.insert_batch_with_kv_algo(items, None)
326    }
327
328    /// Bulk insert, optionally computing an insert-time per-KV digest per item
329    /// under `kv_algo` (`KvChecksumComputePoint::AtInsert`).
330    ///
331    /// `kv_algo` is `Some(algo)` (a 4-byte algorithm) to fix each entry's
332    /// digest at insert for the flush-time residence check, or `None` for the
333    /// plain bulk path. Same single-`fetch_add` / single-`fetch_max` accounting
334    /// as [`Self::insert_batch`].
335    #[doc(hidden)]
336    pub fn insert_batch_with_kv_algo(
337        &self,
338        items: Vec<InternalValue>,
339        kv_algo: Option<crate::runtime_config::ChecksumAlgorithm>,
340    ) -> (u64, u64) {
341        if items.is_empty() {
342            let size = self
343                .approximate_size
344                .load(core::sync::atomic::Ordering::Acquire);
345            return (0, size);
346        }
347
348        let mut total_size: u64 = 0;
349        let mut max_seqno: u64 = 0;
350
351        let overhead =
352            core::mem::size_of::<InternalValue>() + core::mem::size_of::<SharedComparator>();
353
354        for item in &items {
355            #[expect(
356                clippy::expect_used,
357                reason = "keys are limited to 16-bit length + values are limited to 32-bit length"
358            )]
359            let item_size: u64 = (item.key.user_key.len() + item.value.len() + overhead)
360                .try_into()
361                .expect("should fit into u64");
362
363            // Running memtable byte total, bounded by the in-memory data size;
364            // a plain add cannot overflow u64.
365            total_size += item_size;
366
367            if item.key.seqno > max_seqno {
368                max_seqno = item.key.seqno;
369            }
370        }
371
372        let size_before = self
373            .approximate_size
374            .fetch_add(total_size, core::sync::atomic::Ordering::Relaxed);
375
376        if kv_algo.is_some() {
377            // Flag that this memtable carries residence digests for the
378            // flush-time verify. The algorithm lives per node.
379            self.has_at_insert_digests
380                .store(true, core::sync::atomic::Ordering::Relaxed);
381        }
382
383        for item in items {
384            self.note_blob_refs(&item);
385            let digest = kv_algo.and_then(|algo| {
386                crate::table::block::kv_checksum::kv_digest(&item, algo).map(|d| {
387                    #[expect(
388                        clippy::cast_possible_truncation,
389                        reason = "AtInsert is config-validated to a 4-byte algorithm; the digest fits u32"
390                    )]
391                    let lo = d as u32;
392                    (lo, algo)
393                })
394            });
395            self.items
396                .insert_with_kv_digest(&item.key, item.value, digest);
397        }
398
399        self.highest_seqno
400            .fetch_max(max_seqno, core::sync::atomic::Ordering::AcqRel);
401
402        // fetch_add returns value BEFORE the add, so size_before + total_size
403        // = value AFTER add = new memtable size. Same pattern as Memtable::insert().
404        (total_size, size_before + total_size)
405    }
406
407    /// Inserts an item into the memtable
408    #[doc(hidden)]
409    pub fn insert(&self, item: InternalValue) -> (u64, u64) {
410        #[expect(
411            clippy::expect_used,
412            reason = "keys are limited to 16-bit length + values are limited to 32-bit length"
413        )]
414        // Account for MemtableKey overhead (InternalKey + Arc<dyn UserComparator>)
415        let item_size = (item.key.user_key.len()
416            + item.value.len()
417            + core::mem::size_of::<InternalValue>()
418            + core::mem::size_of::<SharedComparator>())
419        .try_into()
420        .expect("should fit into u64");
421
422        let size_before = self
423            .approximate_size
424            .fetch_add(item_size, core::sync::atomic::Ordering::Relaxed);
425
426        self.note_blob_refs(&item);
427
428        // `item.key` is borrowed and `item.value` moved: the key bytes are
429        // copied into the arena, the value handle is stored as-is.
430        self.items.insert(&item.key, item.value);
431
432        self.highest_seqno
433            .fetch_max(item.key.seqno, core::sync::atomic::Ordering::AcqRel);
434
435        (item_size, size_before + item_size)
436    }
437
438    /// Inserts an item, optionally carrying a precomputed insert-time per-KV
439    /// digest (`KvChecksumComputePoint::AtInsert`).
440    ///
441    /// `kv_digest` is `Some((digest, algo))` when the caller computed the
442    /// entry's 4-byte logical-content digest at insert (under `AtInsert` with a
443    /// 4-byte algorithm), or `None` for the plain path. When present, the digest
444    /// and its algorithm are stored in the skiplist node (per node, so a later
445    /// config change cannot misverify it) and the memtable flags that it carries
446    /// at least one digest so [`Self::verify_kv_residence`] knows to walk at
447    /// flush. Mixed inserts (some with, some without a digest) are supported for
448    /// the `Off` -> `AtInsert` live toggle.
449    #[doc(hidden)]
450    pub fn insert_with_kv_digest(
451        &self,
452        item: InternalValue,
453        kv_digest: Option<(u32, crate::runtime_config::ChecksumAlgorithm)>,
454    ) -> (u64, u64) {
455        #[expect(
456            clippy::expect_used,
457            reason = "keys are limited to 16-bit length + values are limited to 32-bit length"
458        )]
459        let item_size = (item.key.user_key.len()
460            + item.value.len()
461            + core::mem::size_of::<InternalValue>()
462            + core::mem::size_of::<SharedComparator>())
463        .try_into()
464        .expect("should fit into u64");
465
466        let size_before = self
467            .approximate_size
468            .fetch_add(item_size, core::sync::atomic::Ordering::Relaxed);
469
470        if kv_digest.is_some() {
471            // Flag that this memtable carries at least one residence digest so
472            // the flush-time verify walks the nodes. The algorithm lives per
473            // node, not here.
474            self.has_at_insert_digests
475                .store(true, core::sync::atomic::Ordering::Relaxed);
476        }
477
478        self.note_blob_refs(&item);
479
480        self.items
481            .insert_with_kv_digest(&item.key, item.value, kv_digest);
482
483        self.highest_seqno
484            .fetch_max(item.key.seqno, core::sync::atomic::Ordering::AcqRel);
485
486        (item_size, size_before + item_size)
487    }
488
489    /// Verifies every insert-time per-KV digest in this memtable against a
490    /// recompute over the entry's current bytes (the
491    /// [`KvChecksumComputePoint::AtInsert`](crate::runtime_config::KvChecksumComputePoint::AtInsert)
492    /// residence check), called once at flush.
493    ///
494    /// Returns `Ok` immediately when no `AtInsert` digest was ever inserted, so
495    /// the default path pays nothing.
496    ///
497    /// # Errors
498    ///
499    /// - [`crate::Error::MemtableKvChecksumMismatch`] when an entry's stored
500    ///   digest diverges from the recompute (a RAM bit-flip during residence).
501    /// - [`crate::Error::FeatureUnsupported`] when a node's algorithm is not
502    ///   compiled into this build.
503    pub fn verify_kv_residence(&self) -> crate::Result<()> {
504        if !self
505            .has_at_insert_digests
506            .load(core::sync::atomic::Ordering::Relaxed)
507        {
508            return Ok(());
509        }
510        self.items.verify_kv_digests()
511    }
512
513    /// Inserts a range tombstone covering `[start, end)` at the given seqno.
514    ///
515    /// Returns the approximate size added to the memtable.
516    ///
517    /// Returns 0 if `start >= end` or if either bound exceeds `u16::MAX` bytes.
518    ///
519    /// # Panics
520    ///
521    /// Panics if the internal `RwLock` is poisoned.
522    #[must_use]
523    pub fn insert_range_tombstone(&self, start: UserKey, end: UserKey, seqno: SeqNo) -> u64 {
524        // flag_rotated() (which sets requested_rotation) is called by the host
525        // crate (fjall) before rotation; this crate never sets it directly.
526        // The assert catches misuse by callers
527        // in debug builds — intentionally debug-only because post-rotation writes
528        // are structurally prevented by the host (sealed memtables are behind Arc
529        // with no write path exposed), and an atomic load here would add overhead
530        // on the hot insert path in release builds for no practical benefit.
531        debug_assert!(
532            !self.is_flagged_for_rotation(),
533            "insert_range_tombstone called after memtable was flagged for rotation"
534        );
535
536        // Reject invalid intervals in release builds (debug_assert is not enough)
537        if self.comparator.compare(&start, &end) != core::cmp::Ordering::Less {
538            return 0;
539        }
540
541        // On-disk RT format writes key lengths as u16, enforce at insertion time.
542        // Emit a warning when rejecting an oversized bound so this failure is diagnosable.
543        if u16::try_from(start.len()).is_err() || u16::try_from(end.len()).is_err() {
544            log::warn!(
545                "insert_range_tombstone: rejecting oversized range tombstone \
546                 bounds (start_len = {}, end_len = {}, max = {})",
547                start.len(),
548                end.len(),
549                u16::MAX,
550            );
551            return 0;
552        }
553
554        let size = (start.len() + end.len() + core::mem::size_of::<RangeTombstone>()) as u64;
555
556        self.range_tombstones
557            .write()
558            .insert(RangeTombstone::new(start, end, seqno));
559
560        self.approximate_size
561            .fetch_add(size, core::sync::atomic::Ordering::AcqRel);
562
563        self.highest_seqno
564            .fetch_max(seqno, core::sync::atomic::Ordering::AcqRel);
565
566        size
567    }
568
569    /// Returns `true` if the key at `key_seqno` is suppressed by a range tombstone
570    /// visible at `read_seqno`.
571    pub(crate) fn is_key_suppressed_by_range_tombstone(
572        &self,
573        key: &[u8],
574        key_seqno: SeqNo,
575        read_seqno: SeqNo,
576    ) -> bool {
577        self.range_tombstones
578            .read()
579            .query_suppression(key, key_seqno, read_seqno)
580    }
581
582    /// Hands `visit` the range tombstones holding `key`.
583    pub(crate) fn for_each_range_tombstone_containing(
584        &self,
585        key: &[u8],
586        visit: impl FnMut(&RangeTombstone),
587    ) {
588        self.range_tombstones.read().for_each_containing(key, visit);
589    }
590
591    /// Returns all range tombstones in sorted order (for flush).
592    pub(crate) fn range_tombstones_sorted(&self) -> Vec<RangeTombstone> {
593        self.range_tombstones.read().iter_sorted()
594    }
595
596    /// Returns the number of range tombstones.
597    #[must_use]
598    pub fn range_tombstone_count(&self) -> usize {
599        self.range_tombstones.read().len()
600    }
601
602    /// Returns the highest sequence number in the memtable.
603    pub fn get_highest_seqno(&self) -> Option<SeqNo> {
604        if self.is_empty() {
605            None
606        } else {
607            Some(
608                self.highest_seqno
609                    .load(core::sync::atomic::Ordering::Acquire),
610            )
611        }
612    }
613}
614
615#[cfg(test)]
616mod tests;