1#[cfg(feature = "rocksdb")]
2use crate::state::{
3 historical_rocksdb::StateRewindPolicy,
4 rocks_db::DatabaseConfig,
5};
6
7use crate::{
8 database::{
9 Database,
10 GenesisDatabase,
11 Result as DatabaseResult,
12 database_description::{
13 compression::CompressionDatabase,
14 gas_price::GasPriceDatabase,
15 off_chain::OffChain,
16 on_chain::OnChain,
17 relayer::Relayer,
18 },
19 },
20 service::DbType,
21};
22#[cfg(feature = "test-helpers")]
23use fuel_core_chain_config::{
24 StateConfig,
25 StateConfigBuilder,
26};
27#[cfg(feature = "backup")]
28use fuel_core_services::TraceErr;
29use fuel_core_storage::Result as StorageResult;
30#[cfg(feature = "test-helpers")]
31use fuel_core_storage::tables::{
32 Coins,
33 ContractsAssets,
34 ContractsLatestUtxo,
35 ContractsRawCode,
36 ContractsState,
37 Messages,
38};
39use fuel_core_types::{
40 blockchain::primitives::DaBlockHeight,
41 fuel_types::BlockHeight,
42};
43use std::path::PathBuf;
44
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CombinedDatabaseConfig {
47 pub database_path: PathBuf,
48 pub database_type: DbType,
49 #[cfg(feature = "rocksdb")]
50 pub database_config: DatabaseConfig,
51 #[cfg(feature = "rocksdb")]
52 pub state_rewind_policy: StateRewindPolicy,
53}
54
55#[derive(Default, Clone)]
57pub struct CombinedDatabase {
58 on_chain: Database<OnChain>,
59 off_chain: Database<OffChain>,
60 relayer: Database<Relayer>,
61 gas_price: Database<GasPriceDatabase>,
62 compression: Database<CompressionDatabase>,
63}
64
65impl CombinedDatabase {
66 pub fn new(
67 on_chain: Database<OnChain>,
68 off_chain: Database<OffChain>,
69 relayer: Database<Relayer>,
70 gas_price: Database<GasPriceDatabase>,
71 compression: Database<CompressionDatabase>,
72 ) -> Self {
73 Self {
74 on_chain,
75 off_chain,
76 relayer,
77 gas_price,
78 compression,
79 }
80 }
81
82 #[cfg(feature = "rocksdb")]
83 pub fn prune(path: &std::path::Path) -> crate::database::Result<()> {
84 crate::state::rocks_db::RocksDb::<OnChain>::prune(path)?;
85 crate::state::rocks_db::RocksDb::<OffChain>::prune(path)?;
86 crate::state::rocks_db::RocksDb::<Relayer>::prune(path)?;
87 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::prune(path)?;
88 crate::state::rocks_db::RocksDb::<CompressionDatabase>::prune(path)?;
89 Ok(())
90 }
91
92 #[cfg(feature = "backup")]
93 pub fn backup(
94 db_dir: &std::path::Path,
95 backup_dir: &std::path::Path,
96 ) -> crate::database::Result<()> {
97 use tempfile::TempDir;
98
99 let temp_backup_dir = TempDir::new()
100 .trace_err("Failed to create temporary backup directory")
101 .map_err(|e| anyhow::anyhow!(e))?;
102
103 Self::backup_databases(db_dir, temp_backup_dir.path())?;
104
105 std::fs::rename(temp_backup_dir.path(), backup_dir)
106 .trace_err("Failed to move temporary backup directory")
107 .map_err(|e| anyhow::anyhow!(e))?;
108
109 Ok(())
110 }
111
112 #[cfg(feature = "backup")]
113 fn backup_databases(
114 db_dir: &std::path::Path,
115 temp_dir: &std::path::Path,
116 ) -> crate::database::Result<()> {
117 crate::state::rocks_db::RocksDb::<OnChain>::backup(db_dir, temp_dir)
118 .trace_err("Failed to backup on-chain database")?;
119
120 crate::state::rocks_db::RocksDb::<OffChain>::backup(db_dir, temp_dir)
121 .trace_err("Failed to backup off-chain database")?;
122
123 crate::state::rocks_db::RocksDb::<Relayer>::backup(db_dir, temp_dir)
124 .trace_err("Failed to backup relayer database")?;
125
126 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::backup(db_dir, temp_dir)
127 .trace_err("Failed to backup gas-price database")?;
128
129 crate::state::rocks_db::RocksDb::<CompressionDatabase>::backup(db_dir, temp_dir)
130 .trace_err("Failed to backup compression database")?;
131
132 Ok(())
133 }
134
135 #[cfg(feature = "backup")]
136 pub fn restore(
137 restore_to: &std::path::Path,
138 backup_dir: &std::path::Path,
139 ) -> crate::database::Result<()> {
140 use tempfile::TempDir;
141
142 let temp_restore_dir = TempDir::new()
143 .trace_err("Failed to create temporary restore directory")
144 .map_err(|e| anyhow::anyhow!(e))?;
145
146 Self::restore_database(backup_dir, temp_restore_dir.path())?;
147
148 std::fs::rename(temp_restore_dir.path(), restore_to)
149 .trace_err("Failed to move temporary restore directory")
150 .map_err(|e| anyhow::anyhow!(e))?;
151
152 Ok(())
155 }
156
157 #[cfg(feature = "backup")]
158 fn restore_database(
159 backup_dir: &std::path::Path,
160 temp_restore_dir: &std::path::Path,
161 ) -> crate::database::Result<()> {
162 crate::state::rocks_db::RocksDb::<OnChain>::restore(temp_restore_dir, backup_dir)
163 .trace_err("Failed to restore on-chain database")?;
164
165 crate::state::rocks_db::RocksDb::<OffChain>::restore(
166 temp_restore_dir,
167 backup_dir,
168 )
169 .trace_err("Failed to restore off-chain database")?;
170
171 crate::state::rocks_db::RocksDb::<Relayer>::restore(temp_restore_dir, backup_dir)
172 .trace_err("Failed to restore relayer database")?;
173
174 crate::state::rocks_db::RocksDb::<GasPriceDatabase>::restore(
175 temp_restore_dir,
176 backup_dir,
177 )
178 .trace_err("Failed to restore gas-price database")?;
179
180 crate::state::rocks_db::RocksDb::<CompressionDatabase>::restore(
181 temp_restore_dir,
182 backup_dir,
183 )
184 .trace_err("Failed to restore compression database")?;
185
186 Ok(())
187 }
188
189 #[cfg(feature = "rocksdb")]
190 pub fn open(
191 path: &std::path::Path,
192 state_rewind_policy: StateRewindPolicy,
193 database_config: DatabaseConfig,
194 ) -> crate::database::Result<Self> {
195 let max_fds = match database_config.max_fds {
198 -1 => -1,
199 _ => database_config.max_fds.saturating_div(4),
200 };
201
202 let on_chain = Database::open_rocksdb(
204 path,
205 state_rewind_policy,
206 DatabaseConfig {
207 max_fds,
208 ..database_config
209 },
210 )?;
211 let off_chain = Database::open_rocksdb(
212 path,
213 state_rewind_policy,
214 DatabaseConfig {
215 max_fds,
216 ..database_config
217 },
218 )?;
219 let relayer = Database::open_rocksdb(
220 path,
221 state_rewind_policy,
222 DatabaseConfig {
223 max_fds,
224 ..database_config
225 },
226 )?;
227 let gas_price = Database::open_rocksdb(
228 path,
229 state_rewind_policy,
230 DatabaseConfig {
231 max_fds,
232 ..database_config
233 },
234 )?;
235 let compression = Database::open_rocksdb(
236 path,
237 state_rewind_policy,
238 DatabaseConfig {
239 max_fds,
240 ..database_config
241 },
242 )?;
243 Ok(Self {
244 on_chain,
245 off_chain,
246 relayer,
247 gas_price,
248 compression,
249 })
250 }
251
252 #[cfg(feature = "rocksdb")]
254 pub fn temp_database_with_state_rewind_policy(
255 state_rewind_policy: StateRewindPolicy,
256 database_config: DatabaseConfig,
257 ) -> DatabaseResult<Self> {
258 Ok(Self {
259 on_chain: Database::rocksdb_temp(state_rewind_policy, database_config)?,
260 off_chain: Database::rocksdb_temp(state_rewind_policy, database_config)?,
261 relayer: Default::default(),
262 gas_price: Default::default(),
263 compression: Default::default(),
264 })
265 }
266
267 pub fn from_config(config: &CombinedDatabaseConfig) -> DatabaseResult<Self> {
268 let combined_database = match config.database_type {
269 #[cfg(feature = "rocksdb")]
270 DbType::RocksDb => {
271 if config.database_path.as_os_str().is_empty() {
273 tracing::warn!(
274 "No RocksDB path configured, initializing database with a tmp directory"
275 );
276 CombinedDatabase::temp_database_with_state_rewind_policy(
277 config.state_rewind_policy,
278 config.database_config,
279 )?
280 } else {
281 tracing::info!(
282 "Opening database {:?} with cache size \"{:?}\" and state rewind policy \"{:?}\"",
283 config.database_path,
284 config.database_config.cache_capacity,
285 config.state_rewind_policy,
286 );
287 CombinedDatabase::open(
288 &config.database_path,
289 config.state_rewind_policy,
290 config.database_config,
291 )?
292 }
293 }
294 DbType::InMemory => CombinedDatabase::in_memory(),
295 #[cfg(not(feature = "rocksdb"))]
296 _ => CombinedDatabase::in_memory(),
297 };
298
299 Ok(combined_database)
300 }
301
302 pub fn in_memory() -> Self {
303 Self::new(
304 Database::in_memory(),
305 Database::in_memory(),
306 Database::in_memory(),
307 Database::in_memory(),
308 Database::in_memory(),
309 )
310 }
311
312 pub fn check_version(&self) -> StorageResult<()> {
313 self.on_chain.check_version()?;
314 self.off_chain.check_version()?;
315 self.relayer.check_version()?;
316 self.gas_price.check_version()?;
317 self.compression.check_version()?;
318 Ok(())
319 }
320
321 pub fn on_chain(&self) -> &Database<OnChain> {
322 &self.on_chain
323 }
324
325 pub fn compression(&self) -> &Database<CompressionDatabase> {
326 &self.compression
327 }
328
329 #[cfg(any(feature = "test-helpers", test))]
330 pub fn on_chain_mut(&mut self) -> &mut Database<OnChain> {
331 &mut self.on_chain
332 }
333
334 pub fn off_chain(&self) -> &Database<OffChain> {
335 &self.off_chain
336 }
337
338 #[cfg(any(feature = "test-helpers", test))]
339 pub fn off_chain_mut(&mut self) -> &mut Database<OffChain> {
340 &mut self.off_chain
341 }
342
343 pub fn relayer(&self) -> &Database<Relayer> {
344 &self.relayer
345 }
346
347 #[cfg(any(feature = "test-helpers", test))]
348 pub fn relayer_mut(&mut self) -> &mut Database<Relayer> {
349 &mut self.relayer
350 }
351
352 pub fn gas_price(&self) -> &Database<GasPriceDatabase> {
353 &self.gas_price
354 }
355
356 #[cfg(any(feature = "test-helpers", test))]
357 pub fn gas_price_mut(&mut self) -> &mut Database<GasPriceDatabase> {
358 &mut self.gas_price
359 }
360
361 #[cfg(feature = "test-helpers")]
362 pub fn read_state_config(&self) -> StorageResult<StateConfig> {
363 use fuel_core_chain_config::AddTable;
364 use fuel_core_producer::ports::BlockProducerDatabase;
365 use fuel_core_storage::transactional::AtomicView;
366 use fuel_core_types::fuel_vm::BlobData;
367 use itertools::Itertools;
368 let mut builder = StateConfigBuilder::default();
369
370 macro_rules! add_tables {
371 ($($table: ty),*) => {
372 $(
373 let table = self
374 .on_chain()
375 .entries::<$table>(None, fuel_core_storage::iter::IterDirection::Forward)
376 .try_collect()?;
377 builder.add(table);
378 )*
379 };
380 }
381
382 add_tables!(
383 Coins,
384 Messages,
385 BlobData,
386 ContractsAssets,
387 ContractsState,
388 ContractsRawCode,
389 ContractsLatestUtxo
390 );
391
392 let view = self.on_chain().latest_view()?;
393 let latest_block = view.latest_block()?;
394 let blocks_root =
395 view.block_header_merkle_root(latest_block.header().height())?;
396 let state_config =
397 builder.build(Some(fuel_core_chain_config::LastBlockConfig::from_header(
398 latest_block.header(),
399 blocks_root,
400 )))?;
401
402 Ok(state_config)
403 }
404
405 pub fn rollback_to<S>(
407 &self,
408 target_block_height: BlockHeight,
409 shutdown_listener: &mut S,
410 ) -> anyhow::Result<()>
411 where
412 S: ShutdownListener,
413 {
414 while !shutdown_listener.is_cancelled() {
415 let on_chain_height = self
416 .on_chain()
417 .latest_height_from_metadata()?
418 .ok_or(anyhow::anyhow!("on-chain database doesn't have height"))?;
419
420 let off_chain_height = self
421 .off_chain()
422 .latest_height_from_metadata()?
423 .ok_or(anyhow::anyhow!("off-chain database doesn't have height"))?;
424
425 let gas_price_chain_height =
426 self.gas_price().latest_height_from_metadata()?;
427 let gas_price_rolled_back =
428 is_equal_or_none(gas_price_chain_height, target_block_height);
429
430 let compression_db_height =
431 self.compression().latest_height_from_metadata()?;
432 let compression_db_rolled_back =
433 is_equal_or_none(compression_db_height, target_block_height);
434
435 if on_chain_height == target_block_height
436 && off_chain_height == target_block_height
437 && gas_price_rolled_back
438 && compression_db_rolled_back
439 {
440 break;
441 }
442
443 if on_chain_height < target_block_height {
444 return Err(anyhow::anyhow!(
445 "on-chain database height({on_chain_height}) \
446 is less than target height({target_block_height})"
447 ));
448 }
449
450 if off_chain_height < target_block_height {
451 return Err(anyhow::anyhow!(
452 "off-chain database height({off_chain_height}) \
453 is less than target height({target_block_height})"
454 ));
455 }
456
457 if let Some(gas_price_chain_height) = gas_price_chain_height
458 && gas_price_chain_height < target_block_height
459 {
460 return Err(anyhow::anyhow!(
461 "gas-price database height({gas_price_chain_height}) \
462 is less than target height({target_block_height})"
463 ));
464 }
465
466 if let Some(compression_db_height) = compression_db_height
467 && compression_db_height < target_block_height
468 {
469 return Err(anyhow::anyhow!(
470 "compression database height({compression_db_height}) \
471 is less than target height({target_block_height})"
472 ));
473 }
474
475 if on_chain_height > target_block_height {
476 self.on_chain().rollback_last_block()?;
477 }
478
479 if off_chain_height > target_block_height {
480 self.off_chain().rollback_last_block()?;
481 }
482
483 if let Some(gas_price_chain_height) = gas_price_chain_height
484 && gas_price_chain_height > target_block_height
485 {
486 self.gas_price().rollback_last_block()?;
487 }
488
489 if let Some(compression_db_height) = compression_db_height
490 && compression_db_height > target_block_height
491 {
492 self.compression().rollback_last_block()?;
493 }
494 }
495
496 if shutdown_listener.is_cancelled() {
497 return Err(anyhow::anyhow!(
498 "Stop the rollback due to shutdown signal received"
499 ));
500 }
501
502 Ok(())
503 }
504
505 pub fn rollback_relayer_to<S>(
507 &self,
508 target_da_height: DaBlockHeight,
509 shutdown_listener: &mut S,
510 ) -> anyhow::Result<()>
511 where
512 S: ShutdownListener,
513 {
514 while !shutdown_listener.is_cancelled() {
515 let relayer_db_height = self.relayer().latest_height_from_metadata()?;
516 let relayer_db_rolled_back =
517 is_equal_or_none(relayer_db_height, target_da_height);
518
519 if relayer_db_rolled_back {
520 break;
521 }
522
523 if let Some(relayer_db_height) = relayer_db_height
524 && relayer_db_height < target_da_height
525 {
526 return Err(anyhow::anyhow!(
527 "relayer database height({relayer_db_height}) \
528 is less than target height({target_da_height})"
529 ));
530 }
531
532 if let Some(relayer_db_height) = relayer_db_height
533 && relayer_db_height > target_da_height
534 {
535 self.relayer().rollback_last_block()?;
536 }
537 }
538
539 if shutdown_listener.is_cancelled() {
540 return Err(anyhow::anyhow!(
541 "Stop the rollback due to shutdown signal received"
542 ));
543 }
544
545 Ok(())
546 }
547
548 pub fn sync_aux_db_heights<S>(&self, shutdown_listener: &mut S) -> anyhow::Result<()>
555 where
556 S: ShutdownListener,
557 {
558 while !shutdown_listener.is_cancelled() {
559 let on_chain_height = match self.on_chain().latest_height_from_metadata()? {
560 Some(height) => height,
561 None => break, };
563
564 let off_chain_height = self.off_chain().latest_height_from_metadata()?;
565 let gas_price_height = self.gas_price().latest_height_from_metadata()?;
566
567 if let Some(off_height) = off_chain_height
569 && off_height > on_chain_height
570 {
571 self.off_chain().rollback_last_block()?;
572 }
573
574 if let Some(gas_height) = gas_price_height
576 && gas_height > on_chain_height
577 {
578 self.gas_price().rollback_last_block()?;
579 }
580
581 if off_chain_height.is_none_or(|h| h <= on_chain_height)
583 && gas_price_height.is_none_or(|h| h <= on_chain_height)
584 {
585 break;
586 }
587 }
588
589 Ok(())
590 }
591
592 pub fn shutdown(self) {
593 self.on_chain.shutdown();
594 self.off_chain.shutdown();
595 self.relayer.shutdown();
596 self.gas_price.shutdown();
597 self.compression.shutdown();
598 }
599}
600
601pub trait ShutdownListener {
603 fn is_cancelled(&self) -> bool;
605}
606
607#[derive(Default, Clone)]
610pub struct CombinedGenesisDatabase {
611 pub on_chain: GenesisDatabase<OnChain>,
612 pub off_chain: GenesisDatabase<OffChain>,
613}
614
615impl CombinedGenesisDatabase {
616 pub fn on_chain(&self) -> &GenesisDatabase<OnChain> {
617 &self.on_chain
618 }
619
620 pub fn off_chain(&self) -> &GenesisDatabase<OffChain> {
621 &self.off_chain
622 }
623}
624
625fn is_equal_or_none<T: PartialEq>(maybe_left: Option<T>, right: T) -> bool {
626 maybe_left.map(|left| left == right).unwrap_or(true)
627}
628
629#[allow(non_snake_case)]
630#[cfg(feature = "backup")]
631#[cfg(test)]
632mod tests {
633 use super::*;
634 use fuel_core_storage::{
635 StorageAsMut,
636 StorageAsRef,
637 };
638 use fuel_core_types::{
639 entities::coins::coin::CompressedCoin,
640 fuel_tx::UtxoId,
641 };
642 use tempfile::TempDir;
643
644 #[test]
645 fn backup_and_restore__works_correctly__happy_path() {
646 let db_dir = TempDir::new().unwrap();
648 let mut combined_db = CombinedDatabase::open(
649 db_dir.path(),
650 StateRewindPolicy::NoRewind,
651 DatabaseConfig::config_for_tests(),
652 )
653 .unwrap();
654 let key = UtxoId::new(Default::default(), Default::default());
655 let expected_value = CompressedCoin::default();
656
657 let on_chain_db = combined_db.on_chain_mut();
658 on_chain_db
659 .storage_as_mut::<Coins>()
660 .insert(&key, &expected_value)
661 .unwrap();
662 drop(combined_db);
663
664 let backup_dir = TempDir::new().unwrap();
666 CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();
667
668 let restore_dir = TempDir::new().unwrap();
670 CombinedDatabase::restore(restore_dir.path(), backup_dir.path()).unwrap();
671 let restored_db = CombinedDatabase::open(
672 restore_dir.path(),
673 StateRewindPolicy::NoRewind,
674 DatabaseConfig::config_for_tests(),
675 )
676 .unwrap();
677
678 let restored_on_chain_db = restored_db.on_chain();
679 let restored_value = restored_on_chain_db
680 .storage::<Coins>()
681 .get(&key)
682 .unwrap()
683 .unwrap()
684 .into_owned();
685 assert_eq!(expected_value, restored_value);
686
687 std::fs::remove_dir_all(db_dir.path()).unwrap();
689 std::fs::remove_dir_all(backup_dir.path()).unwrap();
690 std::fs::remove_dir_all(restore_dir.path()).unwrap();
691 }
692
693 #[test]
694 fn backup__when_backup_fails_it_should_not_leave_any_residue() {
695 use std::os::unix::fs::PermissionsExt;
696
697 let db_dir = TempDir::new().unwrap();
699 let mut combined_db = CombinedDatabase::open(
700 db_dir.path(),
701 StateRewindPolicy::NoRewind,
702 DatabaseConfig::config_for_tests(),
703 )
704 .unwrap();
705 let key = UtxoId::new(Default::default(), Default::default());
706 let expected_value = CompressedCoin::default();
707
708 let on_chain_db = combined_db.on_chain_mut();
709 on_chain_db
710 .storage_as_mut::<Coins>()
711 .insert(&key, &expected_value)
712 .unwrap();
713 drop(combined_db);
714
715 std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o030))
718 .unwrap();
719 let backup_dir = TempDir::new().unwrap();
720
721 CombinedDatabase::backup(db_dir.path(), backup_dir.path())
723 .expect_err("Backup should fail");
724 let backup_dir_contents = std::fs::read_dir(backup_dir.path()).unwrap();
725 assert_eq!(backup_dir_contents.count(), 0);
726
727 std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o770))
729 .unwrap();
730 std::fs::remove_dir_all(db_dir.path()).unwrap();
731 std::fs::remove_dir_all(backup_dir.path()).unwrap();
732 }
733
734 #[test]
735 fn restore__when_restore_fails_it_should_not_leave_any_residue() {
736 use std::os::unix::fs::PermissionsExt;
737
738 let db_dir = TempDir::new().unwrap();
740 let mut combined_db = CombinedDatabase::open(
741 db_dir.path(),
742 StateRewindPolicy::NoRewind,
743 DatabaseConfig::config_for_tests(),
744 )
745 .unwrap();
746 let key = UtxoId::new(Default::default(), Default::default());
747 let expected_value = CompressedCoin::default();
748
749 let on_chain_db = combined_db.on_chain_mut();
750 on_chain_db
751 .storage_as_mut::<Coins>()
752 .insert(&key, &expected_value)
753 .unwrap();
754 drop(combined_db);
755
756 let backup_dir = TempDir::new().unwrap();
757 CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();
758
759 std::fs::set_permissions(
762 backup_dir.path(),
763 std::fs::Permissions::from_mode(0o030),
764 )
765 .unwrap();
766 let restore_dir = TempDir::new().unwrap();
767
768 CombinedDatabase::restore(restore_dir.path(), backup_dir.path())
770 .expect_err("Restore should fail");
771 let restore_dir_contents = std::fs::read_dir(restore_dir.path()).unwrap();
772 assert_eq!(restore_dir_contents.count(), 0);
773
774 std::fs::set_permissions(
776 backup_dir.path(),
777 std::fs::Permissions::from_mode(0o770),
778 )
779 .unwrap();
780 std::fs::remove_dir_all(db_dir.path()).unwrap();
781 std::fs::remove_dir_all(backup_dir.path()).unwrap();
782 std::fs::remove_dir_all(restore_dir.path()).unwrap();
783 }
784
785 #[test]
786 fn backup__should_be_successful_if_db_is_already_opened() {
787 let db_dir = TempDir::new().unwrap();
789 let mut combined_db = CombinedDatabase::open(
790 db_dir.path(),
791 StateRewindPolicy::NoRewind,
792 DatabaseConfig::config_for_tests(),
793 )
794 .unwrap();
795 let key = UtxoId::new(Default::default(), Default::default());
796 let expected_value = CompressedCoin::default();
797
798 let on_chain_db = combined_db.on_chain_mut();
799 on_chain_db
800 .storage_as_mut::<Coins>()
801 .insert(&key, &expected_value)
802 .unwrap();
803
804 let backup_dir = TempDir::new().unwrap();
806 let res = CombinedDatabase::backup(db_dir.path(), backup_dir.path());
810 assert!(res.is_ok());
811
812 std::fs::remove_dir_all(db_dir.path()).unwrap();
814 std::fs::remove_dir_all(backup_dir.path()).unwrap();
815 }
816}