feat(bin, storage): db versioning (#3130)

This commit is contained in:
Alexey Shekhirin
2023-06-19 17:43:17 +01:00
committed by GitHub
parent d02c87d20e
commit 2b6a0468fc
11 changed files with 204 additions and 17 deletions

3
Cargo.lock generated
View File

@ -5097,6 +5097,7 @@ name = "reth-db"
version = "0.1.0"
dependencies = [
"arbitrary",
"assert_matches",
"async-trait",
"bytes",
"criterion",
@ -5127,6 +5128,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"vergen",
]
[[package]]
@ -5740,6 +5742,7 @@ dependencies = [
name = "reth-staged-sync"
version = "0.1.0"
dependencies = [
"assert_matches",
"async-trait",
"confy",
"enr 0.8.1",

View File

@ -10,6 +10,7 @@ use eyre::WrapErr;
use human_bytes::human_bytes;
use reth_db::{database::Database, tables};
use reth_primitives::ChainSpec;
use reth_staged_sync::utils::init::init_db;
use std::sync::Arc;
use tracing::error;
@ -92,13 +93,8 @@ impl Command {
// add network name to data dir
let data_dir = self.datadir.unwrap_or_chain_default(self.chain.chain);
let db_path = data_dir.db_path();
std::fs::create_dir_all(&db_path)?;
// TODO: Auto-impl for Database trait
let db = reth_db::mdbx::Env::<reth_db::mdbx::WriteMap>::open(
db_path.as_ref(),
reth_db::mdbx::EnvKind::RW,
)?;
let db = init_db(&db_path)?;
let mut tool = DbTool::new(&db, self.chain.clone())?;

View File

@ -98,13 +98,8 @@ impl Command {
let data_dir = self.datadir.unwrap_or_chain_default(self.chain.chain);
let db_path = data_dir.db_path();
info!(target: "reth::cli", path = ?db_path, "Opening database");
std::fs::create_dir_all(&db_path)?;
// TODO: Auto-impl for Database trait
let db = reth_db::mdbx::Env::<reth_db::mdbx::WriteMap>::open(
db_path.as_ref(),
reth_db::mdbx::EnvKind::RW,
)?;
let db = Arc::new(init_db(db_path)?);
info!(target: "reth::cli", "Database opened");
let mut tool = DbTool::new(&db, self.chain.clone())?;

View File

@ -120,6 +120,8 @@ impl Command {
info!(target: "reth::cli", path = ?db_path, "Opening database");
let db = Arc::new(init_db(db_path)?);
info!(target: "reth::cli", "Database opened");
let factory = ProviderFactory::new(&db, self.chain.clone());
let mut provider_rw = factory.provider_rw().map_err(PipelineError::Interface)?;

View File

@ -81,6 +81,7 @@ secp256k1 = { workspace = true, features = ["global-context", "rand-std", "recov
confy = "0.5"
tempfile = "3.4"
assert_matches = "1.5.0"
[features]
test-utils = [

View File

@ -1,21 +1,37 @@
use eyre::WrapErr;
use reth_db::{
cursor::DbCursorRO,
database::{Database, DatabaseGAT},
is_database_empty,
mdbx::{Env, WriteMap},
tables,
transaction::{DbTx, DbTxMut},
version::{check_db_version_file, create_db_version_file, DatabaseVersionError},
};
use reth_primitives::{stage::StageId, Account, Bytecode, ChainSpec, H256, U256};
use reth_provider::{
AccountWriter, DatabaseProviderRW, PostState, ProviderFactory, TransactionError,
};
use std::{path::Path, sync::Arc};
use std::{fs, path::Path, sync::Arc};
use tracing::debug;
/// Opens up an existing database or creates a new one at the specified path.
pub fn init_db<P: AsRef<Path>>(path: P) -> eyre::Result<Env<WriteMap>> {
std::fs::create_dir_all(path.as_ref())?;
if is_database_empty(&path) {
fs::create_dir_all(&path).wrap_err_with(|| {
format!("Could not create database directory {}", path.as_ref().display())
})?;
create_db_version_file(&path)?;
} else {
match check_db_version_file(&path) {
Ok(_) => (),
Err(DatabaseVersionError::MissingFile) => create_db_version_file(&path)?,
Err(err) => return Err(err.into()),
}
}
let db = Env::<WriteMap>::open(path.as_ref(), reth_db::mdbx::EnvKind::RW)?;
db.create_tables()?;
Ok(db)
@ -165,11 +181,17 @@ pub fn insert_genesis_header<DB: Database>(
#[cfg(test)]
mod tests {
use super::{init_genesis, InitDatabaseError};
use reth_db::mdbx::test_utils::create_test_rw_db;
use super::{init_db, init_genesis, InitDatabaseError};
use assert_matches::assert_matches;
use reth_db::{
mdbx::test_utils::create_test_rw_db,
version::{db_version_file_path, DatabaseVersionError},
};
use reth_primitives::{
GOERLI, GOERLI_GENESIS, MAINNET, MAINNET_GENESIS, SEPOLIA, SEPOLIA_GENESIS,
};
use std::fs;
use tempfile::tempdir;
#[test]
fn success_init_genesis_mainnet() {
@ -214,4 +236,43 @@ mod tests {
}
)
}
#[test]
fn db_version() {
let path = tempdir().unwrap();
// Database is empty
{
let db = init_db(&path);
assert_matches!(db, Ok(_));
}
// Database is not empty, current version is the same as in the file
{
let db = init_db(&path);
assert_matches!(db, Ok(_));
}
// Database is not empty, version file is malformed
{
fs::write(path.path().join(db_version_file_path(&path)), "invalid-version").unwrap();
let db = init_db(&path);
assert!(db.is_err());
assert_matches!(
db.unwrap_err().downcast_ref::<DatabaseVersionError>(),
Some(DatabaseVersionError::MalformedFile)
)
}
// Database is not empty, version file contains not matching version
{
fs::write(path.path().join(db_version_file_path(&path)), "0").unwrap();
let db = init_db(&path);
assert!(db.is_err());
assert_matches!(
db.unwrap_err().downcast_ref::<DatabaseVersionError>(),
Some(DatabaseVersionError::VersionMismatch { version: 0 })
)
}
}
}

View File

@ -71,6 +71,11 @@ serde_json = { workspace = true }
paste = "1.0"
assert_matches = "1.5.0"
[build-dependencies]
vergen = { version = "8.0.0", features = ["git", "gitcl"] }
[features]
default = ["mdbx"]
test-utils = ["tempfile", "arbitrary"]

View File

@ -0,0 +1,8 @@
use std::error::Error;
use vergen::EmitBuilder;
fn main() -> Result<(), Box<dyn Error>> {
// Emit the instructions
EmitBuilder::builder().git_sha(true).emit()?;
Ok(())
}

View File

@ -68,6 +68,7 @@ pub mod abstraction;
mod implementation;
pub mod tables;
mod utils;
pub mod version;
#[cfg(feature = "mdbx")]
/// Bindings for [MDBX](https://libmdbx.dqdkfa.ru/).
@ -79,3 +80,4 @@ pub mod mdbx {
pub use abstraction::*;
pub use reth_interfaces::db::DatabaseError;
pub use tables::*;
pub use utils::is_database_empty;

View File

@ -1,5 +1,7 @@
//! Utils crate for `db`.
use std::path::Path;
/// Returns the default page size that can be used in this OS.
pub(crate) fn default_page_size() -> usize {
let os_page_size = page_size::get();
@ -13,3 +15,17 @@ pub(crate) fn default_page_size() -> usize {
os_page_size.clamp(min_page_size, libmdbx_max_page_size)
}
/// Check if a db is empty. It does not provide any information on the
/// validity of the data in it. We consider a database as non empty when it's a non empty directory.
pub fn is_database_empty<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
if !path.exists() {
true
} else if let Ok(dir) = path.read_dir() {
dir.count() == 0
} else {
true
}
}

View File

@ -0,0 +1,98 @@
//! Database version utils.
use std::{
fs, io,
path::{Path, PathBuf},
};
/// The name of the file that contains the version of the database.
pub const DB_VERSION_FILE_NAME: &str = "database.version";
/// The version of the database stored in the [DB_VERSION_FILE_NAME] file in the same directory as
/// database. Example: `1`.
pub const DB_VERSION: u64 = 1;
/// Error when checking a database version using [check_db_version_file]
#[allow(missing_docs)]
#[derive(thiserror::Error, Debug)]
pub enum DatabaseVersionError {
#[error("Unable to determine the version of the database, file is missing.")]
MissingFile,
#[error("Unable to determine the version of the database, file is malformed.")]
MalformedFile,
#[error(
"Breaking database change detected. \
Your database version (v{version}) is incompatible with the latest database version (v{}).",
DB_VERSION.to_string()
)]
VersionMismatch { version: u64 },
#[error("IO error occurred while reading {path}: {err}")]
IORead { err: io::Error, path: PathBuf },
}
/// Checks the database version file with [DB_VERSION_FILE_NAME] name.
///
/// Returns [Ok] if file is found and has one line which equals to [DB_VERSION].
/// Otherwise, returns different [DatabaseVersionError] error variants.
pub fn check_db_version_file<P: AsRef<Path>>(db_path: P) -> Result<(), DatabaseVersionError> {
let version_file_path = db_version_file_path(db_path);
match fs::read_to_string(&version_file_path) {
Ok(raw_version) => {
let version =
raw_version.parse::<u64>().map_err(|_| DatabaseVersionError::MalformedFile)?;
if version != DB_VERSION {
return Err(DatabaseVersionError::VersionMismatch { version })
}
Ok(())
}
Err(err) if err.kind() == io::ErrorKind::NotFound => Err(DatabaseVersionError::MissingFile),
Err(err) => Err(DatabaseVersionError::IORead { err, path: version_file_path }),
}
}
/// Creates a database version file with [DB_VERSION_FILE_NAME] name containing [DB_VERSION] string.
///
/// This function will create a file if it does not exist,
/// and will entirely replace its contents if it does.
pub fn create_db_version_file<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
fs::write(db_version_file_path(db_path), DB_VERSION.to_string())
}
/// Returns a database version file path.
pub fn db_version_file_path<P: AsRef<Path>>(db_path: P) -> PathBuf {
db_path.as_ref().join(DB_VERSION_FILE_NAME)
}
#[cfg(test)]
mod tests {
use super::{check_db_version_file, db_version_file_path, DatabaseVersionError};
use assert_matches::assert_matches;
use std::fs;
use tempfile::tempdir;
#[test]
fn missing_file() {
let dir = tempdir().unwrap();
let result = check_db_version_file(&dir);
assert_matches!(result, Err(DatabaseVersionError::MissingFile));
}
#[test]
fn malformed_file() {
let dir = tempdir().unwrap();
fs::write(db_version_file_path(&dir), "invalid-version").unwrap();
let result = check_db_version_file(&dir);
assert_matches!(result, Err(DatabaseVersionError::MalformedFile));
}
#[test]
fn version_mismatch() {
let dir = tempdir().unwrap();
fs::write(db_version_file_path(&dir), "0").unwrap();
let result = check_db_version_file(&dir);
assert_matches!(result, Err(DatabaseVersionError::VersionMismatch { version: 0 }));
}
}