mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(bin, storage): db versioning (#3130)
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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())?;
|
||||
|
||||
|
||||
@ -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())?;
|
||||
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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 })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
|
||||
8
crates/storage/db/build.rs
Normal file
8
crates/storage/db/build.rs
Normal 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(())
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
98
crates/storage/db/src/version.rs
Normal file
98
crates/storage/db/src/version.rs
Normal 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 }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user