1use std::num::{NonZeroU64, NonZeroUsize};
5
6use firewood::{
7 api::{
8 self, ArcDynDbView, Db as _, DbView, FrozenChangeProof, HashKey, HashKeyExt, IntoBatchIter,
9 KeyType,
10 },
11 db::{Db, DbConfig},
12 manager::RevisionManagerConfig,
13 merkle::Merkle,
14};
15use firewood_storage::{Committed, FileBacked, NodeStore};
16
17use crate::{BatchOp, BorrowedBytes, CView, CreateProposalResult, arc_cache::ArcCache};
18
19use crate::revision::{GetRevisionResult, RevisionHandle};
20use firewood_metrics::{
21 MetricsContext, firewood_increment, firewood_record, fwd_expensive_timed_result,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[repr(C)]
29pub enum NodeHashAlgorithm {
30 MerkleDB = 0,
32 Ethereum = 1,
34}
35
36impl From<NodeHashAlgorithm> for firewood_storage::NodeHashAlgorithm {
37 fn from(alg: NodeHashAlgorithm) -> Self {
38 match alg {
39 NodeHashAlgorithm::MerkleDB => firewood_storage::NodeHashAlgorithm::MerkleDB,
40 NodeHashAlgorithm::Ethereum => firewood_storage::NodeHashAlgorithm::Ethereum,
41 }
42 }
43}
44
45#[repr(C)]
49#[derive(Debug)]
50pub struct DatabaseHandleArgs<'a> {
51 pub dir: BorrowedBytes<'a>,
57
58 pub root_store: bool,
64
65 pub node_cache_memory_limit: usize,
70
71 pub free_list_cache_size: usize,
75
76 pub revisions: usize,
80
81 pub strategy: u8,
91
92 pub truncate: bool,
94
95 pub expensive_metrics: bool,
99
100 pub node_hash_algorithm: NodeHashAlgorithm,
108
109 pub deferred_persistence_commit_count: u64,
113}
114
115impl DatabaseHandleArgs<'_> {
116 fn as_rev_manager_config(&self) -> Result<RevisionManagerConfig, api::Error> {
117 let cache_read_strategy = match self.strategy {
118 0 => firewood::manager::CacheReadStrategy::WritesOnly,
119 1 => firewood::manager::CacheReadStrategy::BranchReads,
120 2 => firewood::manager::CacheReadStrategy::All,
121 _ => return Err(invalid_data("invalid cache strategy")),
122 };
123 let free_list_cache_size = NonZeroUsize::new(self.free_list_cache_size)
124 .ok_or_else(|| invalid_data("free list cache size should be non-zero"))?;
125 let commit_count = NonZeroU64::new(self.deferred_persistence_commit_count)
126 .ok_or(api::Error::ZeroCommitCount)?;
127
128 let memory_limit = NonZeroUsize::new(self.node_cache_memory_limit);
129
130 let config = {
131 let builder = RevisionManagerConfig::builder()
132 .max_revisions(self.revisions)
133 .cache_read_strategy(cache_read_strategy)
134 .free_list_cache_size(free_list_cache_size)
135 .deferred_persistence_commit_count(commit_count);
136
137 if let Some(memory_limit) = memory_limit {
138 builder.node_cache_memory_limit(memory_limit).build()
139 } else {
140 builder.build()
141 }
142 };
143
144 Ok(config)
145 }
146}
147
148#[derive(Debug)]
153#[repr(C)]
154pub struct DatabaseHandle {
155 cached_view: ArcCache<HashKey, dyn api::DynDbView>,
157
158 db: Db,
160
161 metrics_context: MetricsContext,
162}
163
164impl DatabaseHandle {
165 pub fn new(args: DatabaseHandleArgs<'_>) -> Result<Self, api::Error> {
171 let metrics_context = MetricsContext::new(args.expensive_metrics);
172
173 let cfg = DbConfig::builder()
174 .node_hash_algorithm(args.node_hash_algorithm.into())
175 .truncate(args.truncate)
176 .manager(args.as_rev_manager_config()?)
177 .root_store(args.root_store)
178 .build();
179
180 let path = args
181 .dir
182 .as_str()
183 .map_err(|err| invalid_data(format!("database path contains invalid utf-8: {err}")))?;
184
185 if path.is_empty() {
186 return Err(invalid_data("database path cannot be empty"));
187 }
188
189 let db = Db::new(path, cfg)?;
190 Ok(Self {
191 cached_view: ArcCache::new(),
192 db,
193 metrics_context,
194 })
195 }
196
197 pub fn current_root_hash(&self) -> Option<HashKey> {
203 self.db.root_hash()
204 }
205
206 pub fn get_latest(&self, key: impl KeyType) -> Result<Option<Box<[u8]>>, api::Error> {
212 let Some(root) = self.current_root_hash() else {
213 return Err(api::Error::RevisionNotFound {
214 provided: HashKey::default_root_hash(),
215 });
216 };
217
218 self.db.revision(root)?.val(key)
219 }
220
221 pub fn create_batch<'a>(
227 &self,
228 values: impl AsRef<[BatchOp<'a>]> + 'a,
229 ) -> Result<Option<HashKey>, api::Error> {
230 let (root_hash_result, elapsed) =
231 fwd_expensive_timed_result!(crate::registry::BATCH_MS_BUCKET, {
232 let CreateProposalResult { handle } =
233 self.create_proposal_handle(values.as_ref())?;
234 handle.commit_proposal()
235 });
236 let root_hash = root_hash_result?;
237 firewood_increment!(crate::registry::BATCH_MS, elapsed.as_millis() as u64);
238 firewood_increment!(crate::registry::BATCH_COUNT, 1);
239 firewood_record!(
240 crate::registry::BATCH_MS_BUCKET,
241 elapsed.as_secs_f64() * 1000.0,
242 expensive
243 );
244
245 Ok(root_hash)
246 }
247
248 pub fn get_revision(&self, root: HashKey) -> Result<GetRevisionResult<'_>, api::Error> {
256 let view = self.db.view(root.clone())?;
257 let historical = match self.db.revision(root.clone()) {
258 Ok(rev) => Some(rev),
259 Err(api::Error::RevisionNotFound { .. }) => None,
260 Err(err) => return Err(err),
261 };
262 Ok(GetRevisionResult {
263 handle: RevisionHandle::new(view, historical, self.metrics_context, self),
264 root_hash: root,
265 })
266 }
267
268 pub fn reconstruct_from_view<'db>(
274 &'db self,
275 parent: &NodeStore<Committed, FileBacked>,
276 batch: impl IntoBatchIter,
277 ) -> Result<firewood::db::ReconstructedView<'db>, api::Error> {
278 self.db.reconstruct_from_view(parent, batch)
279 }
280
281 pub(crate) fn get_root(&self, root: HashKey) -> Result<ArcDynDbView, api::Error> {
282 let mut cache_miss = false;
283 let view = self.cached_view.get_or_try_insert_with(root, |key| {
284 cache_miss = true;
285 self.db.view(HashKey::clone(key))
286 })?;
287
288 if cache_miss {
289 firewood_increment!(crate::registry::CACHED_VIEW_MISS, 1);
290 } else {
291 firewood_increment!(crate::registry::CACHED_VIEW_HIT, 1);
292 }
293
294 Ok(view)
295 }
296
297 pub(crate) fn clear_cached_view(&self) {
298 self.cached_view.clear();
299 }
300
301 pub(crate) fn merge_key_value_range(
302 &self,
303 first_key: Option<impl KeyType>,
304 last_key: Option<impl KeyType>,
305 key_values: impl IntoIterator<Item: api::KeyValuePair>,
306 ) -> Result<CreateProposalResult<'_>, api::Error> {
307 CreateProposalResult::new(self, || {
308 self.db
309 .merge_key_value_range(first_key, last_key, key_values)
310 })
311 }
312
313 pub(crate) fn change_proof(
333 &self,
334 start_hash: HashKey,
335 end_hash: HashKey,
336 start_key: Option<&[u8]>,
337 end_key: Option<&[u8]>,
338 limit: Option<NonZeroUsize>,
339 ) -> Result<FrozenChangeProof, api::Error> {
340 let end_merkle = Merkle::from(self.db.revision(end_hash).map_err(|err| {
344 if let api::Error::RevisionNotFound { provided } = err {
345 api::Error::EndRevisionNotFound { provided }
346 } else {
347 err
348 }
349 })?);
350
351 let start_merkle = Merkle::from(self.db.revision(start_hash).map_err(|err| {
353 if let api::Error::RevisionNotFound { provided } = err {
354 api::Error::StartRevisionNotFound { provided }
355 } else {
356 err
357 }
358 })?);
359
360 end_merkle.change_proof(start_key, end_key, start_merkle.nodestore(), limit)
361 }
362
363 pub fn apply_change_proof_to_parent(
370 &self,
371 start_hash: HashKey,
372 change_proof: &FrozenChangeProof,
373 ) -> Result<CreateProposalResult<'_>, api::Error> {
374 CreateProposalResult::new(self, || {
375 let parent = &self.db.revision(start_hash)?;
376 self.db.apply_change_proof_to_parent(change_proof, parent)
377 })
378 }
379
380 pub fn dump_to_string(&self) -> Result<String, api::Error> {
386 self.db.dump_to_string().map_err(api::Error::from)
387 }
388
389 pub fn close(self) -> Result<(), api::Error> {
396 self.db.close()
397 }
398}
399
400impl<'db> CView<'db> for &'db crate::DatabaseHandle {
401 fn handle(&self) -> &'db crate::DatabaseHandle {
402 self
403 }
404
405 fn create_proposal(
406 self,
407 values: impl IntoBatchIter,
408 ) -> Result<firewood::db::Proposal<'db>, api::Error> {
409 self.db.propose(values)
410 }
411}
412
413impl crate::MetricsContextExt for DatabaseHandle {
414 fn metrics_context(&self) -> Option<MetricsContext> {
415 Some(self.metrics_context)
416 }
417}
418
419fn invalid_data(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> api::Error {
420 api::Error::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, error))
421}