erigon_seg/stack.rs
1//! A newest-wins stack of seg files spanning a step range.
2//!
3//! Erigon keeps a domain's state across several files covering successive *step* ranges
4//! (e.g. `v1.1-accounts.0-1024.kv`, `v1.1-accounts.1024-2048.kv`, …). A newer file may
5//! override a key carried by an older one, so a point lookup must consult the files
6//! newest-first and return the first hit. [`KvStack`] wraps an ordered set of
7//! [`KvReader`]s and implements exactly that semantics, resolving the `.kvei` bloom salt
8//! once and enabling each file's filter against it.
9
10use std::path::{Path, PathBuf};
11
12use crate::error::{Error, Result};
13use crate::hash::murmur3_x64_128_h1;
14use crate::reader::KvReader;
15use crate::salt::Salt;
16
17/// A stack of seg files for one domain, queried newest-first so overrides win.
18///
19/// The salt is resolved once at [`open`](KvStack::open) time (brute-forced from the
20/// oldest file when [`Salt::Find`] is used) and then applied to every file's bloom, so a
21/// wrong or absent salt only disables the negative-lookup speedup — it can never cause a
22/// missed key.
23pub struct KvStack {
24 /// Ordered oldest → newest; queried in reverse so the newest match wins.
25 readers: Vec<KvReader>,
26 /// The resolved salt, if one was supplied or found.
27 salt: Option<u32>,
28}
29
30impl KvStack {
31 /// Open an explicit set of `.kv` files as a newest-wins stack. The files are sorted
32 /// oldest → newest by their `<from>-<to>` step range (parsed from the file name), so
33 /// the caller need not pre-sort them. Errors if `paths` is empty or any file fails to
34 /// open.
35 ///
36 /// `salt` controls the `.kvei` bloom accelerator (see [`Salt`]): the salt is resolved
37 /// once (brute-forced from the oldest file for [`Salt::Find`]) and each file's bloom
38 /// is enabled only if it self-validates against that file's real keys.
39 pub fn open<I, P>(paths: I, salt: Salt) -> Result<KvStack>
40 where
41 I: IntoIterator<Item = P>,
42 P: AsRef<Path>,
43 {
44 let mut paths: Vec<PathBuf> = paths
45 .into_iter()
46 .map(|p| p.as_ref().to_path_buf())
47 .collect();
48 if paths.is_empty() {
49 return Err(Error::format("KvStack::open: no .kv files supplied"));
50 }
51 paths.sort_by_key(|p| step_key(p));
52 let mut readers = Vec::with_capacity(paths.len());
53 for p in &paths {
54 readers.push(KvReader::open(p)?);
55 }
56 let salt = resolve_and_enable(&mut readers, salt);
57 Ok(KvStack { readers, salt })
58 }
59
60 /// Open every `.kv` in `dir` whose file name contains `name_filter`, as a newest-wins
61 /// stack. Use `name_filter` to select a single domain (e.g. `"accounts"`) so files
62 /// from different domains in the same directory are not mixed. Errors if no matching
63 /// `.kv` file is found.
64 pub fn open_dir(dir: impl AsRef<Path>, name_filter: &str, salt: Salt) -> Result<KvStack> {
65 let dir = dir.as_ref();
66 let mut kvs: Vec<PathBuf> = std::fs::read_dir(dir)
67 .map_err(|e| Error::format(format!("read_dir {}: {e}", dir.display())))?
68 .filter_map(|e| e.ok().map(|e| e.path()))
69 .filter(|p| {
70 if p.extension().is_none_or(|x| x != "kv") {
71 return false;
72 }
73 p.file_name()
74 .map(|s| s.to_string_lossy().contains(name_filter))
75 .unwrap_or(false)
76 })
77 .collect();
78 if kvs.is_empty() {
79 return Err(Error::format(format!(
80 "no .kv files matching {name_filter:?} in {}",
81 dir.display()
82 )));
83 }
84 kvs.sort_by_key(|p| step_key(p));
85 KvStack::open(kvs, salt)
86 }
87
88 /// The resolved bloom salt, if one was supplied or found.
89 pub fn salt(&self) -> Option<u32> {
90 self.salt
91 }
92
93 /// Number of files with an active (validated) bloom accelerator.
94 pub fn bloom_count(&self) -> usize {
95 self.readers.iter().filter(|r| r.bloom_active()).count()
96 }
97
98 /// Number of files in the stack.
99 pub fn len(&self) -> usize {
100 self.readers.len()
101 }
102
103 /// Whether the stack has no files. (Never true for a stack from [`open`](KvStack::open),
104 /// which rejects an empty input.)
105 pub fn is_empty(&self) -> bool {
106 self.readers.is_empty()
107 }
108
109 /// The readers, oldest → newest.
110 pub fn readers(&self) -> &[KvReader] {
111 &self.readers
112 }
113
114 /// Iterate `(file name, key count)` for each file, oldest → newest.
115 pub fn files(&self) -> impl Iterator<Item = (&str, u64)> {
116 self.readers.iter().map(|r| (r.name(), r.key_count()))
117 }
118
119 /// Look up `key` across all files, newest-first; returns the value from the newest
120 /// file that contains it (overrides win), or `None` if no file has it.
121 pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
122 // One salt covers the whole stack, so the bloom hash is the same for every file:
123 // compute it once instead of re-hashing the key per reader.
124 let key_hash = self.salt.map(|s| murmur3_x64_128_h1(key, s));
125 for r in self.readers.iter().rev() {
126 // Only reuse the hash for readers whose bloom was enabled with that salt.
127 let h = key_hash.filter(|_| r.salt() == self.salt);
128 if let Some(v) = r.get_hashed(key, h) {
129 return Ok(Some(v));
130 }
131 }
132 Ok(None)
133 }
134
135 /// Advise the kernel that every file in the stack is read by point lookup. See
136 /// [`KvReader::advise_random`] for when this is worth setting.
137 pub fn advise_random(&self) -> std::io::Result<()> {
138 for r in &self.readers {
139 r.advise_random()?;
140 }
141 Ok(())
142 }
143
144 /// Total bytes [`preload_index`](KvStack::preload_index) would make resident across
145 /// the stack. A stack lookup consults every file, so this is the figure to budget.
146 pub fn index_bytes(&self) -> u64 {
147 self.readers.iter().map(|r| r.index_bytes()).sum()
148 }
149
150 /// Preload every file's index into the page cache. See
151 /// [`KvReader::preload_index`] — the case for it is stronger here, because a stack
152 /// lookup searches each file in turn until one hits.
153 pub fn preload_index(&self) -> u64 {
154 self.readers.iter().map(|r| r.preload_index()).sum()
155 }
156
157 /// Pin every file's index in RAM. See [`KvReader::lock_index`] for the
158 /// `RLIMIT_MEMLOCK` caveat, which applies to the stack's total.
159 pub fn lock_index(&self) -> std::io::Result<()> {
160 for r in &self.readers {
161 r.lock_index()?;
162 }
163 Ok(())
164 }
165
166 /// Release the pages pinned by [`lock_index`](KvStack::lock_index).
167 pub fn unlock_index(&self) -> std::io::Result<()> {
168 for r in &self.readers {
169 r.unlock_index()?;
170 }
171 Ok(())
172 }
173}
174
175/// Resolve the salt once (brute-forcing from the oldest file for [`Salt::Find`]) and
176/// enable each file's bloom against it. Returns the resolved salt, if any.
177fn resolve_and_enable(readers: &mut [KvReader], salt: Salt) -> Option<u32> {
178 let resolved = match salt {
179 Salt::None => None,
180 Salt::Known(s) => Some(s),
181 Salt::Find(threads) => readers.first().and_then(|r| r.find_salt(threads)),
182 };
183 if let Some(s) = resolved {
184 for r in readers.iter_mut() {
185 r.enable_bloom(Salt::Known(s));
186 }
187 }
188 resolved
189}
190
191/// Sort key from a `…<name>.<from>-<to>.kv` file name: the `<from>` step (then `<to>`),
192/// so files order oldest → newest (override files have a higher `<from>`). Names without
193/// a recognizable range sort first.
194fn step_key(p: &Path) -> (u64, u64) {
195 let name = p
196 .file_name()
197 .map(|s| s.to_string_lossy().into_owned())
198 .unwrap_or_default();
199 for seg in name.split('.') {
200 if let Some((a, b)) = seg.split_once('-')
201 && let (Ok(a), Ok(b)) = (a.parse::<u64>(), b.parse::<u64>())
202 {
203 return (a, b);
204 }
205 }
206 (0, 0)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use std::path::Path;
213
214 #[test]
215 fn step_key_parses_range() {
216 assert_eq!(step_key(Path::new("v1.1-accounts.0-1024.kv")), (0, 1024));
217 assert_eq!(
218 step_key(Path::new("v1.1-accounts.1024-2048.kv")),
219 (1024, 2048)
220 );
221 // No range segment -> sorts first.
222 assert_eq!(step_key(Path::new("accounts.kv")), (0, 0));
223 }
224
225 #[test]
226 fn empty_open_errors() {
227 let empty: Vec<&Path> = Vec::new();
228 assert!(KvStack::open(empty, Salt::None).is_err());
229 }
230}