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