Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 79 additions & 1 deletion crates/cli/commands/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ use reth_node_core::{
DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs,
NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, StaticFilesArgs, TxPoolArgs,
},
dirs::DataDirPath,
node_config::NodeConfig,
utils::is_disk_space_low,
version,
};
use reth_tasks::{shutdown::Shutdown, TaskExecutor};
use std::{ffi::OsString, fmt, path::PathBuf, sync::Arc};
use tracing::error;

/// Start the node
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -172,6 +176,9 @@ where
ext,
} = self;

// Save min_free_disk before moving datadir
let min_free_disk_mb = datadir.min_free_disk;

// set up node config
let mut node_config = NodeConfig {
datadir,
Expand All @@ -195,16 +202,45 @@ where
let data_dir = node_config.datadir();
let db_path = data_dir.db();

// Check disk space at startup if min_free_disk is configured
if min_free_disk_mb > 0 {
use reth_node_core::utils::is_disk_space_low;
if is_disk_space_low(data_dir.data_dir(), min_free_disk_mb) {
eyre::bail!(
"Insufficient disk space: available space is below minimum threshold of {} MB",
min_free_disk_mb
);
}
}

tracing::info!(target: "reth::cli", path = ?db_path, "Opening database");
let database = Arc::new(init_db(db_path.clone(), self.db.database_args())?.with_metrics());

if with_unused_ports {
node_config = node_config.with_unused_ports();
}

// Spawn disk space monitoring task if min_free_disk is configured
// Start monitoring immediately after database is opened
if min_free_disk_mb > 0 {
let data_dir_for_monitor = data_dir.clone();
let shutdown = ctx.task_executor.on_shutdown_signal().clone();
let task_executor = ctx.task_executor.clone();

ctx.task_executor.spawn_critical(
"disk space monitor",
Box::pin(disk_space_monitor_task(
data_dir_for_monitor,
min_free_disk_mb,
shutdown,
task_executor,
)),
);
}

let builder = NodeBuilder::new(node_config)
.with_database(database)
.with_launch_context(ctx.task_executor);
.with_launch_context(ctx.task_executor.clone());

launcher.entrypoint(builder, ext).await
}
Expand All @@ -217,6 +253,48 @@ impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> NodeCommand<C, Ext> {
}
}

/// Disk space monitoring task that periodically checks disk space and triggers shutdown if below
/// threshold.
async fn disk_space_monitor_task(
data_dir: reth_node_core::dirs::ChainPath<DataDirPath>,
min_free_disk_mb: u64,
mut shutdown: Shutdown,
task_executor: TaskExecutor,
) {
use tokio::time::{interval, Duration as TokioDuration};

let mut interval = interval(TokioDuration::from_secs(60)); // Check every minute
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

loop {
tokio::select! {
_ = interval.tick() => {
if is_disk_space_low(data_dir.data_dir(), min_free_disk_mb) {
error!(
target: "reth::cli",
?data_dir,
available_threshold = min_free_disk_mb,
"Disk space below minimum threshold, initiating shutdown"
);
// Trigger graceful shutdown
if let Err(e) = task_executor.initiate_graceful_shutdown() {
error!(
target: "reth::cli",
%e,
"Failed to initiate graceful shutdown"
);
}
return;
}
}
_ = &mut shutdown => {
// Normal shutdown signal received
return;
}
}
}
}

/// No Additional arguments
#[derive(Debug, Clone, Copy, Default, Args)]
#[non_exhaustive]
Expand Down
1 change: 1 addition & 0 deletions crates/node/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ipnet.workspace = true
# io
dirs-next.workspace = true
shellexpand.workspace = true
sysinfo = { workspace = true, features = ["disk"] }

# obs
tracing.workspace = true
Expand Down
49 changes: 49 additions & 0 deletions crates/node/core/src/args/datadir_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ pub struct DatadirArgs {
verbatim_doc_comment
)]
pub static_files_path: Option<PathBuf>,

/// Minimum free disk space in MB, once reached triggers auto shut down (default = 0,
/// disabled).
#[arg(
long = "datadir.min-free-disk",
alias = "datadir.min_free_disk",
value_name = "MB",
default_value_t = 0,
verbatim_doc_comment
)]
pub min_free_disk: u64,
}

impl DatadirArgs {
Expand Down Expand Up @@ -55,4 +66,42 @@ mod tests {
let args = CommandParser::<DatadirArgs>::parse_from(["reth"]).args;
assert_eq!(args, default_args);
}

#[test]
fn test_parse_min_free_disk_flag() {
// Test with hyphen format
let args =
CommandParser::<DatadirArgs>::parse_from(["reth", "--datadir.min-free-disk", "1000"])
.args;
assert_eq!(args.min_free_disk, 1000);

// Test with underscore format (alias)
let args =
CommandParser::<DatadirArgs>::parse_from(["reth", "--datadir.min_free_disk", "500"])
.args;
assert_eq!(args.min_free_disk, 500);

// Test default value (0 = disabled)
let args = CommandParser::<DatadirArgs>::parse_from(["reth"]).args;
assert_eq!(args.min_free_disk, 0);
}

#[test]
fn test_min_free_disk_default() {
let args = DatadirArgs::default();
assert_eq!(args.min_free_disk, 0, "Default should be 0 (disabled)");
}

#[test]
fn test_min_free_disk_with_datadir() {
let args = CommandParser::<DatadirArgs>::parse_from([
"reth",
"--datadir",
"/tmp/test-datadir",
"--datadir.min-free-disk",
"2000",
])
.args;
assert_eq!(args.min_free_disk, 2000);
}
}
153 changes: 152 additions & 1 deletion crates/node/core/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::{
env::VarError,
path::{Path, PathBuf},
};
use tracing::{debug, info};
use tracing::{debug, info, warn};

/// Parses a user-specified path with support for environment variables and common shorthands (e.g.
/// ~ for the user's home directory).
Expand Down Expand Up @@ -89,3 +89,154 @@ where

Ok(block)
}

/// Check available disk space for the given path using sysinfo.
///
/// Returns the available space in MB, or None if the check fails.
pub fn get_available_disk_space_mb(path: &Path) -> Option<u64> {
use sysinfo::Disks;

// Find the disk that contains the given path
let path_canonical = match std::fs::canonicalize(path) {
Ok(p) => p,
Err(_) => return None,
};

let disks = Disks::new_with_refreshed_list();

// Find the disk that contains the given path
for disk in &disks {
let mount_point = disk.mount_point();
if path_canonical.starts_with(mount_point) {
// Get available space in bytes, convert to MB
let available_bytes = disk.available_space();
return Some(available_bytes / (1024 * 1024));
}
}

None
}

/// Check if disk space is below the minimum threshold.
///
/// Returns true if the available disk space is below the minimum threshold (in MB).
pub fn is_disk_space_low(path: &Path, min_free_disk_mb: u64) -> bool {
if min_free_disk_mb == 0 {
return false; // Feature disabled
}

match get_available_disk_space_mb(path) {
Some(available_mb) => {
if available_mb <= min_free_disk_mb {
warn!(
target: "reth::cli",
?path,
available_mb,
min_free_disk_mb,
"Disk space below minimum threshold"
);
return true;
}
false
}
None => {
warn!(
target: "reth::cli",
?path,
"Failed to check disk space, continuing anyway"
);
false
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;

#[test]
fn test_is_disk_space_low_disabled() {
// When min_free_disk is 0, feature is disabled
let temp_dir = std::env::temp_dir();
assert!(!is_disk_space_low(&temp_dir, 0), "Should return false when disabled");
}

#[test]
fn test_is_disk_space_low_with_valid_path() {
// Test with a valid path (should not panic)
// We can't easily mock sysinfo, so we just test that it doesn't panic
// and returns a boolean value
let temp_dir = std::env::temp_dir();
// Should return either true or false, but not panic
let _result = is_disk_space_low(&temp_dir, 1_000_000_000); // Very large threshold
}

#[test]
fn test_is_disk_space_low_with_invalid_path() {
// Test with a non-existent path
let invalid_path = Path::new("/nonexistent/path/that/does/not/exist");
// Should not panic, but may return false (feature disabled) or handle gracefully
// Result depends on sysinfo behavior
let _result = is_disk_space_low(invalid_path, 1000);
}

#[test]
fn test_get_available_disk_space_mb_with_valid_path() {
// Test with a valid path
let temp_dir = std::env::temp_dir();
let result = get_available_disk_space_mb(&temp_dir);
// Should return Some(u64) if successful, or None if it fails
match result {
Some(_mb) => {
// If successful, should be a reasonable value (not 0 for temp dir)
// Value is already validated as u64, so no need to check bounds
}
None => {
// It's okay if it fails, sysinfo might not work in all test environments
}
}
}

#[test]
fn test_get_available_disk_space_mb_with_invalid_path() {
// Test with a non-existent path
let invalid_path = Path::new("/nonexistent/path/that/does/not/exist");
let result = get_available_disk_space_mb(invalid_path);
// Should return None for invalid paths
assert_eq!(result, None);
}

#[test]
fn test_is_disk_space_low_threshold_comparison() {
// Test that the function correctly compares available space with threshold
let temp_dir = std::env::temp_dir();

if let Some(available_mb) = get_available_disk_space_mb(&temp_dir) {
// Test with a threshold smaller than available space (should pass - space is
// sufficient)
if available_mb > 0 {
let result_small = is_disk_space_low(&temp_dir, available_mb.saturating_sub(1));
assert!(
!result_small,
"Should pass (return false) when threshold is less than available space"
);
}

// Test with a threshold larger than available space (should fail - space is
// insufficient)
let result_large = is_disk_space_low(&temp_dir, available_mb.saturating_add(1));
assert!(
result_large,
"Should fail (return true) when threshold is greater than available space"
);

// Test with threshold equal to available space (edge case)
// When available == threshold, should trigger shutdown (return true) because "once
// reached" includes equality
let result_equal = is_disk_space_low(&temp_dir, available_mb);
assert!(result_equal, "Should fail (return true) when threshold equals available space, as 'once reached' includes equality");
}
// If we can't get disk space, that's okay - test environment might not support it
}
}
6 changes: 6 additions & 0 deletions docs/vocs/docs/pages/cli/op-reth/db.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ Datadir:
--datadir.static-files <PATH>
The absolute path to store static files in.

--datadir.min-free-disk <MB>
Minimum free disk space in MB, once reached triggers auto shut down (default = 0,
disabled).

[default: 0]

--config <FILE>
The path to the configuration file to use

Expand Down
6 changes: 6 additions & 0 deletions docs/vocs/docs/pages/cli/op-reth/import-op.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Datadir:
--datadir.static-files <PATH>
The absolute path to store static files in.

--datadir.min-free-disk <MB>
Minimum free disk space in MB, once reached triggers auto shut down (default = 0,
disabled).

[default: 0]

--config <FILE>
The path to the configuration file to use

Expand Down
Loading
Loading