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}