Skip to main content

lsm_tree/
pinnable_slice.rs

1// Copyright (c) 2024-present, fjall-rs
2// This source code is licensed under both the Apache 2.0 and MIT License
3// (found in the LICENSE-* files in the repository)
4
5//! Zero-copy value reference that keeps the decompressed block buffer alive.
6//!
7//! [`PinnableSlice`] is inspired by `RocksDB`'s `PinnableSlice`
8//! (`include/rocksdb/slice.h:179-263`). It wraps a value that was read from
9//! the LSM tree and indicates whether the underlying data shares the
10//! decompressed block buffer or is independently owned (e.g. from a memtable
11//! or merge result).
12//!
13//! When the value comes from an on-disk data block, holding a
14//! `PinnableSlice::Pinned` keeps the block's decompressed buffer alive
15//! (via the refcounted [`Slice`] / `ByteView` backing) for the duration of
16//! the reference. The value bytes are a sub-slice of that buffer — no copy
17//! is performed. Note: this does **not** prevent the block cache from
18//! evicting its entry; it only ensures the backing memory remains valid.
19//!
20//! Memtable and blob-resolved values use the `Owned` variant.
21
22use crate::{Slice, UserValue, table::Block};
23
24/// A value reference that may share the decompressed block buffer.
25///
26/// Use [`PinnableSlice::as_ref`] to access the raw bytes regardless of variant.
27///
28/// # Lifetime
29///
30/// The `Pinned` variant holds a [`Block`] clone whose `data` field is a
31/// refcounted [`Slice`]. As long as the `PinnableSlice` is alive, the
32/// decompressed block buffer remains valid. Dropping it releases the
33/// reference count on the underlying `ByteView` allocation.
34#[derive(Clone)]
35pub enum PinnableSlice {
36    /// Value sharing the decompressed block buffer — zero copy.
37    ///
38    /// The [`Block`] keeps the decompressed data alive via refcounted
39    /// `Slice` / `ByteView`. `value` is a sub-slice created via
40    /// [`Slice::slice`], sharing the same backing allocation.
41    Pinned {
42        /// Keeps the decompressed block buffer alive via refcount.
43        _block: Block,
44        /// Zero-copy sub-slice into the block's decompressed data.
45        value: Slice,
46    },
47
48    /// Value owned independently (memtable, blob, merge result).
49    Owned(UserValue),
50}
51
52impl PinnableSlice {
53    /// Creates a pinned value sharing the decompressed block buffer.
54    #[must_use]
55    pub fn pinned(block: Block, value: Slice) -> Self {
56        Self::Pinned {
57            _block: block,
58            value,
59        }
60    }
61
62    /// Creates an owned value (not sharing any block buffer).
63    #[must_use]
64    pub fn owned(value: UserValue) -> Self {
65        Self::Owned(value)
66    }
67
68    /// Returns `true` if this value shares the decompressed block buffer.
69    #[must_use]
70    pub fn is_pinned(&self) -> bool {
71        matches!(self, Self::Pinned { .. })
72    }
73
74    /// Returns the raw value bytes.
75    #[must_use]
76    pub fn value(&self) -> &[u8] {
77        self.as_ref()
78    }
79
80    /// Returns the length of the value in bytes.
81    #[must_use]
82    pub fn len(&self) -> usize {
83        self.as_ref().len()
84    }
85
86    /// Returns `true` if the value is empty.
87    #[must_use]
88    pub fn is_empty(&self) -> bool {
89        self.as_ref().is_empty()
90    }
91
92    /// Converts this `PinnableSlice` into an owned `UserValue`.
93    ///
94    /// For the `Pinned` variant, the `Block` is dropped but the returned
95    /// `Slice` still shares the same `ByteView` backing allocation.
96    /// For the `Owned` variant, the value is returned directly.
97    #[must_use]
98    pub fn into_value(self) -> UserValue {
99        match self {
100            Self::Pinned { value, .. } => value,
101            Self::Owned(v) => v,
102        }
103    }
104}
105
106impl std::fmt::Debug for PinnableSlice {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Self::Pinned { value, .. } => {
110                f.debug_struct("Pinned").field("len", &value.len()).finish()
111            }
112            Self::Owned(v) => f.debug_tuple("Owned").field(&v.len()).finish(),
113        }
114    }
115}
116
117impl AsRef<[u8]> for PinnableSlice {
118    fn as_ref(&self) -> &[u8] {
119        match self {
120            Self::Pinned { value, .. } => value.as_ref(),
121            Self::Owned(v) => v.as_ref(),
122        }
123    }
124}
125
126impl PartialEq<[u8]> for PinnableSlice {
127    fn eq(&self, other: &[u8]) -> bool {
128        self.as_ref() == other
129    }
130}
131
132impl PartialEq<&[u8]> for PinnableSlice {
133    fn eq(&self, other: &&[u8]) -> bool {
134        self.as_ref() == *other
135    }
136}
137
138impl From<PinnableSlice> for UserValue {
139    fn from(ps: PinnableSlice) -> Self {
140        ps.into_value()
141    }
142}