1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::must_use_candidate)]
4#![allow(clippy::needless_lifetimes)]
5
6pub mod envs;
7
8use std::fmt;
9use std::ops::Range;
10use std::path::Path;
11use std::str::FromStr;
12
13use async_trait::async_trait;
14use fedimint_core::db::{
15 DatabaseError, DatabaseResult, IDatabaseTransactionOps, IDatabaseTransactionOpsCore,
16 IRawDatabase, IRawDatabaseTransaction, PrefixStream,
17};
18use fedimint_core::task::block_in_place;
19use fedimint_db_locked::{DbLockError, Locked, LockedBuilder};
20use futures::stream;
21pub use rocksdb;
22use rocksdb::{
23 DBRecoveryMode, OptimisticTransactionDB, OptimisticTransactionOptions, WriteOptions,
24};
25use tracing::debug;
26
27use crate::envs::{FM_ROCKSDB_BLOCK_CACHE_SIZE_ENV, FM_ROCKSDB_WRITE_BUFFER_SIZE_ENV};
28
29fn convert_to_async_stream<'i, I>(iter: I) -> impl futures::Stream<Item = I::Item> + use<I>
32where
33 I: Iterator + Send + 'i,
34 I::Item: Send,
35{
36 stream::unfold(iter, |mut iter| async {
37 fedimint_core::runtime::block_in_place(|| {
38 let item = iter.next();
39 item.map(|item| (item, iter))
40 })
41 })
42}
43
44#[derive(Debug)]
45pub struct RocksDb(rocksdb::OptimisticTransactionDB);
46
47pub struct RocksDbTransaction<'a>(rocksdb::Transaction<'a, rocksdb::OptimisticTransactionDB>);
48
49#[bon::bon]
50impl RocksDb {
51 #[builder(start_fn = build)]
53 #[builder(finish_fn = open_blocking)]
54 pub fn open_blocking(
55 #[builder(start_fn)] db_path: impl AsRef<Path>,
56 ) -> Result<Locked<RocksDb>, RocksDbOpenError> {
57 let db_path = db_path.as_ref();
58
59 block_in_place(|| {
60 std::fs::create_dir_all(db_path.parent().ok_or(RocksDbOpenError::NoBaseDir)?)?;
61 LockedBuilder::new(db_path)?.with_db(|| Self::open_blocking_unlocked(db_path))
62 })
63 }
64}
65
66impl<I1, S> RocksDbOpenBlockingBuilder<I1, S>
67where
68 S: rocks_db_open_blocking_builder::State,
69 I1: std::convert::AsRef<std::path::Path>,
70{
71 #[allow(clippy::unused_async)]
73 pub async fn open(self) -> Result<Locked<RocksDb>, RocksDbOpenError> {
74 block_in_place(|| self.open_blocking())
75 }
76}
77
78impl RocksDb {
79 fn open_blocking_unlocked(db_path: &Path) -> Result<RocksDb, RocksDbOpenError> {
80 let mut opts = get_default_options()?;
81 opts.set_wal_recovery_mode(DBRecoveryMode::TolerateCorruptedTailRecords);
89 let db: rocksdb::OptimisticTransactionDB =
90 rocksdb::OptimisticTransactionDB::<rocksdb::SingleThreaded>::open(&opts, db_path)?;
91 Ok(RocksDb(db))
92 }
93
94 pub fn inner(&self) -> &rocksdb::OptimisticTransactionDB {
95 &self.0
96 }
97}
98
99fn is_power_of_two(num: usize) -> bool {
101 num.is_power_of_two()
102}
103
104impl fmt::Debug for RocksDbReadOnlyTransaction<'_> {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str("RocksDbTransaction")
107 }
108}
109
110impl fmt::Debug for RocksDbTransaction<'_> {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str("RocksDbTransaction")
113 }
114}
115
116#[test]
117fn is_power_of_two_sanity() {
118 assert!(!is_power_of_two(0));
119 assert!(is_power_of_two(1));
120 assert!(is_power_of_two(2));
121 assert!(!is_power_of_two(3));
122 assert!(is_power_of_two(4));
123 assert!(!is_power_of_two(5));
124 assert!(is_power_of_two(2 << 10));
125 assert!(!is_power_of_two((2 << 10) + 1));
126}
127
128const DEFAULT_WRITE_BUFFER_SIZE: usize = 2 * 1024 * 1024;
130
131const DEFAULT_BLOCK_CACHE_SIZE: usize = 2 * 1024 * 1024;
136
137const DEFAULT_MAX_OPEN_FILES: i32 = 256;
140
141fn parse_env_size(env_name: &'static str) -> Result<Option<usize>, RocksDbOpenError> {
142 let Ok(var) = std::env::var(env_name) else {
143 return Ok(None);
144 };
145 let size: usize = FromStr::from_str(&var).map_err(|source| RocksDbOpenError::EnvParse {
146 var: env_name,
147 source,
148 })?;
149 if !is_power_of_two(size) {
150 return Err(RocksDbOpenError::EnvNotPowerOfTwo { var: env_name });
151 }
152 Ok(Some(size))
153}
154
155fn get_default_options() -> Result<rocksdb::Options, RocksDbOpenError> {
156 let mut opts = rocksdb::Options::default();
157
158 let write_buffer_size =
159 parse_env_size(FM_ROCKSDB_WRITE_BUFFER_SIZE_ENV)?.unwrap_or(DEFAULT_WRITE_BUFFER_SIZE);
160 opts.set_write_buffer_size(write_buffer_size);
161
162 opts.set_max_write_buffer_number(2);
164
165 let block_cache_size =
166 parse_env_size(FM_ROCKSDB_BLOCK_CACHE_SIZE_ENV)?.unwrap_or(DEFAULT_BLOCK_CACHE_SIZE);
167 let cache = rocksdb::Cache::new_lru_cache(block_cache_size);
168 let mut block_opts = rocksdb::BlockBasedOptions::default();
169 block_opts.set_block_cache(&cache);
170 block_opts.set_cache_index_and_filter_blocks(true);
173 opts.set_block_based_table_factory(&block_opts);
174
175 opts.set_max_open_files(DEFAULT_MAX_OPEN_FILES);
176
177 debug!(
178 write_buffer_size,
179 block_cache_size,
180 max_open_files = DEFAULT_MAX_OPEN_FILES,
181 "RocksDB memory options"
182 );
183
184 opts.create_if_missing(true);
185 Ok(opts)
186}
187
188#[derive(Debug)]
189pub struct RocksDbReadOnly(rocksdb::DB);
190
191pub struct RocksDbReadOnlyTransaction<'a>(&'a rocksdb::DB);
192
193impl RocksDbReadOnly {
194 #[allow(clippy::unused_async)]
195 pub async fn open_read_only(
196 db_path: impl AsRef<Path>,
197 ) -> Result<RocksDbReadOnly, RocksDbOpenError> {
198 let db_path = db_path.as_ref();
199 block_in_place(|| Self::open_read_only_blocking(db_path))
200 }
201
202 pub fn open_read_only_blocking(db_path: &Path) -> Result<RocksDbReadOnly, RocksDbOpenError> {
203 let opts = get_default_options()?;
204 let db = rocksdb::DB::open_for_read_only(&opts, db_path, false)?;
206 Ok(RocksDbReadOnly(db))
207 }
208}
209
210#[derive(Debug, thiserror::Error)]
212#[non_exhaustive]
213pub enum RocksDbOpenError {
214 #[error("db path must have a base dir")]
217 NoBaseDir,
218
219 #[error(transparent)]
222 CreateDir(#[from] std::io::Error),
223
224 #[error(transparent)]
227 Lock(#[from] DbLockError),
228
229 #[error("Could not parse {var}")]
232 EnvParse {
233 var: &'static str,
235 #[source]
237 source: std::num::ParseIntError,
238 },
239
240 #[error("{var} is not a power of 2")]
242 EnvNotPowerOfTwo {
243 var: &'static str,
245 },
246
247 #[error(transparent)]
249 RocksDb(#[from] rocksdb::Error),
250}
251
252impl From<rocksdb::OptimisticTransactionDB> for RocksDb {
253 fn from(db: OptimisticTransactionDB) -> Self {
254 RocksDb(db)
255 }
256}
257
258impl From<RocksDb> for rocksdb::OptimisticTransactionDB {
259 fn from(db: RocksDb) -> Self {
260 db.0
261 }
262}
263
264fn next_prefix(prefix: &[u8]) -> Option<Vec<u8>> {
270 let mut next_prefix = prefix.to_vec();
271 let mut is_last_prefix = true;
272 for i in (0..next_prefix.len()).rev() {
273 next_prefix[i] = next_prefix[i].wrapping_add(1);
274 if next_prefix[i] > 0 {
275 is_last_prefix = false;
276 break;
277 }
278 }
279 if is_last_prefix {
280 None
283 } else {
284 Some(next_prefix)
285 }
286}
287
288#[async_trait]
289impl IRawDatabase for RocksDb {
290 type Transaction<'a> = RocksDbTransaction<'a>;
291 async fn begin_transaction<'a>(&'a self) -> RocksDbTransaction {
292 let mut optimistic_options = OptimisticTransactionOptions::default();
293 optimistic_options.set_snapshot(true);
294
295 let mut write_options = WriteOptions::default();
296 write_options.set_sync(true);
298
299 RocksDbTransaction(self.0.transaction_opt(&write_options, &optimistic_options))
300 }
301
302 fn checkpoint(&self, backup_path: &Path) -> DatabaseResult<()> {
303 let checkpoint =
304 rocksdb::checkpoint::Checkpoint::new(&self.0).map_err(DatabaseError::backend)?;
305 checkpoint
306 .create_checkpoint(backup_path)
307 .map_err(DatabaseError::backend)?;
308 Ok(())
309 }
310}
311
312#[async_trait]
313impl IRawDatabase for RocksDbReadOnly {
314 type Transaction<'a> = RocksDbReadOnlyTransaction<'a>;
315 async fn begin_transaction<'a>(&'a self) -> RocksDbReadOnlyTransaction<'a> {
316 RocksDbReadOnlyTransaction(&self.0)
317 }
318
319 fn checkpoint(&self, backup_path: &Path) -> DatabaseResult<()> {
320 let checkpoint =
321 rocksdb::checkpoint::Checkpoint::new(&self.0).map_err(DatabaseError::backend)?;
322 checkpoint
323 .create_checkpoint(backup_path)
324 .map_err(DatabaseError::backend)?;
325 Ok(())
326 }
327}
328
329#[async_trait]
330impl IDatabaseTransactionOpsCore for RocksDbTransaction<'_> {
331 async fn raw_insert_bytes(
332 &mut self,
333 key: &[u8],
334 value: &[u8],
335 ) -> DatabaseResult<Option<Vec<u8>>> {
336 fedimint_core::runtime::block_in_place(|| {
337 let val = self.0.snapshot().get(key).unwrap();
338 self.0.put(key, value).map_err(DatabaseError::backend)?;
339 Ok(val)
340 })
341 }
342
343 async fn raw_get_bytes(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
344 fedimint_core::runtime::block_in_place(|| {
345 self.0.snapshot().get(key).map_err(DatabaseError::backend)
346 })
347 }
348
349 async fn raw_remove_entry(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
350 fedimint_core::runtime::block_in_place(|| {
351 let val = self.0.snapshot().get(key).unwrap();
352 self.0.delete(key).map_err(DatabaseError::backend)?;
353 Ok(val)
354 })
355 }
356
357 async fn raw_find_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<PrefixStream<'_>> {
358 Ok(fedimint_core::runtime::block_in_place(|| {
359 let prefix = key_prefix.to_vec();
360 let mut options = rocksdb::ReadOptions::default();
361 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
362 let iter = self.0.snapshot().iterator_opt(
363 rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
364 options,
365 );
366 let rocksdb_iter = iter.map_while(move |res| {
367 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
368 key_bytes
369 .starts_with(&prefix)
370 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
371 });
372 Box::pin(convert_to_async_stream(rocksdb_iter))
373 }))
374 }
375
376 async fn raw_find_by_range(&mut self, range: Range<&[u8]>) -> DatabaseResult<PrefixStream<'_>> {
377 Ok(fedimint_core::runtime::block_in_place(|| {
378 let range = Range {
379 start: range.start.to_vec(),
380 end: range.end.to_vec(),
381 };
382 let mut options = rocksdb::ReadOptions::default();
383 options.set_iterate_range(range.clone());
384 let iter = self.0.snapshot().iterator_opt(
385 rocksdb::IteratorMode::From(&range.start, rocksdb::Direction::Forward),
386 options,
387 );
388 let rocksdb_iter = iter.map_while(move |res| {
389 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
390 (key_bytes.as_ref() < range.end.as_slice())
391 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
392 });
393 Box::pin(convert_to_async_stream(rocksdb_iter))
394 }))
395 }
396
397 async fn raw_remove_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<()> {
398 fedimint_core::runtime::block_in_place(|| {
399 let mut options = rocksdb::ReadOptions::default();
401 options.set_iterate_range(rocksdb::PrefixRange(key_prefix.to_owned()));
402 let iter = self
403 .0
404 .snapshot()
405 .iterator_opt(
406 rocksdb::IteratorMode::From(key_prefix, rocksdb::Direction::Forward),
407 options,
408 )
409 .map_while(|res| {
410 res.map(|(key_bytes, _)| {
411 key_bytes
412 .starts_with(key_prefix)
413 .then_some(key_bytes.to_vec())
414 })
415 .transpose()
416 });
417
418 for item in iter {
419 let key = item.map_err(DatabaseError::backend)?;
420 self.0.delete(key).map_err(DatabaseError::backend)?;
421 }
422
423 Ok(())
424 })
425 }
426
427 async fn raw_find_by_prefix_sorted_descending(
428 &mut self,
429 key_prefix: &[u8],
430 ) -> DatabaseResult<PrefixStream<'_>> {
431 let prefix = key_prefix.to_vec();
432 let next_prefix = next_prefix(&prefix);
433 let iterator_mode = if let Some(next_prefix) = &next_prefix {
434 rocksdb::IteratorMode::From(next_prefix, rocksdb::Direction::Reverse)
435 } else {
436 rocksdb::IteratorMode::End
437 };
438 Ok(fedimint_core::runtime::block_in_place(|| {
439 let mut options = rocksdb::ReadOptions::default();
440 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
441 let iter = self.0.snapshot().iterator_opt(iterator_mode, options);
442 let rocksdb_iter = iter.map_while(move |res| {
443 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
444 key_bytes
445 .starts_with(&prefix)
446 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
447 });
448 Box::pin(convert_to_async_stream(rocksdb_iter))
449 }))
450 }
451}
452
453impl IDatabaseTransactionOps for RocksDbTransaction<'_> {}
454
455#[async_trait]
456impl IRawDatabaseTransaction for RocksDbTransaction<'_> {
457 async fn commit_tx(self) -> DatabaseResult<()> {
458 fedimint_core::runtime::block_in_place(|| {
459 match self.0.commit() {
460 Ok(()) => Ok(()),
461 Err(err) => {
462 match err.kind() {
476 rocksdb::ErrorKind::Busy => Err(DatabaseError::WriteConflict),
477 rocksdb::ErrorKind::TryAgain => Err(DatabaseError::snapshot_too_old(err)),
478 _ => Err(DatabaseError::backend(err)),
479 }
480 }
481 }
482 })
483 }
484}
485
486#[async_trait]
487impl IDatabaseTransactionOpsCore for RocksDbReadOnlyTransaction<'_> {
488 async fn raw_insert_bytes(
489 &mut self,
490 _key: &[u8],
491 _value: &[u8],
492 ) -> DatabaseResult<Option<Vec<u8>>> {
493 panic!("Cannot insert into a read only transaction");
494 }
495
496 async fn raw_get_bytes(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
497 fedimint_core::runtime::block_in_place(|| {
498 self.0.snapshot().get(key).map_err(DatabaseError::backend)
499 })
500 }
501
502 async fn raw_remove_entry(&mut self, _key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
503 panic!("Cannot remove from a read only transaction");
504 }
505
506 async fn raw_find_by_range(&mut self, range: Range<&[u8]>) -> DatabaseResult<PrefixStream<'_>> {
507 Ok(fedimint_core::runtime::block_in_place(|| {
508 let range = Range {
509 start: range.start.to_vec(),
510 end: range.end.to_vec(),
511 };
512 let mut options = rocksdb::ReadOptions::default();
513 options.set_iterate_range(range.clone());
514 let iter = self.0.snapshot().iterator_opt(
515 rocksdb::IteratorMode::From(&range.start, rocksdb::Direction::Forward),
516 options,
517 );
518 let rocksdb_iter = iter.map_while(move |res| {
519 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
520 (key_bytes.as_ref() < range.end.as_slice())
521 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
522 });
523 Box::pin(convert_to_async_stream(rocksdb_iter))
524 }))
525 }
526
527 async fn raw_find_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<PrefixStream<'_>> {
528 Ok(fedimint_core::runtime::block_in_place(|| {
529 let prefix = key_prefix.to_vec();
530 let mut options = rocksdb::ReadOptions::default();
531 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
532 let iter = self.0.snapshot().iterator_opt(
533 rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
534 options,
535 );
536 let rocksdb_iter = iter.map_while(move |res| {
537 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
538 key_bytes
539 .starts_with(&prefix)
540 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
541 });
542 Box::pin(convert_to_async_stream(rocksdb_iter))
543 }))
544 }
545
546 async fn raw_remove_by_prefix(&mut self, _key_prefix: &[u8]) -> DatabaseResult<()> {
547 panic!("Cannot remove from a read only transaction");
548 }
549
550 async fn raw_find_by_prefix_sorted_descending(
551 &mut self,
552 key_prefix: &[u8],
553 ) -> DatabaseResult<PrefixStream<'_>> {
554 let prefix = key_prefix.to_vec();
555 let next_prefix = next_prefix(&prefix);
556 let iterator_mode = if let Some(next_prefix) = &next_prefix {
557 rocksdb::IteratorMode::From(next_prefix, rocksdb::Direction::Reverse)
558 } else {
559 rocksdb::IteratorMode::End
560 };
561 Ok(fedimint_core::runtime::block_in_place(|| {
562 let mut options = rocksdb::ReadOptions::default();
563 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
564 let iter = self.0.snapshot().iterator_opt(iterator_mode, options);
565 let rocksdb_iter = iter.map_while(move |res| {
566 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
567 key_bytes
568 .starts_with(&prefix)
569 .then_some((key_bytes.to_vec(), value_bytes.to_vec()))
570 });
571 Box::pin(stream::iter(rocksdb_iter))
572 }))
573 }
574}
575
576impl IDatabaseTransactionOps for RocksDbReadOnlyTransaction<'_> {}
577
578#[async_trait]
579impl IRawDatabaseTransaction for RocksDbReadOnlyTransaction<'_> {
580 async fn commit_tx(self) -> DatabaseResult<()> {
581 panic!("Cannot commit a read only transaction");
582 }
583}
584
585#[cfg(test)]
586mod fedimint_rocksdb_tests;