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 = 64 * 1024 * 1024;
138
139const DEFAULT_MAX_OPEN_FILES: i32 = 256;
142
143fn parse_env_size(env_name: &'static str) -> Result<Option<usize>, RocksDbOpenError> {
144 let Ok(var) = std::env::var(env_name) else {
145 return Ok(None);
146 };
147 let size: usize = FromStr::from_str(&var).map_err(|source| RocksDbOpenError::EnvParse {
148 var: env_name,
149 source,
150 })?;
151 if !is_power_of_two(size) {
152 return Err(RocksDbOpenError::EnvNotPowerOfTwo { var: env_name });
153 }
154 Ok(Some(size))
155}
156
157fn get_default_options() -> Result<rocksdb::Options, RocksDbOpenError> {
158 let mut opts = rocksdb::Options::default();
159
160 let write_buffer_size =
161 parse_env_size(FM_ROCKSDB_WRITE_BUFFER_SIZE_ENV)?.unwrap_or(DEFAULT_WRITE_BUFFER_SIZE);
162 opts.set_write_buffer_size(write_buffer_size);
163
164 opts.set_max_write_buffer_number(2);
166
167 let block_cache_size =
168 parse_env_size(FM_ROCKSDB_BLOCK_CACHE_SIZE_ENV)?.unwrap_or(DEFAULT_BLOCK_CACHE_SIZE);
169 let mut cache_opts = rocksdb::LruCacheOptions::default();
174 cache_opts.set_capacity(block_cache_size);
175 cache_opts.set_num_shard_bits(0);
176 let cache = rocksdb::Cache::new_lru_cache_opts(&cache_opts);
177 let mut block_opts = rocksdb::BlockBasedOptions::default();
178 block_opts.set_block_cache(&cache);
179 block_opts.set_cache_index_and_filter_blocks(true);
182 opts.set_block_based_table_factory(&block_opts);
183
184 opts.set_max_open_files(DEFAULT_MAX_OPEN_FILES);
185
186 debug!(
187 write_buffer_size,
188 block_cache_size,
189 max_open_files = DEFAULT_MAX_OPEN_FILES,
190 "RocksDB memory options"
191 );
192
193 opts.create_if_missing(true);
194 Ok(opts)
195}
196
197#[derive(Debug)]
198pub struct RocksDbReadOnly(rocksdb::DB);
199
200pub struct RocksDbReadOnlyTransaction<'a>(&'a rocksdb::DB);
201
202impl RocksDbReadOnly {
203 #[allow(clippy::unused_async)]
204 pub async fn open_read_only(
205 db_path: impl AsRef<Path>,
206 ) -> Result<RocksDbReadOnly, RocksDbOpenError> {
207 let db_path = db_path.as_ref();
208 block_in_place(|| Self::open_read_only_blocking(db_path))
209 }
210
211 pub fn open_read_only_blocking(db_path: &Path) -> Result<RocksDbReadOnly, RocksDbOpenError> {
212 let opts = get_default_options()?;
213 let db = rocksdb::DB::open_for_read_only(&opts, db_path, false)?;
215 Ok(RocksDbReadOnly(db))
216 }
217}
218
219#[derive(Debug, thiserror::Error)]
221#[non_exhaustive]
222pub enum RocksDbOpenError {
223 #[error("db path must have a base dir")]
226 NoBaseDir,
227
228 #[error(transparent)]
231 CreateDir(#[from] std::io::Error),
232
233 #[error(transparent)]
236 Lock(#[from] DbLockError),
237
238 #[error("Could not parse {var}")]
241 EnvParse {
242 var: &'static str,
244 #[source]
246 source: std::num::ParseIntError,
247 },
248
249 #[error("{var} is not a power of 2")]
251 EnvNotPowerOfTwo {
252 var: &'static str,
254 },
255
256 #[error(transparent)]
258 RocksDb(#[from] rocksdb::Error),
259}
260
261impl From<rocksdb::OptimisticTransactionDB> for RocksDb {
262 fn from(db: OptimisticTransactionDB) -> Self {
263 RocksDb(db)
264 }
265}
266
267impl From<RocksDb> for rocksdb::OptimisticTransactionDB {
268 fn from(db: RocksDb) -> Self {
269 db.0
270 }
271}
272
273fn next_prefix(prefix: &[u8]) -> Option<Vec<u8>> {
279 let mut next_prefix = prefix.to_vec();
280 let mut is_last_prefix = true;
281 for i in (0..next_prefix.len()).rev() {
282 next_prefix[i] = next_prefix[i].wrapping_add(1);
283 if next_prefix[i] > 0 {
284 is_last_prefix = false;
285 break;
286 }
287 }
288 if is_last_prefix {
289 None
292 } else {
293 Some(next_prefix)
294 }
295}
296
297#[async_trait]
298impl IRawDatabase for RocksDb {
299 type Transaction<'a> = RocksDbTransaction<'a>;
300 async fn begin_transaction<'a>(&'a self) -> RocksDbTransaction {
301 let mut optimistic_options = OptimisticTransactionOptions::default();
302 optimistic_options.set_snapshot(true);
303
304 let mut write_options = WriteOptions::default();
305 write_options.set_sync(true);
307
308 RocksDbTransaction(self.0.transaction_opt(&write_options, &optimistic_options))
309 }
310
311 fn checkpoint(&self, backup_path: &Path) -> DatabaseResult<()> {
312 let checkpoint =
313 rocksdb::checkpoint::Checkpoint::new(&self.0).map_err(DatabaseError::backend)?;
314 checkpoint
315 .create_checkpoint(backup_path)
316 .map_err(DatabaseError::backend)?;
317 Ok(())
318 }
319}
320
321#[async_trait]
322impl IRawDatabase for RocksDbReadOnly {
323 type Transaction<'a> = RocksDbReadOnlyTransaction<'a>;
324 async fn begin_transaction<'a>(&'a self) -> RocksDbReadOnlyTransaction<'a> {
325 RocksDbReadOnlyTransaction(&self.0)
326 }
327
328 fn checkpoint(&self, backup_path: &Path) -> DatabaseResult<()> {
329 let checkpoint =
330 rocksdb::checkpoint::Checkpoint::new(&self.0).map_err(DatabaseError::backend)?;
331 checkpoint
332 .create_checkpoint(backup_path)
333 .map_err(DatabaseError::backend)?;
334 Ok(())
335 }
336}
337
338#[async_trait]
339impl IDatabaseTransactionOpsCore for RocksDbTransaction<'_> {
340 async fn raw_insert_bytes_no_previous(
341 &mut self,
342 key: &[u8],
343 value: &[u8],
344 ) -> DatabaseResult<()> {
345 fedimint_core::runtime::block_in_place(|| {
346 self.0.put(key, value).map_err(DatabaseError::backend)
347 })
348 }
349
350 async fn raw_remove_entry_no_previous(&mut self, key: &[u8]) -> DatabaseResult<()> {
351 fedimint_core::runtime::block_in_place(|| {
352 self.0.delete(key).map_err(DatabaseError::backend)
353 })
354 }
355
356 async fn raw_insert_bytes(
357 &mut self,
358 key: &[u8],
359 value: &[u8],
360 ) -> DatabaseResult<Option<Vec<u8>>> {
361 fedimint_core::runtime::block_in_place(|| {
362 let val = self.0.snapshot().get(key).unwrap();
363 self.0.put(key, value).map_err(DatabaseError::backend)?;
364 Ok(val)
365 })
366 }
367
368 async fn raw_get_bytes(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
369 fedimint_core::runtime::block_in_place(|| {
370 self.0.snapshot().get(key).map_err(DatabaseError::backend)
371 })
372 }
373
374 async fn raw_remove_entry(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
375 fedimint_core::runtime::block_in_place(|| {
376 let val = self.0.snapshot().get(key).unwrap();
377 self.0.delete(key).map_err(DatabaseError::backend)?;
378 Ok(val)
379 })
380 }
381
382 async fn raw_find_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<PrefixStream<'_>> {
383 Ok(fedimint_core::runtime::block_in_place(|| {
384 let prefix = key_prefix.to_vec();
385 let mut options = rocksdb::ReadOptions::default();
386 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
387 let iter = self.0.snapshot().iterator_opt(
388 rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
389 options,
390 );
391 let rocksdb_iter = iter.map_while(move |res| {
392 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
393 key_bytes
394 .starts_with(&prefix)
395 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
396 });
397 Box::pin(convert_to_async_stream(rocksdb_iter))
398 }))
399 }
400
401 async fn raw_find_by_range(&mut self, range: Range<&[u8]>) -> DatabaseResult<PrefixStream<'_>> {
402 Ok(fedimint_core::runtime::block_in_place(|| {
403 let range = Range {
404 start: range.start.to_vec(),
405 end: range.end.to_vec(),
406 };
407 let mut options = rocksdb::ReadOptions::default();
408 options.set_iterate_range(range.clone());
409 let iter = self.0.snapshot().iterator_opt(
410 rocksdb::IteratorMode::From(&range.start, rocksdb::Direction::Forward),
411 options,
412 );
413 let rocksdb_iter = iter.map_while(move |res| {
414 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
415 (key_bytes.as_ref() < range.end.as_slice())
416 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
417 });
418 Box::pin(convert_to_async_stream(rocksdb_iter))
419 }))
420 }
421
422 async fn raw_remove_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<()> {
423 fedimint_core::runtime::block_in_place(|| {
424 let mut options = rocksdb::ReadOptions::default();
426 options.set_iterate_range(rocksdb::PrefixRange(key_prefix.to_owned()));
427 let iter = self
428 .0
429 .snapshot()
430 .iterator_opt(
431 rocksdb::IteratorMode::From(key_prefix, rocksdb::Direction::Forward),
432 options,
433 )
434 .map_while(|res| {
435 res.map(|(key_bytes, _)| {
436 key_bytes
437 .starts_with(key_prefix)
438 .then_some(key_bytes.into_vec())
439 })
440 .transpose()
441 });
442
443 for item in iter {
444 let key = item.map_err(DatabaseError::backend)?;
445 self.0.delete(key).map_err(DatabaseError::backend)?;
446 }
447
448 Ok(())
449 })
450 }
451
452 async fn raw_find_by_prefix_sorted_descending(
453 &mut self,
454 key_prefix: &[u8],
455 ) -> DatabaseResult<PrefixStream<'_>> {
456 let prefix = key_prefix.to_vec();
457 let next_prefix = next_prefix(&prefix);
458 let iterator_mode = if let Some(next_prefix) = &next_prefix {
459 rocksdb::IteratorMode::From(next_prefix, rocksdb::Direction::Reverse)
460 } else {
461 rocksdb::IteratorMode::End
462 };
463 Ok(fedimint_core::runtime::block_in_place(|| {
464 let mut options = rocksdb::ReadOptions::default();
465 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
466 let iter = self.0.snapshot().iterator_opt(iterator_mode, options);
467 let rocksdb_iter = iter.map_while(move |res| {
468 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
469 key_bytes
470 .starts_with(&prefix)
471 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
472 });
473 Box::pin(convert_to_async_stream(rocksdb_iter))
474 }))
475 }
476}
477
478impl IDatabaseTransactionOps for RocksDbTransaction<'_> {}
479
480#[async_trait]
481impl IRawDatabaseTransaction for RocksDbTransaction<'_> {
482 async fn commit_tx(self) -> DatabaseResult<()> {
483 fedimint_core::runtime::block_in_place(|| {
484 match self.0.commit() {
485 Ok(()) => Ok(()),
486 Err(err) => {
487 match err.kind() {
501 rocksdb::ErrorKind::Busy => Err(DatabaseError::WriteConflict),
502 rocksdb::ErrorKind::TryAgain => Err(DatabaseError::snapshot_too_old(err)),
503 _ => Err(DatabaseError::backend(err)),
504 }
505 }
506 }
507 })
508 }
509}
510
511#[async_trait]
512impl IDatabaseTransactionOpsCore for RocksDbReadOnlyTransaction<'_> {
513 async fn raw_insert_bytes(
514 &mut self,
515 _key: &[u8],
516 _value: &[u8],
517 ) -> DatabaseResult<Option<Vec<u8>>> {
518 panic!("Cannot insert into a read only transaction");
519 }
520
521 async fn raw_get_bytes(&mut self, key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
522 fedimint_core::runtime::block_in_place(|| {
523 self.0.snapshot().get(key).map_err(DatabaseError::backend)
524 })
525 }
526
527 async fn raw_remove_entry(&mut self, _key: &[u8]) -> DatabaseResult<Option<Vec<u8>>> {
528 panic!("Cannot remove from a read only transaction");
529 }
530
531 async fn raw_find_by_range(&mut self, range: Range<&[u8]>) -> DatabaseResult<PrefixStream<'_>> {
532 Ok(fedimint_core::runtime::block_in_place(|| {
533 let range = Range {
534 start: range.start.to_vec(),
535 end: range.end.to_vec(),
536 };
537 let mut options = rocksdb::ReadOptions::default();
538 options.set_iterate_range(range.clone());
539 let iter = self.0.snapshot().iterator_opt(
540 rocksdb::IteratorMode::From(&range.start, rocksdb::Direction::Forward),
541 options,
542 );
543 let rocksdb_iter = iter.map_while(move |res| {
544 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
545 (key_bytes.as_ref() < range.end.as_slice())
546 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
547 });
548 Box::pin(convert_to_async_stream(rocksdb_iter))
549 }))
550 }
551
552 async fn raw_find_by_prefix(&mut self, key_prefix: &[u8]) -> DatabaseResult<PrefixStream<'_>> {
553 Ok(fedimint_core::runtime::block_in_place(|| {
554 let prefix = key_prefix.to_vec();
555 let mut options = rocksdb::ReadOptions::default();
556 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
557 let iter = self.0.snapshot().iterator_opt(
558 rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
559 options,
560 );
561 let rocksdb_iter = iter.map_while(move |res| {
562 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
563 key_bytes
564 .starts_with(&prefix)
565 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
566 });
567 Box::pin(convert_to_async_stream(rocksdb_iter))
568 }))
569 }
570
571 async fn raw_remove_by_prefix(&mut self, _key_prefix: &[u8]) -> DatabaseResult<()> {
572 panic!("Cannot remove from a read only transaction");
573 }
574
575 async fn raw_find_by_prefix_sorted_descending(
576 &mut self,
577 key_prefix: &[u8],
578 ) -> DatabaseResult<PrefixStream<'_>> {
579 let prefix = key_prefix.to_vec();
580 let next_prefix = next_prefix(&prefix);
581 let iterator_mode = if let Some(next_prefix) = &next_prefix {
582 rocksdb::IteratorMode::From(next_prefix, rocksdb::Direction::Reverse)
583 } else {
584 rocksdb::IteratorMode::End
585 };
586 Ok(fedimint_core::runtime::block_in_place(|| {
587 let mut options = rocksdb::ReadOptions::default();
588 options.set_iterate_range(rocksdb::PrefixRange(prefix.clone()));
589 let iter = self.0.snapshot().iterator_opt(iterator_mode, options);
590 let rocksdb_iter = iter.map_while(move |res| {
591 let (key_bytes, value_bytes) = res.expect("Error reading from RocksDb");
592 key_bytes
593 .starts_with(&prefix)
594 .then_some((key_bytes.into_vec(), value_bytes.into_vec()))
595 });
596 Box::pin(stream::iter(rocksdb_iter))
597 }))
598 }
599}
600
601impl IDatabaseTransactionOps for RocksDbReadOnlyTransaction<'_> {}
602
603#[async_trait]
604impl IRawDatabaseTransaction for RocksDbReadOnlyTransaction<'_> {
605 async fn commit_tx(self) -> DatabaseResult<()> {
606 panic!("Cannot commit a read only transaction");
607 }
608}
609
610#[cfg(test)]
611mod fedimint_rocksdb_tests;