Skip to main content

reth_db/
mdbx.rs

1//! Helper functions for initializing and opening a database.
2
3use crate::{is_database_empty, TableSet, Tables};
4use eyre::Context;
5use std::path::Path;
6use tracing::{info, warn};
7
8pub use crate::implementation::mdbx::*;
9use crate::version::{check_db_version_file, create_db_version_file, DatabaseVersionError};
10pub use reth_libmdbx::*;
11
12/// Tables that have been removed from the schema but may still exist on disk from previous
13/// versions. These will be dropped during database initialization.
14const ORPHAN_TABLES: &[&str] = &["AccountsTrieChangeSets", "StoragesTrieChangeSets"];
15
16/// Checks if the given path resides on a ZFS filesystem and logs a warning.
17///
18/// ZFS uses copy-on-write (COW) semantics which conflict with MDBX's write patterns, leading to
19/// significant performance degradation.
20fn warn_if_zfs(path: &Path) {
21    if matches!(is_zfs(path), Ok(true)) {
22        warn!(
23            target: "reth::db",
24            path = %path.display(),
25            "Database is on a ZFS filesystem. ZFS's copy-on-write behavior causes significant \
26             performance degradation with MDBX. Consider using ext4 or xfs instead."
27        );
28    }
29}
30
31/// Returns `true` if the given path is on a ZFS filesystem.
32#[cfg(target_os = "linux")]
33fn is_zfs(path: &Path) -> std::io::Result<bool> {
34    use std::{ffi::CString, os::unix::ffi::OsStrExt};
35
36    /// ZFS filesystem magic number.
37    const ZFS_SUPER_MAGIC: i64 = 0x2fc12fc1;
38
39    let c_path = CString::new(path.as_os_str().as_bytes())
40        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
41
42    unsafe {
43        let mut stat: libc::statfs = std::mem::zeroed();
44        if libc::statfs(c_path.as_ptr(), &raw mut stat) == 0 {
45            Ok(stat.f_type == ZFS_SUPER_MAGIC)
46        } else {
47            Err(std::io::Error::last_os_error())
48        }
49    }
50}
51
52/// Returns `true` if the given path is on a ZFS filesystem.
53#[cfg(target_os = "macos")]
54fn is_zfs(path: &Path) -> std::io::Result<bool> {
55    use std::{ffi::CString, os::unix::ffi::OsStrExt};
56
57    let c_path = CString::new(path.as_os_str().as_bytes())
58        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
59
60    unsafe {
61        let mut stat: libc::statfs = std::mem::zeroed();
62        if libc::statfs(c_path.as_ptr(), &raw mut stat) == 0 {
63            let fstype = std::ffi::CStr::from_ptr(stat.f_fstypename.as_ptr());
64            Ok(fstype.to_bytes() == b"zfs")
65        } else {
66            Err(std::io::Error::last_os_error())
67        }
68    }
69}
70
71/// ZFS detection is unsupported on this platform, always returns `Ok(false)`.
72#[cfg(not(any(target_os = "linux", target_os = "macos")))]
73fn is_zfs(_path: &Path) -> std::io::Result<bool> {
74    Ok(false)
75}
76
77/// Creates a new database at the specified path if it doesn't exist. Does NOT create tables. Check
78/// [`init_db`].
79pub fn create_db<P: AsRef<Path>>(path: P, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
80    let rpath = path.as_ref();
81    warn_if_zfs(rpath);
82
83    if is_database_empty(rpath) {
84        reth_fs_util::create_dir_all(rpath)
85            .wrap_err_with(|| format!("Could not create database directory {}", rpath.display()))?;
86        create_db_version_file(rpath)?;
87    } else {
88        match check_db_version_file(rpath) {
89            Ok(_) => (),
90            Err(DatabaseVersionError::MissingFile) => create_db_version_file(rpath)?,
91            Err(err) => return Err(err.into()),
92        }
93    }
94
95    Ok(DatabaseEnv::open(rpath, DatabaseEnvKind::RW, args)?)
96}
97
98/// Opens up an existing database or creates a new one at the specified path. Creates tables defined
99/// in [`Tables`] if necessary. Read/Write mode.
100pub fn init_db<P: AsRef<Path>>(path: P, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
101    init_db_for::<P, Tables>(path, args)
102}
103
104/// Opens up an existing database or creates a new one at the specified path. Creates tables defined
105/// in the given [`TableSet`] if necessary. Read/Write mode.
106pub fn init_db_for<P: AsRef<Path>, TS: TableSet>(
107    path: P,
108    args: DatabaseArguments,
109) -> eyre::Result<DatabaseEnv> {
110    let client_version = args.client_version().clone();
111    let mut db = create_db(path, args)?;
112    db.create_and_track_tables_for::<TS>()?;
113    db.record_client_version(client_version)?;
114    drop_orphan_tables(&db);
115    Ok(db)
116}
117
118/// Drops orphaned tables that are no longer part of the schema.
119fn drop_orphan_tables(db: &DatabaseEnv) {
120    for table_name in ORPHAN_TABLES {
121        match db.drop_orphan_table(table_name) {
122            Ok(true) => {
123                info!(target: "reth::db", table = %table_name, "Dropped orphaned database table");
124            }
125            Ok(false) => {}
126            Err(e) => {
127                tracing::warn!(
128                    target: "reth::db",
129                    table = %table_name,
130                    %e,
131                    "Failed to drop orphaned database table"
132                );
133            }
134        }
135    }
136}
137
138/// Opens up an existing database. Read only mode. It doesn't create it or create tables if missing.
139pub fn open_db_read_only(
140    path: impl AsRef<Path>,
141    args: DatabaseArguments,
142) -> eyre::Result<DatabaseEnv> {
143    let path = path.as_ref();
144    DatabaseEnv::open(path, DatabaseEnvKind::RO, args)
145        .with_context(|| format!("Could not open database at path: {}", path.display()))
146}
147
148/// Opens up an existing database. Read/Write mode with `WriteMap` enabled. It doesn't create it or
149/// create tables if missing.
150pub fn open_db(path: impl AsRef<Path>, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
151    fn open(path: &Path, args: DatabaseArguments) -> eyre::Result<DatabaseEnv> {
152        let client_version = args.client_version().clone();
153        let db = DatabaseEnv::open(path, DatabaseEnvKind::RW, args)
154            .with_context(|| format!("Could not open database at path: {}", path.display()))?;
155        db.record_client_version(client_version)?;
156        Ok(db)
157    }
158    open(path.as_ref(), args)
159}