ethrex_storage/api/mod.rs
1//! # Storage Backend API
2//!
3//! This module provides a thin, minimal interface for storage backends:
4//!
5//! - Thin: Minimal set of operations that databases must provide
6//! - Simple: Avoids type-system complexity and focuses on core functionality
7//!
8//! Rather than implementing business logic in each database backend, this API
9//! provides low-level primitives that higher-level code can build upon.
10//! This eliminates code duplication and makes adding new database backends trivial.
11//!
12//! The API differentiates between three types of database access:
13//!
14//! - Read views ([`StorageReadView`]): read-only views of the database,
15//! with no atomicity guarantees between operations.
16//! - Write batches ([`StorageWriteBatch`]): write batch functionality, with
17//! atomicity guarantees at commit time.
18//! - Locked views ([`StorageLockedView`]): read-only views of a point in time (snapshots), right now it's
19//! only used during snap-sync.
20
21use crate::error::StoreError;
22use std::{fmt::Debug, path::Path, sync::Arc};
23
24pub mod tables;
25
26/// Type alias for the result of a prefix iterator.
27pub type PrefixResult = Result<(Box<[u8]>, Box<[u8]>), StoreError>;
28
29/// Per-column-family on-disk statistics, for DB observability.
30#[derive(Debug, Clone, Default)]
31pub struct CfStats {
32 pub name: String,
33 /// Live SST bytes on disk for this CF.
34 pub live_sst_bytes: u64,
35 /// Total SST bytes incl. not-yet-compacted/obsolete (for space amplification).
36 pub total_sst_bytes: u64,
37 /// Estimated live (logical) data size.
38 pub live_data_bytes: u64,
39 /// Estimated number of keys.
40 pub num_keys: u64,
41 /// Live SST file count.
42 pub num_files: u64,
43 /// Live blob file bytes (only ACCOUNT_CODES uses blobs).
44 pub blob_bytes: u64,
45 /// Estimated pending compaction bytes (write-debt indicator).
46 pub pending_compaction_bytes: u64,
47 /// Current memtable bytes for this CF.
48 pub memtable_bytes: u64,
49}
50
51/// Snapshot of RocksDB observability stats: per-CF sizes plus DB-wide block-cache
52/// counters. Read from RocksDB properties (+ statistics, when enabled).
53#[derive(Debug, Clone, Default)]
54pub struct RocksDbStats {
55 pub cfs: Vec<CfStats>,
56 pub block_cache_usage_bytes: u64,
57 pub block_cache_capacity_bytes: u64,
58 pub block_cache_pinned_bytes: u64,
59 pub running_compactions: u64,
60 /// Cumulative block-cache hits (0 unless RocksDB statistics are enabled).
61 pub block_cache_hits: u64,
62 /// Cumulative block-cache misses (0 unless RocksDB statistics are enabled).
63 pub block_cache_misses: u64,
64}
65
66/// This trait provides a minimal set of operations required from a database backend.
67/// Implementations should focus on providing efficient access to the underlying storage
68/// without implementing business logic.
69pub trait StorageBackend: Debug + Send + Sync {
70 /// Removes all data from the specified table.
71 fn clear_table(&self, table: &'static str) -> Result<(), StoreError>;
72
73 /// Opens a new read view.
74 fn begin_read(&self) -> Result<Arc<dyn StorageReadView>, StoreError>;
75
76 /// Creates a new write batch.
77 fn begin_write(&self) -> Result<Box<dyn StorageWriteBatch + 'static>, StoreError>;
78
79 /// Creates a locked snapshot for a specific table.
80 ///
81 /// This provides a persistent read-only view of a single table, optimized
82 /// for batch read operations. The snapshot remains valid until dropped.
83 fn begin_locked(
84 &self,
85 table_name: &'static str,
86 ) -> Result<Box<dyn StorageLockedView + 'static>, StoreError>;
87
88 // TODO: remove this and provide historic data via diff-layers
89 /// Creates a checkpoint of the current database state at the specified path.
90 fn create_checkpoint(&self, path: &Path) -> Result<(), StoreError>;
91
92 /// Durably persists all buffered writes to disk, so a subsequent process
93 /// start needs no crash recovery. Called on graceful shutdown. Defaults to a
94 /// no-op for backends that are already durable or purely in-memory.
95 fn flush(&self) -> Result<(), StoreError> {
96 Ok(())
97 }
98
99 /// Returns per-column-family and DB-wide RocksDB observability stats, or
100 /// `None` for backends that don't expose them (e.g. in-memory).
101 fn db_stats(&self) -> Option<RocksDbStats> {
102 None
103 }
104}
105
106/// Read-only transaction interface.
107/// Provides methods to read data from the database
108pub trait StorageReadView: Send + Sync {
109 /// Retrieves a value by key from the specified table.
110 fn get(&self, table: &'static str, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
111
112 /// Retrieves multiple values by key from the specified table.
113 /// Returns results in the same order as the input keys.
114 /// Backends that support batched reads (e.g. RocksDB `multi_get_cf`)
115 /// should override this for better throughput. Callers should not
116 /// assume `multi_get` is asymptotically faster than `get`; on backends
117 /// without a batched read primitive (e.g. the in-memory backend) the
118 /// default impl below is equivalent to N independent `get` calls.
119 fn multi_get(
120 &self,
121 table: &'static str,
122 keys: &[&[u8]],
123 ) -> Vec<Result<Option<Vec<u8>>, StoreError>> {
124 keys.iter().map(|k| self.get(table, k)).collect()
125 }
126
127 /// Returns an iterator over all key-value pairs with the given prefix.
128 fn prefix_iterator(
129 &self,
130 table: &'static str,
131 prefix: &[u8],
132 ) -> Result<Box<dyn Iterator<Item = PrefixResult> + '_>, StoreError>;
133
134 /// Returns the lowest key in `table` by lexicographic order, or `None` if the table is
135 /// empty. Backends that support forward iteration (e.g. RocksDB `IteratorMode::Start`)
136 /// should implement this in O(1).
137 fn first_key(&self, table: &'static str) -> Result<Option<Vec<u8>>, StoreError>;
138
139 /// Returns the highest key in `table` by lexicographic order, or `None` if the table is
140 /// empty. Backends that support reverse iteration (e.g. RocksDB `IteratorMode::End`) should
141 /// implement this in O(1).
142 fn last_key(&self, table: &'static str) -> Result<Option<Vec<u8>>, StoreError>;
143}
144
145/// Write transaction interface.
146///
147/// Note that this does not provide read access, since we don't currently use that functionality.
148///
149/// Changes are not persisted until [`commit()`](StorageWriteBatch::commit) is called.
150pub trait StorageWriteBatch: Send {
151 /// Stores a key-value pair in the specified table.
152 fn put(&mut self, table: &'static str, key: &[u8], value: &[u8]) -> Result<(), StoreError> {
153 self.put_batch(table, vec![(key.to_vec(), value.to_vec())])
154 }
155
156 /// Stores multiple key-value pairs in the specified table within the transaction.
157 fn put_batch(
158 &mut self,
159 table: &'static str,
160 batch: Vec<(Vec<u8>, Vec<u8>)>,
161 ) -> Result<(), StoreError>;
162
163 /// Removes a key-value pair from the specified table.
164 fn delete(&mut self, table: &'static str, key: &[u8]) -> Result<(), StoreError>;
165
166 /// Removes every key in `[start, end)` from the specified table.
167 ///
168 /// Half-open range; `end` is exclusive. Equivalent to enumerating each key
169 /// in the range and calling [`delete`], but backends with native range-delete
170 /// support (e.g. RocksDB's `delete_range_cf`) can implement it more efficiently.
171 ///
172 /// Lexicographic byte order is used for the range bounds — callers using
173 /// numeric keys must encode them in a representation whose lex order matches
174 /// numeric order (e.g. `u64::to_be_bytes()`).
175 fn delete_range(
176 &mut self,
177 table: &'static str,
178 start: &[u8],
179 end: &[u8],
180 ) -> Result<(), StoreError>;
181
182 /// Appends a merge operand for the given key in the specified table.
183 ///
184 /// The actual combine step is deferred — backends with a registered merge
185 /// operator (RocksDB) apply it at read or compaction time; backends without
186 /// (InMemory) dispatch by table and apply inline.
187 ///
188 /// Currently used for `TRANSACTION_LOCATIONS`. Calling on a table without
189 /// a registered merge function is an error.
190 fn merge(&mut self, table: &'static str, key: &[u8], operand: &[u8]) -> Result<(), StoreError>;
191
192 /// Commits all changes made in this transaction.
193 fn commit(&mut self) -> Result<(), StoreError>;
194}
195
196/// Locked snapshot interface for batch read operations.
197/// Provides read-only access to a specific table with a persistent snapshot.
198/// This is optimized for scenarios where many reads are performed on the same
199/// table, such as trie traversal operations.
200/// This is currently only used in snapsync stage.
201// TODO: Check if we can remove this trait and use [`StorageReadView`] instead.
202pub trait StorageLockedView: Send + Sync {
203 /// Retrieves a value by key from the locked table.
204 fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
205}