Skip to content

Commit 6ff0cc5

Browse files
prestwichclaude
andauthored
feat(hot-mdbx): pre-populate FSI cache at DB open (ENG-2136) (#51)
* refactor(hot-mdbx): replace FsiCache type alias with two-tier struct (ENG-2136) Replaces Arc<RwLock<HashMap>> type alias with a two-tier FsiCache struct: lock-free linear scan over 9 known tables, RwLock<HashMap> fallback for dynamic tables. Updates callsites in tx.rs and lib.rs to use new API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(hot-mdbx): pre-populate FSI cache at DB open (ENG-2136) Read FSI metadata for all 9 known tables at open time and store them in the lock-free known array of FsiCache, eliminating the dynamic map fallback for standard tables on every transaction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(hot-mdbx): review fixes — NUM_TABLES constant, visibility, comments (ENG-2136) - Add NUM_TABLES constant in signet-hot tables module, replace all hardcoded 9s - Make FsiCache pub(crate) — only used within the crate - Update stale field doc on fsi_cache to reflect pre-population at open - Clarify comment in create_tables_and_populate_cache about throwaway cache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 76886ef commit 6ff0cc5

4 files changed

Lines changed: 191 additions & 36 deletions

File tree

crates/hot-mdbx/src/db_info.rs

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,66 @@
11
use bytes::Buf;
22
use parking_lot::RwLock;
3-
use signet_hot::ValSer;
4-
use std::collections::HashMap;
3+
use signet_hot::{ValSer, tables::NUM_TABLES};
4+
use std::{collections::HashMap, sync::Arc};
5+
6+
/// Inner storage for the two-tier FSI cache.
7+
///
8+
/// The `known` array holds pre-populated entries for the standard tables,
9+
/// searched via lock-free linear scan. The `dynamic` map holds entries for
10+
/// tables created at runtime.
11+
#[derive(Debug)]
12+
struct FsiCacheInner {
13+
/// Pre-populated at open time. Lock-free linear scan.
14+
known: [(&'static str, FixedSizeInfo); NUM_TABLES],
15+
/// Locking fallback for dynamically created tables.
16+
dynamic: RwLock<HashMap<&'static str, FixedSizeInfo>>,
17+
}
18+
19+
/// Two-tier cache for [`FixedSizeInfo`].
20+
///
21+
/// The fast path is a lock-free linear scan over the known table entries.
22+
/// The slow path acquires a `RwLock` for dynamically created tables.
23+
#[derive(Debug, Clone)]
24+
pub(crate) struct FsiCache(Arc<FsiCacheInner>);
25+
26+
impl Default for FsiCache {
27+
fn default() -> Self {
28+
Self::new([("", FixedSizeInfo::None); NUM_TABLES])
29+
}
30+
}
531

6-
/// Type alias for the FixedSizeInfo cache.
7-
pub type FsiCache = std::sync::Arc<RwLock<HashMap<&'static str, FixedSizeInfo>>>;
32+
impl FsiCache {
33+
/// Create a new `FsiCache` pre-populated with the known table entries.
34+
pub(crate) fn new(known: [(&'static str, FixedSizeInfo); NUM_TABLES]) -> Self {
35+
Self(Arc::new(FsiCacheInner { known, dynamic: RwLock::new(HashMap::new()) }))
36+
}
37+
38+
/// Look up a table's [`FixedSizeInfo`].
39+
///
40+
/// Checks the lock-free known array first, then the locked dynamic map.
41+
/// Returns `None` if the table is not cached.
42+
pub(crate) fn get(&self, name: &str) -> Option<FixedSizeInfo> {
43+
// Fast path: linear scan over known tables (no lock).
44+
for &(known_name, fsi) in &self.0.known {
45+
if known_name == name {
46+
return Some(fsi);
47+
}
48+
}
49+
// Slow path: check dynamic map.
50+
self.0.dynamic.read().get(name).copied()
51+
}
52+
53+
/// Insert a dynamically created table's [`FixedSizeInfo`].
54+
pub(crate) fn insert_dynamic(&self, name: &'static str, fsi: FixedSizeInfo) {
55+
self.0.dynamic.write().insert(name, fsi);
56+
}
57+
}
858

959
/// Information about fixed size values in a database.
10-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1161
pub enum FixedSizeInfo {
1262
/// Not a DUPSORT table.
63+
#[default]
1364
None,
1465
/// DUPSORT table without DUP_FIXED (variable value size).
1566
DupSort {
@@ -144,4 +195,53 @@ mod tests {
144195
other => panic!("expected InsufficientData, got: {other:?}"),
145196
}
146197
}
198+
199+
#[test]
200+
fn fsi_cache_known_path() {
201+
let known = [
202+
("TableA", FixedSizeInfo::None),
203+
("TableB", FixedSizeInfo::DupSort { key2_size: 32 }),
204+
("TableC", FixedSizeInfo::DupFixed { key2_size: 32, total_size: 64 }),
205+
("TableD", FixedSizeInfo::None),
206+
("TableE", FixedSizeInfo::None),
207+
("TableF", FixedSizeInfo::None),
208+
("TableG", FixedSizeInfo::None),
209+
("TableH", FixedSizeInfo::None),
210+
("TableI", FixedSizeInfo::None),
211+
];
212+
let cache = FsiCache::new(known);
213+
214+
assert_eq!(cache.get("TableA"), Some(FixedSizeInfo::None));
215+
assert_eq!(cache.get("TableB"), Some(FixedSizeInfo::DupSort { key2_size: 32 }));
216+
assert_eq!(
217+
cache.get("TableC"),
218+
Some(FixedSizeInfo::DupFixed { key2_size: 32, total_size: 64 })
219+
);
220+
// Unknown table returns None
221+
assert_eq!(cache.get("Unknown"), None);
222+
}
223+
224+
#[test]
225+
fn fsi_cache_dynamic_path() {
226+
let known = [
227+
("T1", FixedSizeInfo::None),
228+
("T2", FixedSizeInfo::None),
229+
("T3", FixedSizeInfo::None),
230+
("T4", FixedSizeInfo::None),
231+
("T5", FixedSizeInfo::None),
232+
("T6", FixedSizeInfo::None),
233+
("T7", FixedSizeInfo::None),
234+
("T8", FixedSizeInfo::None),
235+
("T9", FixedSizeInfo::None),
236+
];
237+
let cache = FsiCache::new(known);
238+
239+
// Not in known set
240+
assert_eq!(cache.get("DynTable"), None);
241+
242+
// Insert dynamically
243+
let fsi = FixedSizeInfo::DupSort { key2_size: 20 };
244+
cache.insert_dynamic("DynTable", fsi);
245+
assert_eq!(cache.get("DynTable"), Some(fsi));
246+
}
147247
}

crates/hot-mdbx/src/lib.rs

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,18 @@
5151
#![deny(unused_must_use, rust_2018_idioms)]
5252
#![cfg_attr(docsrs, feature(doc_cfg))]
5353

54-
use parking_lot::RwLock;
5554
use signet_libmdbx::{
5655
Environment, EnvironmentFlags, Geometry, Mode, Ro, RoSync, Rw, RwSync, SyncMode, ffi,
5756
sys::{HandleSlowReadersReturnCode, PageSize},
5857
};
59-
use std::{collections::HashMap, ops::Range, path::Path, sync::Arc};
58+
use std::{ops::Range, path::Path};
6059

6160
mod cursor;
6261
pub use cursor::{Cursor, CursorRo, CursorRoSync, CursorRw, CursorRwSync};
6362

6463
mod db_info;
65-
pub use db_info::{FixedSizeInfo, FsiCache};
64+
pub use db_info::FixedSizeInfo;
65+
use db_info::FsiCache;
6666

6767
mod error;
6868
pub use error::MdbxError;
@@ -78,7 +78,26 @@ pub use tx::Tx;
7878

7979
mod utils;
8080

81-
use signet_hot::model::{HotKv, HotKvError, HotKvWrite};
81+
use signet_hot::{
82+
model::{HotKv, HotKvError, HotKvWrite},
83+
tables::{
84+
AccountChangeSets, AccountsHistory, Bytecodes, HeaderNumbers, Headers, NUM_TABLES,
85+
PlainAccountState, PlainStorageState, StorageChangeSets, StorageHistory, Table,
86+
},
87+
};
88+
89+
/// The known table names, used to pre-populate the FSI cache at open time.
90+
const KNOWN_TABLE_NAMES: [&str; NUM_TABLES] = [
91+
Headers::NAME,
92+
HeaderNumbers::NAME,
93+
Bytecodes::NAME,
94+
PlainAccountState::NAME,
95+
PlainStorageState::NAME,
96+
AccountsHistory::NAME,
97+
AccountChangeSets::NAME,
98+
StorageHistory::NAME,
99+
StorageChangeSets::NAME,
100+
];
82101

83102
/// 1 KB in bytes
84103
pub const KILOBYTE: usize = 1024;
@@ -247,12 +266,11 @@ impl DatabaseArguments {
247266
pub struct DatabaseEnv {
248267
/// Libmdbx-sys environment.
249268
inner: Environment,
250-
/// Cached FixedSizeInfo for tables.
269+
/// Cached FixedSizeInfo for tables, pre-populated at open time.
251270
///
252-
/// Important: Do not manually close these DBIs, like via `mdbx_dbi_close`.
253-
/// More generally, do not dynamically create, re-open, or drop tables at
254-
/// runtime. It's better to perform table creation and migration only once
255-
/// at startup.
271+
/// The standard tables are created and their FSI entries cached during
272+
/// [`DatabaseEnv::open`]. Do not manually close DBIs (e.g. via
273+
/// `mdbx_dbi_close`) or dynamically drop tables at runtime.
256274
fsi_cache: FsiCache,
257275

258276
/// Write lock for when dealing with a read-write environment.
@@ -366,24 +384,15 @@ impl DatabaseEnv {
366384
// https://github.com/paradigmxyz/reth/blob/fa2b9b685ed9787636d962f4366caf34a9186e66/crates/storage/libmdbx-rs/mdbx-sys/libmdbx/mdbx.c#L16017.
367385
inner_env.set_rp_augment_limit(256 * 1024);
368386

369-
let fsi_cache = Arc::new(RwLock::new(HashMap::new()));
370-
let env = Self { inner: inner_env.open(path)?, fsi_cache, _lock_file };
371-
372-
if kind.is_rw() {
373-
env.create_tables()?;
374-
}
387+
let inner = inner_env.open(path)?;
375388

376-
Ok(env)
377-
}
389+
let fsi_cache = if kind.is_rw() {
390+
create_tables_and_populate_cache(&inner)?
391+
} else {
392+
populate_cache_ro(&inner)?
393+
};
378394

379-
/// Create all standard hot storage tables.
380-
///
381-
/// Called automatically when opening in read-write mode.
382-
fn create_tables(&self) -> Result<(), MdbxError> {
383-
let tx = self.tx_rw()?;
384-
tx.queue_db_init()?;
385-
tx.raw_commit()?;
386-
Ok(())
395+
Ok(Self { inner, fsi_cache, _lock_file })
387396
}
388397

389398
/// Start a new read-only transaction.
@@ -431,3 +440,41 @@ impl HotKv for DatabaseEnv {
431440
self.tx_rw().map_err(HotKvError::from_err)
432441
}
433442
}
443+
444+
/// Create all standard hot storage tables and return a pre-populated
445+
/// [`FsiCache`]. Called during RW open.
446+
fn create_tables_and_populate_cache(env: &Environment) -> Result<FsiCache, MdbxError> {
447+
let inner_tx = env.begin_rw_unsync().map_err(MdbxError::Mdbx)?;
448+
// Tx requires an FsiCache, so we pass a throwaway empty one. The FSI
449+
// entries written by queue_db_init's store_fsi calls land in this
450+
// temporary cache's dynamic map — they are discarded. We re-read the
451+
// authoritative values from the metadata table via read_known_fsi.
452+
let tmp_cache = FsiCache::new(Default::default());
453+
let tx = Tx::new(inner_tx, tmp_cache);
454+
tx.queue_db_init()?;
455+
456+
let known = read_known_fsi(&tx)?;
457+
tx.raw_commit()?;
458+
Ok(FsiCache::new(known))
459+
}
460+
461+
/// Read FSI entries for all known tables from the metadata table.
462+
fn read_known_fsi<K: signet_libmdbx::TransactionKind>(
463+
tx: &Tx<K>,
464+
) -> Result<[(&'static str, FixedSizeInfo); NUM_TABLES], MdbxError> {
465+
let mut known = [("", FixedSizeInfo::None); NUM_TABLES];
466+
for (i, &name) in KNOWN_TABLE_NAMES.iter().enumerate() {
467+
known[i] = (name, tx.read_fsi_from_table(name)?);
468+
}
469+
Ok(known)
470+
}
471+
472+
/// Read FSI entries for all known tables via a temporary RO transaction.
473+
/// Called during RO open.
474+
fn populate_cache_ro(env: &Environment) -> Result<FsiCache, MdbxError> {
475+
let inner_tx = env.begin_ro_unsync().map_err(MdbxError::Mdbx)?;
476+
let tmp_cache = FsiCache::new(Default::default());
477+
let tx = Tx::new(inner_tx, tmp_cache);
478+
let known = read_known_fsi(&tx)?;
479+
Ok(FsiCache::new(known))
480+
}

crates/hot-mdbx/src/tx.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ impl<K: TransactionKind> Tx<K> {
4040
}
4141

4242
/// Reads FixedSizeInfo from the metadata table.
43-
fn read_fsi_from_table(&self, name: &'static str) -> Result<FixedSizeInfo, MdbxError> {
43+
pub(crate) fn read_fsi_from_table(
44+
&self,
45+
name: &'static str,
46+
) -> Result<FixedSizeInfo, MdbxError> {
4447
let db = self.inner.open_db(None)?;
4548

4649
let data: [u8; 8] = self
@@ -54,13 +57,13 @@ impl<K: TransactionKind> Tx<K> {
5457

5558
/// Gets cached FixedSizeInfo for a table.
5659
pub fn get_fsi(&self, name: &'static str) -> Result<FixedSizeInfo, MdbxError> {
57-
// Fast path: read lock
58-
if let Some(&fsi) = self.fsi_cache.read().get(name) {
60+
// Fast path: lock-free scan over known tables, then locked dynamic map.
61+
if let Some(fsi) = self.fsi_cache.get(name) {
5962
return Ok(fsi);
6063
}
61-
// Slow path: read from table, then write lock
64+
// Slow path: read from table, then insert into dynamic map.
6265
let fsi = self.read_fsi_from_table(name)?;
63-
self.fsi_cache.write().insert(name, fsi);
66+
self.fsi_cache.insert_dynamic(name, fsi);
6467
Ok(fsi)
6568
}
6669

@@ -135,7 +138,7 @@ impl<K: TransactionKind + WriteMarker> Tx<K> {
135138
fsi.encode_value_to(&mut value_buf.as_mut_slice());
136139

137140
self.inner.put(db, fsi_name_to_key(table).as_slice(), value_buf, WriteFlags::UPSERT)?;
138-
self.fsi_cache.write().insert(table, fsi);
141+
self.fsi_cache.insert_dynamic(table, fsi);
139142

140143
Ok(())
141144
}

crates/hot/src/tables/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ mod macros;
55
mod definitions;
66
pub use definitions::*;
77

8+
/// The number of standard hot storage tables created by
9+
/// [`queue_db_init`](crate::model::HotKvWrite::queue_db_init). Update this
10+
/// constant whenever a table is added to or removed from `queue_db_init`.
11+
pub const NUM_TABLES: usize = 9;
12+
813
use crate::{
914
DeserError, KeySer, MAX_FIXED_VAL_SIZE, MAX_KEY_SIZE, ValSer,
1015
model::{DualKeyValue, KeyValue},

0 commit comments

Comments
 (0)