Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rocksdb): remove getters for internal rocksdb handles, expose backup instead #2535

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [2439](https://github.com/FuelLabs/fuel-core/pull/2439): Add gas costs for the two new zk opcodes `ecop` and `eadd` and the benches that allow to calibrate them.
- [2472](https://github.com/FuelLabs/fuel-core/pull/2472): Added the `amountU128` field to the `Balance` GraphQL schema, providing the total balance as a `U128`. The existing `amount` field clamps any balance exceeding `U64` to `u64::MAX`.
- [2526](https://github.com/FuelLabs/fuel-core/pull/2526): Add possibility to not have any cache set for RocksDB. Add an option to either load the RocksDB columns families on creation of the database or when the column is used.
- [2532](https://github.com/FuelLabs/fuel-core/pull/2532): Getters for inner rocksdb database handles.
- [2535](https://github.com/FuelLabs/fuel-core/pull/2535): Expose `backup` and `restore` APIs on the `CombinedDatabase` struct to create portable backups and restore from them.

### Fixed
- [2365](https://github.com/FuelLabs/fuel-core/pull/2365): Fixed the error during dry run in the case of race condition.
Expand Down
1 change: 1 addition & 0 deletions crates/database/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ fuel-core-trace = { path = "../trace" }

[features]
test-helpers = []
backup = []
12 changes: 12 additions & 0 deletions crates/database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ pub enum Error {
#[display(fmt = "Reached the end of the history")]
ReachedEndOfHistory,

#[cfg(feature = "backup")]
#[display(fmt = "BackupEngine initialization error: {}", _0)]
BackupEngineInitError(anyhow::Error),

#[cfg(feature = "backup")]
#[display(fmt = "Backup error: {}", _0)]
BackupError(anyhow::Error),

#[cfg(feature = "backup")]
#[display(fmt = "Restore error: {}", _0)]
RestoreError(anyhow::Error),

/// Not related to database error.
#[from]
Other(anyhow::Error),
Expand Down
1 change: 1 addition & 0 deletions crates/fuel-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ smt = [
p2p = ["dep:fuel-core-p2p", "dep:fuel-core-sync"]
relayer = ["dep:fuel-core-relayer"]
rocksdb = ["dep:rocksdb", "dep:tempfile", "dep:num_cpus", "dep:postcard"]
backup = ["rocksdb", "fuel-core-database/backup"]
test-helpers = [
"fuel-core-database/test-helpers",
"fuel-core-p2p?/test-helpers",
Expand Down
336 changes: 336 additions & 0 deletions crates/fuel-core/src/combined_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,156 @@ impl CombinedDatabase {
Ok(())
}

#[cfg(all(feature = "rocksdb", feature = "backup"))]
AurelienFT marked this conversation as resolved.
Show resolved Hide resolved
pub fn backup(
db_dir: &std::path::Path,
backup_dir: &std::path::Path,
) -> crate::database::Result<()> {
use tempfile::TempDir;

let temp_backup_dir = TempDir::new().map_err(|e| {
crate::database::Error::BackupError(anyhow::anyhow!(
"Failed to create temporary backup directory: {}",
e
))
})?;

let mut must_rollback = false;

for _ in 0..1 {
if let Err(e) = crate::state::rocks_db::RocksDb::<OnChain>::backup(
db_dir,
temp_backup_dir.path(),
) {
tracing::error!("Failed to backup on-chain database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<OffChain>::backup(
db_dir,
temp_backup_dir.path(),
) {
tracing::error!("Failed to backup off-chain database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<Relayer>::backup(
db_dir,
temp_backup_dir.path(),
) {
tracing::error!("Failed to backup relayer database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<GasPriceDatabase>::backup(
db_dir,
temp_backup_dir.path(),
) {
tracing::error!("Failed to backup gas-price database: {}", e);
must_rollback = true;
break;
}
}

if must_rollback {
std::fs::remove_dir_all(temp_backup_dir.path()).map_err(|e| {
crate::database::Error::BackupError(anyhow::anyhow!(
"Failed to remove temporary backup directory: {}",
e
))
})?;

return Err(crate::database::Error::BackupError(anyhow::anyhow!(
"Failed to backup databases"
)));
}

std::fs::rename(temp_backup_dir.path(), backup_dir).map_err(|e| {
crate::database::Error::BackupError(anyhow::anyhow!(
"Failed to move temporary backup directory: {}",
e
))
})?;

Ok(())
}

#[cfg(all(feature = "rocksdb", feature = "backup"))]
pub fn restore(
restore_to: &std::path::Path,
backup_dir: &std::path::Path,
) -> crate::database::Result<()> {
use tempfile::TempDir;

let temp_restore_dir = TempDir::new().map_err(|e| {
crate::database::Error::RestoreError(anyhow::anyhow!(
"Failed to create temporary restore directory: {}",
e
))
})?;

let mut must_rollback = false;

for _ in 0..1 {
if let Err(e) = crate::state::rocks_db::RocksDb::<OnChain>::restore(
temp_restore_dir.path(),
backup_dir,
) {
tracing::error!("Failed to restore on-chain database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<OffChain>::restore(
temp_restore_dir.path(),
backup_dir,
) {
tracing::error!("Failed to restore off-chain database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<Relayer>::restore(
temp_restore_dir.path(),
backup_dir,
) {
tracing::error!("Failed to restore relayer database: {}", e);
must_rollback = true;
break;
}
if let Err(e) = crate::state::rocks_db::RocksDb::<GasPriceDatabase>::restore(
temp_restore_dir.path(),
backup_dir,
) {
tracing::error!("Failed to restore gas-price database: {}", e);
must_rollback = true;
break;
}
}

if must_rollback {
std::fs::remove_dir_all(temp_restore_dir.path()).map_err(|e| {
crate::database::Error::RestoreError(anyhow::anyhow!(
"Failed to remove temporary restore directory: {}",
e
))
})?;

return Err(crate::database::Error::RestoreError(anyhow::anyhow!(
"Failed to restore databases"
)));
}

std::fs::rename(temp_restore_dir.path(), restore_to).map_err(|e| {
crate::database::Error::RestoreError(anyhow::anyhow!(
"Failed to move temporary restore directory: {}",
e
))
})?;

// we don't return a CombinedDatabase here
// because the consumer can use any db config while opening it
Ok(())
}

#[cfg(feature = "rocksdb")]
pub fn open(
path: &std::path::Path,
Expand Down Expand Up @@ -426,3 +576,189 @@ impl CombinedGenesisDatabase {
&self.off_chain
}
}

#[allow(non_snake_case)]
#[cfg(all(feature = "backup", feature = "rocksdb"))]
#[cfg(test)]
mod tests {
use super::*;
use fuel_core_storage::StorageAsMut;
use fuel_core_types::{
entities::coins::coin::CompressedCoin,
fuel_tx::UtxoId,
};
use tempfile::TempDir;

#[test]
fn backup_and_restore__works_correctly__happy_path() {
// given
let db_dir = TempDir::new().unwrap();
let mut combined_db = CombinedDatabase::open(
db_dir.path(),
StateRewindPolicy::NoRewind,
DatabaseConfig::config_for_tests(),
)
.unwrap();
let key = UtxoId::new(Default::default(), Default::default());
let expected_value = CompressedCoin::default();

let on_chain_db = combined_db.on_chain_mut();
on_chain_db
.storage_as_mut::<Coins>()
.insert(&key, &expected_value)
.unwrap();
drop(combined_db);

// when
let backup_dir = TempDir::new().unwrap();
CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();

// then
let restore_dir = TempDir::new().unwrap();
CombinedDatabase::restore(restore_dir.path(), backup_dir.path()).unwrap();
let restored_db = CombinedDatabase::open(
restore_dir.path(),
StateRewindPolicy::NoRewind,
DatabaseConfig::config_for_tests(),
)
.unwrap();

let mut restored_on_chain_db = restored_db.on_chain();
let restored_value = restored_on_chain_db
.storage::<Coins>()
.get(&key)
.unwrap()
.unwrap()
.into_owned();
assert_eq!(expected_value, restored_value);

// cleanup
std::fs::remove_dir_all(db_dir.path()).unwrap();
std::fs::remove_dir_all(backup_dir.path()).unwrap();
std::fs::remove_dir_all(restore_dir.path()).unwrap();
}

#[test]
fn backup__when_backup_fails_it_should_not_leave_any_residue() {
use std::os::unix::fs::PermissionsExt;

// given
let db_dir = TempDir::new().unwrap();
let mut combined_db = CombinedDatabase::open(
db_dir.path(),
StateRewindPolicy::NoRewind,
DatabaseConfig::config_for_tests(),
)
.unwrap();
let key = UtxoId::new(Default::default(), Default::default());
let expected_value = CompressedCoin::default();

let on_chain_db = combined_db.on_chain_mut();
on_chain_db
.storage_as_mut::<Coins>()
.insert(&key, &expected_value)
.unwrap();
drop(combined_db);

// when
// we set the permissions of db_dir to not allow reading
std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o030))
.unwrap();
let backup_dir = TempDir::new().unwrap();

// then
CombinedDatabase::backup(db_dir.path(), backup_dir.path())
.expect_err("Backup should fail");
let backup_dir_contents = std::fs::read_dir(backup_dir.path()).unwrap();
assert_eq!(backup_dir_contents.count(), 0);

// cleanup
std::fs::set_permissions(db_dir.path(), std::fs::Permissions::from_mode(0o770))
.unwrap();
std::fs::remove_dir_all(db_dir.path()).unwrap();
std::fs::remove_dir_all(backup_dir.path()).unwrap();
}

#[test]
fn restore__when_restore_fails_it_should_not_leave_any_residue() {
use std::os::unix::fs::PermissionsExt;

// given
let db_dir = TempDir::new().unwrap();
let mut combined_db = CombinedDatabase::open(
db_dir.path(),
StateRewindPolicy::NoRewind,
DatabaseConfig::config_for_tests(),
)
.unwrap();
let key = UtxoId::new(Default::default(), Default::default());
let expected_value = CompressedCoin::default();

let on_chain_db = combined_db.on_chain_mut();
on_chain_db
.storage_as_mut::<Coins>()
.insert(&key, &expected_value)
.unwrap();
drop(combined_db);

let backup_dir = TempDir::new().unwrap();
CombinedDatabase::backup(db_dir.path(), backup_dir.path()).unwrap();

// when
// we set the permissions of backup_dir to not allow reading
std::fs::set_permissions(
backup_dir.path(),
std::fs::Permissions::from_mode(0o030),
)
.unwrap();
let restore_dir = TempDir::new().unwrap();

// then
CombinedDatabase::restore(restore_dir.path(), backup_dir.path())
.expect_err("Restore should fail");
let restore_dir_contents = std::fs::read_dir(restore_dir.path()).unwrap();
assert_eq!(restore_dir_contents.count(), 0);

// cleanup
std::fs::set_permissions(
backup_dir.path(),
std::fs::Permissions::from_mode(0o770),
)
.unwrap();
std::fs::remove_dir_all(db_dir.path()).unwrap();
std::fs::remove_dir_all(backup_dir.path()).unwrap();
std::fs::remove_dir_all(restore_dir.path()).unwrap();
}

#[test]
fn backup__cannot_backup_while_db_is_opened() {
// given
let db_dir = TempDir::new().unwrap();
let mut combined_db = CombinedDatabase::open(
db_dir.path(),
StateRewindPolicy::NoRewind,
DatabaseConfig::config_for_tests(),
)
.unwrap();
let key = UtxoId::new(Default::default(), Default::default());
let expected_value = CompressedCoin::default();

let on_chain_db = combined_db.on_chain_mut();
on_chain_db
.storage_as_mut::<Coins>()
.insert(&key, &expected_value)
.unwrap();

// when
let backup_dir = TempDir::new().unwrap();
// no drop for combined_db

// then
CombinedDatabase::backup(db_dir.path(), backup_dir.path())
.expect_err("Backup should fail");

// cleanup
std::fs::remove_dir_all(db_dir.path()).unwrap();
std::fs::remove_dir_all(backup_dir.path()).unwrap();
}
}
Loading
Loading