Skip to main content

erigon_seg/
reader.rs

1//! High-level reader over a `.kv` + sibling `.bt` + `.kvei` triple.
2//!
3//! [`KvReader`] is the main entry point: it opens the data file and, when present, its
4//! B-tree index and existence filter, then answers point lookups ([`get`](KvReader::get))
5//! and sequential scans ([`iter`](KvReader::iter)).
6
7use std::path::Path;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use crate::bloom::ExistenceFilter;
11use crate::btree::BtreeIndex;
12use crate::error::Result;
13use crate::hash::murmur3_x64_128_h1;
14use crate::salt::Salt;
15use crate::seg::{Getter, OpenOptions, Seg};
16
17/// A reader over one seg file set (`.kv` data, optional `.bt` index, optional `.kvei`
18/// existence filter).
19pub struct KvReader {
20    seg: Seg,
21    index: Option<BtreeIndex>,
22    bloom: Option<ExistenceFilter>,
23    /// Active salt: `Some` only once a bloom has been validated via [`enable_bloom`].
24    salt: Option<u32>,
25    /// The `.kv` file's base name (e.g. `v1.1-accounts.0-1024.kv`), for display.
26    name: String,
27}
28
29impl KvReader {
30    /// Open a `.kv` file and any sibling `.bt` / `.kvei` files found next to it (same
31    /// base name). The existence filter is loaded but not used for lookups until a salt
32    /// is supplied via [`enable_bloom`](KvReader::enable_bloom).
33    pub fn open(kv_path: impl AsRef<Path>) -> Result<KvReader> {
34        KvReader::open_with(kv_path, OpenOptions::default())
35    }
36
37    /// Like [`open`](KvReader::open) but with explicit seg [`OpenOptions`] (e.g. for
38    /// files carrying out-of-band metadata).
39    pub fn open_with(kv_path: impl AsRef<Path>, opts: OpenOptions) -> Result<KvReader> {
40        let kv_path = kv_path.as_ref();
41        let seg = Seg::open_with(kv_path, opts)?;
42
43        let bt_path = kv_path.with_extension("bt");
44        let index = if bt_path.exists() {
45            Some(BtreeIndex::open(&bt_path)?)
46        } else {
47            None
48        };
49
50        let kvei_path = kv_path.with_extension("kvei");
51        let bloom = if kvei_path.exists() {
52            Some(ExistenceFilter::open(&kvei_path)?)
53        } else {
54            None
55        };
56
57        let name = kv_path
58            .file_name()
59            .map(|s| s.to_string_lossy().into_owned())
60            .unwrap_or_default();
61
62        Ok(KvReader {
63            seg,
64            index,
65            bloom,
66            salt: None,
67            name,
68        })
69    }
70
71    /// The `.kv` file's base name (e.g. `v1.1-accounts.0-1024.kv`).
72    pub fn name(&self) -> &str {
73        &self.name
74    }
75
76    /// Whether the bloom filter is active for lookups — i.e. a `.kvei` is present and a
77    /// salt has been validated against real keys via [`enable_bloom`](KvReader::enable_bloom).
78    pub fn bloom_active(&self) -> bool {
79        self.salt.is_some()
80    }
81
82    /// The underlying seg data file.
83    pub fn seg(&self) -> &Seg {
84        &self.seg
85    }
86
87    /// The B-tree index, if a `.bt` was found.
88    pub fn index(&self) -> Option<&BtreeIndex> {
89        self.index.as_ref()
90    }
91
92    /// The existence filter, if a `.kvei` was found.
93    pub fn existence_filter(&self) -> Option<&ExistenceFilter> {
94        self.bloom.as_ref()
95    }
96
97    /// The active bloom salt, if [`enable_bloom`](KvReader::enable_bloom) has succeeded.
98    pub fn salt(&self) -> Option<u32> {
99        self.salt
100    }
101
102    /// Number of keys: from the `.bt` index if present, otherwise inferred as
103    /// `words_count / 2` (domain files store alternating key/value words).
104    pub fn key_count(&self) -> u64 {
105        match &self.index {
106            Some(idx) => idx.key_count(),
107            None => self.seg.words_count() / 2,
108        }
109    }
110
111    /// Enable the `.kvei` bloom as a negative-lookup accelerator, resolving the salt per
112    /// [`Salt`]. Returns `true` only if a usable bloom is present and the resolved salt
113    /// self-validates against real keys (so a wrong salt can never cause a missed key —
114    /// it just leaves lookups unaccelerated).
115    pub fn enable_bloom(&mut self, salt: Salt) -> bool {
116        let Some(bloom) = &self.bloom else {
117            return false;
118        };
119        if !bloom.is_accelerating() {
120            return false;
121        }
122        let resolved = match salt {
123            Salt::None => return false,
124            Salt::Known(s) => s,
125            Salt::Find(threads) => match self.find_salt(threads) {
126                Some(s) => s,
127                None => return false,
128            },
129        };
130        let samples = self.sample_keys(64);
131        if samples.is_empty() {
132            return false;
133        }
134        let ok = samples
135            .iter()
136            .all(|k| bloom.contains_hash(murmur3_x64_128_h1(k, resolved)));
137        if ok {
138            self.salt = Some(resolved);
139        }
140        ok
141    }
142
143    /// Brute-force the bloom salt by requiring a batch of real keys to all hit the
144    /// filter, using `threads` workers. Returns `None` if no `.kvei` bloom is usable or
145    /// no salt validates (e.g. a fuse-filter or format mismatch).
146    pub fn find_salt(&self, threads: usize) -> Option<u32> {
147        let bloom = self.bloom.as_ref()?;
148        if !bloom.is_accelerating() {
149            return None;
150        }
151        let samples = self.sample_keys(16);
152        if samples.is_empty() {
153            return None;
154        }
155        let threads = threads.clamp(1, 256) as u32;
156        let found = AtomicU64::new(u64::MAX);
157        std::thread::scope(|sc| {
158            for t in 0..threads {
159                let (found, bloom, samples) = (&found, bloom, &samples);
160                sc.spawn(move || {
161                    let mut salt = t;
162                    loop {
163                        if found.load(Ordering::Relaxed) != u64::MAX {
164                            return;
165                        }
166                        if samples
167                            .iter()
168                            .all(|k| bloom.contains_hash(murmur3_x64_128_h1(k, salt)))
169                        {
170                            found.fetch_min(salt as u64, Ordering::Relaxed);
171                            return;
172                        }
173                        match salt.checked_add(threads) {
174                            Some(s) => salt = s,
175                            None => return,
176                        }
177                    }
178                });
179            }
180        });
181        let f = found.load(Ordering::Relaxed);
182        (f != u64::MAX).then_some(f as u32)
183    }
184
185    /// Advise the kernel that this file set is read by point lookup, so a page fault
186    /// should read one page instead of a read-ahead window.
187    ///
188    /// A binary search touches a handful of scattered pages, and the kernel's default
189    /// fault-around then reads far more than is used — on a file much larger than RAM
190    /// that read amplification dominates lookup latency. This is *not* the default,
191    /// because suppressing read-ahead is a regression for a file small enough to sit in
192    /// the page cache, where the surplus pages get used by later lookups anyway. Set it
193    /// when the data is large relative to RAM; leave it alone otherwise.
194    ///
195    /// Advice is a hint: errors are reported but ignoring them is safe, and on platforms
196    /// without `madvise` this does nothing.
197    pub fn advise_random(&self) -> std::io::Result<()> {
198        self.seg.advise_random()?;
199        if let Some(idx) = &self.index {
200            idx.advise_random()?;
201        }
202        if let Some(bloom) = &self.bloom {
203            bloom.advise_random()?;
204        }
205        Ok(())
206    }
207
208    /// Advise the kernel that this `.kv` is about to be read front to back — before an
209    /// [`iter`](KvReader::iter) or a merge — so read-ahead works in your favour.
210    pub fn advise_sequential(&self) -> std::io::Result<()> {
211        self.seg.advise_sequential()
212    }
213
214    /// Bytes [`preload_index`](KvReader::preload_index) would make resident: the `.bt`,
215    /// plus the `.kvei` when the bloom is active. Use it to budget before calling.
216    pub fn index_bytes(&self) -> u64 {
217        let bt = self.index.as_ref().map_or(0, |i| i.mapped_bytes());
218        let kvei = match (&self.bloom, self.salt) {
219            (Some(b), Some(_)) => b.mapped_bytes(),
220            _ => 0,
221        };
222        bt + kvei
223    }
224
225    /// Read the index files into the page cache and return once they are resident,
226    /// reporting how many bytes were loaded.
227    ///
228    /// A point lookup touches the `.bt` far more than the `.kv` — every search
229    /// comparison reads the Elias-Fano offset array, while only the final block of keys
230    /// is decompressed — and the `.bt` is one to two orders of magnitude smaller. On a
231    /// machine with RAM to spare, holding the whole index resident removes nearly all
232    /// the remaining faults: on a 37 GiB file set with a 1.4 GiB `.bt`, cold lookups
233    /// went from ~440 µs to ~155 µs, for a one-off ~0.7 s load.
234    ///
235    /// The `.kvei` is included only when the bloom is active, since otherwise it is
236    /// never read. Loading is a hint to the kernel, not a reservation: these pages can
237    /// still be evicted under memory pressure — see [`lock_index`](KvReader::lock_index)
238    /// to prevent that.
239    pub fn preload_index(&self) -> u64 {
240        let mut n = 0;
241        if let Some(idx) = &self.index {
242            n += idx.preload();
243        }
244        if self.salt.is_some()
245            && let Some(bloom) = &self.bloom
246        {
247            n += bloom.preload();
248        }
249        n
250    }
251
252    /// Pin the index files in RAM with `mlock`, so the kernel cannot evict them.
253    ///
254    /// Stronger than [`preload_index`](KvReader::preload_index), and worth it when a
255    /// large `.kv` is streaming through the page cache and would otherwise push the
256    /// index back out. Same file selection: the `.bt`, plus the `.kvei` when the bloom
257    /// is active.
258    ///
259    /// Fails with `ENOMEM` (or `EPERM`) if the total exceeds `RLIMIT_MEMLOCK`, which is
260    /// commonly a few megabytes by default; check [`index_bytes`](KvReader::index_bytes)
261    /// against `ulimit -l` first. A failure is safe to ignore — it just leaves the pages
262    /// evictable — but note the limit applies per process across every locked mapping.
263    ///
264    /// Unix only: elsewhere this reports [`ErrorKind::Unsupported`] rather than quietly
265    /// doing nothing, since the point of the call is a guarantee.
266    /// [`preload_index`](KvReader::preload_index) still works everywhere.
267    ///
268    /// [`ErrorKind::Unsupported`]: std::io::ErrorKind::Unsupported
269    ///
270    /// Preloads first: `mlock` faults the pages in itself, but page-at-a-time, so
271    /// warming them sequentially beforehand is markedly faster.
272    pub fn lock_index(&self) -> std::io::Result<()> {
273        self.preload_index();
274        if let Some(idx) = &self.index {
275            idx.lock()?;
276        }
277        if self.salt.is_some()
278            && let Some(bloom) = &self.bloom
279        {
280            bloom.lock()?;
281        }
282        Ok(())
283    }
284
285    /// Release the pages pinned by [`lock_index`](KvReader::lock_index).
286    pub fn unlock_index(&self) -> std::io::Result<()> {
287        if let Some(idx) = &self.index {
288            idx.unlock()?;
289        }
290        if let Some(bloom) = &self.bloom {
291            bloom.unlock()?;
292        }
293        Ok(())
294    }
295
296    /// Look up `key`, returning its value if present.
297    ///
298    /// Uses, in order: the bloom filter for a fast definite-absent answer (if enabled),
299    /// then the `.bt` index for an `O(log n)` binary search, or — if there is no index —
300    /// an ordered linear scan.
301    pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
302        Ok(self.get_hashed(key, None))
303    }
304
305    /// [`get`](KvReader::get) with the bloom hash supplied by the caller.
306    ///
307    /// `key_hash` must be `murmur3_x64_128_h1(key, salt)` for *this* reader's active
308    /// salt; pass `None` to compute it here. [`KvStack`](crate::KvStack) uses this to
309    /// hash a key once and reuse it across every file, since one salt covers the stack.
310    pub(crate) fn get_hashed(&self, key: &[u8], key_hash: Option<u64>) -> Option<Vec<u8>> {
311        // Fast negative.
312        if let Some(salt) = self.salt
313            && let Some(bloom) = &self.bloom
314            && !bloom.contains_hash(key_hash.unwrap_or_else(|| murmur3_x64_128_h1(key, salt)))
315        {
316            return None;
317        }
318        match &self.index {
319            Some(idx) => self.get_indexed(idx, key),
320            None => self.get_scan(key),
321        }
322    }
323
324    fn get_indexed(&self, idx: &BtreeIndex, key: &[u8]) -> Option<Vec<u8>> {
325        if idx.key_count() == 0 {
326            return None;
327        }
328        // The `.bt` di-nodes cut the search to the one M-key block that can hold `key`,
329        // using in-memory comparisons; without them this is the full range.
330        let (mut lo, mut hi) = idx.narrow(key);
331        let mut g = self.seg.getter();
332        while lo < hi {
333            let mid = lo + (hi - lo) / 2;
334            let off = idx.key_offset(mid)?;
335            g.reset(off);
336            if !g.has_next() {
337                return None;
338            }
339            let probe = g.next();
340            match probe.as_slice().cmp(key) {
341                std::cmp::Ordering::Less => lo = mid + 1,
342                std::cmp::Ordering::Greater => hi = mid,
343                std::cmp::Ordering::Equal => {
344                    return Some(if g.has_next() { g.next() } else { Vec::new() });
345                }
346            }
347        }
348        None
349    }
350
351    fn get_scan(&self, key: &[u8]) -> Option<Vec<u8>> {
352        let mut g = self.seg.getter();
353        while g.has_next() {
354            let k = g.next();
355            match k.as_slice().cmp(key) {
356                // Skipping the value avoids decompressing and allocating a word we are
357                // about to discard — half the words in the file, on a miss.
358                std::cmp::Ordering::Less => {
359                    if g.has_next() {
360                        g.skip();
361                    }
362                }
363                std::cmp::Ordering::Greater => return None, // keys are sorted
364                std::cmp::Ordering::Equal => {
365                    return Some(if g.has_next() { g.next() } else { Vec::new() });
366                }
367            }
368        }
369        None
370    }
371
372    /// Sample up to `n` real keys spread across the file, for salt validation / search.
373    /// Each returned key is genuinely present, so a correct bloom must contain it.
374    fn sample_keys(&self, n: usize) -> Vec<Vec<u8>> {
375        match &self.index {
376            Some(idx) => {
377                let count = idx.key_count();
378                if count == 0 {
379                    return Vec::new();
380                }
381                let n = (n as u64).min(count);
382                let mut g = self.seg.getter();
383                (0..n)
384                    .filter_map(|s| {
385                        let di = s * count / n;
386                        idx.key_offset(di).map(|off| {
387                            g.reset(off);
388                            g.next()
389                        })
390                    })
391                    .collect()
392            }
393            None => {
394                // No index: take the first `n` keys by scanning.
395                let mut g = self.seg.getter();
396                let mut out = Vec::new();
397                while out.len() < n && g.has_next() {
398                    out.push(g.next());
399                    if g.has_next() {
400                        g.next(); // skip value
401                    }
402                }
403                out
404            }
405        }
406    }
407
408    /// Iterate every `(key, value)` pair sequentially, in stored (key) order.
409    pub fn iter(&self) -> KvIter<'_> {
410        KvIter {
411            getter: self.seg.getter(),
412        }
413    }
414}
415
416/// Iterator over the `(key, value)` pairs of a [`KvReader`], in stored order.
417pub struct KvIter<'a> {
418    getter: Getter<'a>,
419}
420
421impl Iterator for KvIter<'_> {
422    type Item = Result<(Vec<u8>, Vec<u8>)>;
423
424    fn next(&mut self) -> Option<Self::Item> {
425        if !self.getter.has_next() {
426            return None;
427        }
428        let key = self.getter.next();
429        let value = if self.getter.has_next() {
430            self.getter.next()
431        } else {
432            Vec::new()
433        };
434        Some(Ok((key, value)))
435    }
436}