Skip to main content

lsm_db/
config.rs

1//! Engine configuration.
2//!
3//! [`LsmConfig`] is the Tier-2 tuning surface. The Tier-1 entry point
4//! [`Lsm::open`](crate::Lsm::open) uses [`LsmConfig::default`], so most callers
5//! never name this type. Reach for it when the default write-buffer size does
6//! not suit the workload.
7
8/// Default memtable capacity: 4 MiB of live key and value bytes.
9///
10/// Chosen as a balance for the foundation release — small enough that flushes
11/// stay cheap and predictable, large enough that bulk loads do not flush on
12/// every handful of writes. Tune with [`LsmConfig::memtable_capacity`].
13pub const DEFAULT_MEMTABLE_CAPACITY: usize = 4 * 1024 * 1024;
14
15/// Default number of on-disk runs that triggers a background compaction.
16///
17/// Each flush adds a run, and every point read may have to consult each run, so
18/// the run count bounds read amplification. When it reaches this many, the
19/// background compactor merges the runs into one. Tune with
20/// [`LsmConfig::compaction_trigger`].
21pub const DEFAULT_COMPACTION_TRIGGER: usize = 4;
22
23/// Default block-cache capacity: 8 MiB of decoded data blocks.
24///
25/// The cache holds recently-read run blocks so a repeat point lookup over a hot
26/// working set returns with no I/O, checksum, or parse. Set to `0` to disable
27/// it. Tune with [`LsmConfig::block_cache_capacity`].
28pub const DEFAULT_BLOCK_CACHE_CAPACITY: usize = 8 * 1024 * 1024;
29
30/// Tuning parameters for an [`Lsm`](crate::Lsm) engine.
31///
32/// Construct with [`LsmConfig::new`] (or [`LsmConfig::default`]) and refine with
33/// the chained setters, then pass to [`Lsm::open_with`](crate::Lsm::open_with).
34///
35/// # Examples
36///
37/// ```
38/// use lsm_db::LsmConfig;
39///
40/// // A 64 KiB write buffer — flushes often, keeps resident memory tiny.
41/// let config = LsmConfig::new().memtable_capacity(64 * 1024);
42/// assert_eq!(config.memtable_capacity_bytes(), 64 * 1024);
43/// ```
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct LsmConfig {
46    memtable_capacity: usize,
47    compaction_trigger: usize,
48    block_cache_capacity: usize,
49}
50
51impl LsmConfig {
52    /// Start from the default configuration.
53    ///
54    /// Equivalent to [`LsmConfig::default`]; provided so configuration reads as
55    /// a builder chain.
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use lsm_db::LsmConfig;
61    /// let config = LsmConfig::new();
62    /// assert_eq!(config, LsmConfig::default());
63    /// ```
64    #[inline]
65    #[must_use]
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Set the memtable capacity, in bytes of live key and value data.
71    ///
72    /// When the in-memory write buffer reaches this size, the next write
73    /// triggers a flush to disk. A capacity of `0` flushes after every write,
74    /// which is useful in tests but rarely otherwise.
75    ///
76    /// The figure counts key and value bytes only, not per-entry bookkeeping, so
77    /// peak resident memory is somewhat higher than the configured number.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use lsm_db::LsmConfig;
83    /// let config = LsmConfig::new().memtable_capacity(1 << 20); // 1 MiB
84    /// assert_eq!(config.memtable_capacity_bytes(), 1 << 20);
85    /// ```
86    #[inline]
87    #[must_use]
88    pub fn memtable_capacity(mut self, bytes: usize) -> Self {
89        self.memtable_capacity = bytes;
90        self
91    }
92
93    /// The configured memtable capacity, in bytes.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use lsm_db::LsmConfig;
99    /// assert_eq!(
100    ///     LsmConfig::default().memtable_capacity_bytes(),
101    ///     lsm_db::DEFAULT_MEMTABLE_CAPACITY,
102    /// );
103    /// ```
104    #[inline]
105    #[must_use]
106    pub fn memtable_capacity_bytes(&self) -> usize {
107        self.memtable_capacity
108    }
109
110    /// Set the number of on-disk runs that triggers a background compaction.
111    ///
112    /// Reads may consult every run, so this bounds read amplification: the
113    /// engine keeps at most roughly this many runs before merging them into one
114    /// in the background. Smaller values keep reads fast at the cost of more
115    /// compaction work; larger values do the reverse. Values below `2` are
116    /// treated as `2`, since merging a single run is pointless.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use lsm_db::LsmConfig;
122    /// let config = LsmConfig::new().compaction_trigger(8);
123    /// assert_eq!(config.compaction_trigger_runs(), 8);
124    /// ```
125    #[inline]
126    #[must_use]
127    pub fn compaction_trigger(mut self, runs: usize) -> Self {
128        self.compaction_trigger = runs.max(2);
129        self
130    }
131
132    /// The configured compaction trigger, in number of runs.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use lsm_db::LsmConfig;
138    /// assert_eq!(
139    ///     LsmConfig::default().compaction_trigger_runs(),
140    ///     lsm_db::DEFAULT_COMPACTION_TRIGGER,
141    /// );
142    /// ```
143    #[inline]
144    #[must_use]
145    pub fn compaction_trigger_runs(&self) -> usize {
146        self.compaction_trigger
147    }
148
149    /// Set the block-cache capacity, in bytes of decoded data blocks.
150    ///
151    /// The cache keeps recently-read run blocks so a repeat point lookup over a
152    /// hot working set returns with no I/O, checksum, or parse. Set to `0` to
153    /// disable it (every lookup decodes directly). The capacity is approximate —
154    /// it is counted in block-sized units — and shared across all of an engine's
155    /// runs.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use lsm_db::LsmConfig;
161    /// // A 32 MiB block cache.
162    /// let config = LsmConfig::new().block_cache_capacity(32 * 1024 * 1024);
163    /// assert_eq!(config.block_cache_capacity_bytes(), 32 * 1024 * 1024);
164    /// // Disable the cache.
165    /// assert_eq!(LsmConfig::new().block_cache_capacity(0).block_cache_capacity_bytes(), 0);
166    /// ```
167    #[inline]
168    #[must_use]
169    pub fn block_cache_capacity(mut self, bytes: usize) -> Self {
170        self.block_cache_capacity = bytes;
171        self
172    }
173
174    /// The configured block-cache capacity, in bytes.
175    ///
176    /// # Examples
177    ///
178    /// ```
179    /// use lsm_db::LsmConfig;
180    /// assert_eq!(
181    ///     LsmConfig::default().block_cache_capacity_bytes(),
182    ///     lsm_db::DEFAULT_BLOCK_CACHE_CAPACITY,
183    /// );
184    /// ```
185    #[inline]
186    #[must_use]
187    pub fn block_cache_capacity_bytes(&self) -> usize {
188        self.block_cache_capacity
189    }
190}
191
192impl Default for LsmConfig {
193    /// The default configuration: a [`DEFAULT_MEMTABLE_CAPACITY`] write buffer
194    /// and a [`DEFAULT_COMPACTION_TRIGGER`] run threshold.
195    fn default() -> Self {
196        LsmConfig {
197            memtable_capacity: DEFAULT_MEMTABLE_CAPACITY,
198            compaction_trigger: DEFAULT_COMPACTION_TRIGGER,
199            block_cache_capacity: DEFAULT_BLOCK_CACHE_CAPACITY,
200        }
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used, clippy::expect_used)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_default_capacity_is_documented_constant() {
211        assert_eq!(
212            LsmConfig::default().memtable_capacity_bytes(),
213            DEFAULT_MEMTABLE_CAPACITY
214        );
215    }
216
217    #[test]
218    fn test_builder_overrides_capacity() {
219        let c = LsmConfig::new().memtable_capacity(123);
220        assert_eq!(c.memtable_capacity_bytes(), 123);
221    }
222
223    #[test]
224    fn test_new_equals_default() {
225        assert_eq!(LsmConfig::new(), LsmConfig::default());
226    }
227
228    #[test]
229    fn test_default_compaction_trigger_is_documented_constant() {
230        assert_eq!(
231            LsmConfig::default().compaction_trigger_runs(),
232            DEFAULT_COMPACTION_TRIGGER
233        );
234    }
235
236    #[test]
237    fn test_compaction_trigger_override() {
238        assert_eq!(
239            LsmConfig::new()
240                .compaction_trigger(8)
241                .compaction_trigger_runs(),
242            8
243        );
244    }
245
246    #[test]
247    fn test_compaction_trigger_clamped_to_two() {
248        assert_eq!(
249            LsmConfig::new()
250                .compaction_trigger(0)
251                .compaction_trigger_runs(),
252            2
253        );
254        assert_eq!(
255            LsmConfig::new()
256                .compaction_trigger(1)
257                .compaction_trigger_runs(),
258            2
259        );
260    }
261}