Skip to main content

reth_node_core/args/
database.rs

1//! clap [`Args`] for database configuration.
2
3use std::{fmt, str::FromStr, time::Duration};
4
5use crate::version::default_client_version;
6use clap::{
7    builder::{PossibleValue, TypedValueParser},
8    error::ErrorKind,
9    value_parser, Arg, Args, Command, Error,
10};
11use reth_db::{
12    mdbx::{MaxReadTransactionDuration, SyncMode},
13    ClientVersion,
14};
15use reth_storage_errors::db::LogLevel;
16
17/// Parameters for database configuration
18#[derive(Debug, Args, PartialEq, Eq, Default, Clone, Copy)]
19#[command(next_help_heading = "Database")]
20pub struct DatabaseArgs {
21    /// Database logging level. Levels higher than "notice" require a debug build.
22    #[arg(long = "db.log-level", value_parser = LogLevelValueParser::default())]
23    pub log_level: Option<LogLevel>,
24    /// Open environment in exclusive/monopolistic mode. Makes it possible to open a database on an
25    /// NFS volume.
26    #[arg(long = "db.exclusive")]
27    pub exclusive: Option<bool>,
28    /// Maximum database size (e.g., 4TB, 8TB).
29    ///
30    /// This sets the "map size" of the database. If the database grows beyond this
31    /// limit, the node will stop with an "environment map size limit reached" error.
32    ///
33    /// The default value is 8TB.
34    #[arg(long = "db.max-size", value_parser = parse_byte_size)]
35    pub max_size: Option<usize>,
36    /// Database page size (e.g., 4KB, 8KB, 16KB).
37    ///
38    /// Specifies the page size used by the MDBX database.
39    ///
40    /// The page size determines the maximum database size.
41    /// MDBX supports up to 2^31 pages, so with the default 4KB page size, the maximum
42    /// database size is 8TB. To allow larger databases, increase this value to 8KB or higher.
43    ///
44    /// WARNING: This setting is only configurable at database creation; changing
45    /// it later requires re-syncing.
46    #[arg(long = "db.page-size", value_parser = parse_byte_size)]
47    pub page_size: Option<usize>,
48    /// Database growth step (e.g., 4GB, 4KB)
49    #[arg(long = "db.growth-step", value_parser = parse_byte_size)]
50    pub growth_step: Option<usize>,
51    /// Read transaction timeout in seconds, 0 means no timeout.
52    #[arg(long = "db.read-transaction-timeout")]
53    pub read_transaction_timeout: Option<u64>,
54    /// Maximum number of readers allowed to access the database concurrently.
55    #[arg(long = "db.max-readers")]
56    pub max_readers: Option<u64>,
57    /// Controls how aggressively the database synchronizes data to disk.
58    #[arg(
59        long = "db.sync-mode",
60        env = "RETH_DB_SYNC_MODE",
61        value_parser = value_parser!(SyncMode),
62    )]
63    pub sync_mode: Option<SyncMode>,
64    /// `RocksDB` block cache size (e.g., 512MB, 4GB).
65    ///
66    /// Controls the size of the in-memory LRU cache for decompressed `RocksDB` blocks.
67    /// A larger cache reduces repeated decompression of hot blocks, improving read
68    /// performance for history lookups.
69    #[arg(long = "db.rocksdb-block-cache-size", value_parser = parse_byte_size)]
70    pub rocksdb_block_cache_size: Option<usize>,
71    /// Number of recent blocks to keep in the in-memory BAL store cache.
72    #[arg(long = "db.balstore-cache-size")]
73    pub balstore_cache_size: Option<u64>,
74    /// Disable built-in database metrics.
75    #[arg(long = "db.disable-metrics")]
76    pub disable_metrics: bool,
77}
78
79impl DatabaseArgs {
80    /// Returns default database arguments with configured log level and client version.
81    pub fn database_args(&self) -> reth_db::mdbx::DatabaseArguments {
82        self.get_database_args(default_client_version())
83    }
84
85    /// Returns the database arguments with configured log level, client version,
86    /// max read transaction duration, and geometry.
87    pub fn get_database_args(
88        &self,
89        client_version: ClientVersion,
90    ) -> reth_db::mdbx::DatabaseArguments {
91        let max_read_transaction_duration = match self.read_transaction_timeout {
92            None => None, // if not specified, use default value
93            Some(0) => Some(MaxReadTransactionDuration::Unbounded), // if 0, disable timeout
94            Some(secs) => Some(MaxReadTransactionDuration::Set(Duration::from_secs(secs))),
95        };
96
97        reth_db::mdbx::DatabaseArguments::new(client_version)
98            .with_log_level(self.log_level)
99            .with_exclusive(self.exclusive)
100            .with_max_read_transaction_duration(max_read_transaction_duration)
101            .with_geometry_max_size(self.max_size)
102            .with_geometry_page_size(self.page_size)
103            .with_growth_step(self.growth_step)
104            .with_max_readers(self.max_readers)
105            .with_sync_mode(self.sync_mode)
106    }
107
108    /// Returns whether built-in database metrics are enabled.
109    pub const fn metrics_enabled(&self) -> bool {
110        !self.disable_metrics
111    }
112}
113
114/// clap value parser for [`LogLevel`].
115#[derive(Clone, Debug, Default)]
116#[non_exhaustive]
117struct LogLevelValueParser;
118
119impl TypedValueParser for LogLevelValueParser {
120    type Value = LogLevel;
121
122    fn parse_ref(
123        &self,
124        _cmd: &Command,
125        arg: Option<&Arg>,
126        value: &std::ffi::OsStr,
127    ) -> Result<Self::Value, Error> {
128        let val =
129            value.to_str().ok_or_else(|| Error::raw(ErrorKind::InvalidUtf8, "Invalid UTF-8"))?;
130
131        val.parse::<LogLevel>().map_err(|err| {
132            let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
133            let possible_values = LogLevel::value_variants()
134                .iter()
135                .map(|v| format!("- {:?}: {}", v, v.help_message()))
136                .collect::<Vec<_>>()
137                .join("\n");
138            let msg = format!(
139                "Invalid value '{val}' for {arg}: {err}.\n    Possible values:\n{possible_values}"
140            );
141            clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
142        })
143    }
144
145    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
146        let values = LogLevel::value_variants()
147            .iter()
148            .map(|v| PossibleValue::new(v.variant_name()).help(v.help_message()));
149        Some(Box::new(values))
150    }
151}
152
153/// Size in bytes.
154#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
155pub struct ByteSize(pub usize);
156
157impl From<ByteSize> for usize {
158    fn from(s: ByteSize) -> Self {
159        s.0
160    }
161}
162
163impl FromStr for ByteSize {
164    type Err = String;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        let s = s.trim().to_uppercase();
168        let parts: Vec<&str> = s.split_whitespace().collect();
169
170        let (num_str, unit) = match parts.len() {
171            1 => {
172                let (num, unit) =
173                    s.split_at(s.find(|c: char| c.is_alphabetic()).unwrap_or(s.len()));
174                (num, unit)
175            }
176            2 => (parts[0], parts[1]),
177            _ => {
178                return Err("Invalid format. Use '<number><unit>' or '<number> <unit>'.".to_string())
179            }
180        };
181
182        let num: usize = num_str.parse().map_err(|_| "Invalid number".to_string())?;
183
184        let multiplier = match unit {
185            "B" | "" => 1, // Assume bytes if no unit is specified
186            "KB" => 1024,
187            "MB" => 1024 * 1024,
188            "GB" => 1024 * 1024 * 1024,
189            "TB" => 1024 * 1024 * 1024 * 1024,
190            _ => return Err(format!("Invalid unit: {unit}. Use B, KB, MB, GB, or TB.")),
191        };
192
193        num.checked_mul(multiplier)
194            .map(Self)
195            .ok_or_else(|| "Byte size exceeds the maximum supported value".to_string())
196    }
197}
198
199impl fmt::Display for ByteSize {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        const KB: usize = 1024;
202        const MB: usize = KB * 1024;
203        const GB: usize = MB * 1024;
204        const TB: usize = GB * 1024;
205
206        let (size, unit) = if self.0 >= TB {
207            (self.0 as f64 / TB as f64, "TB")
208        } else if self.0 >= GB {
209            (self.0 as f64 / GB as f64, "GB")
210        } else if self.0 >= MB {
211            (self.0 as f64 / MB as f64, "MB")
212        } else if self.0 >= KB {
213            (self.0 as f64 / KB as f64, "KB")
214        } else {
215            (self.0 as f64, "B")
216        };
217
218        write!(f, "{size:.2}{unit}")
219    }
220}
221
222/// Value parser function that supports various formats.
223pub(super) fn parse_byte_size(s: &str) -> Result<usize, String> {
224    s.parse::<ByteSize>().map(Into::into)
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use clap::Parser;
231    use reth_db::mdbx::{GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE};
232
233    /// A helper type to parse Args more easily
234    #[derive(Parser)]
235    struct CommandParser<T: Args> {
236        #[command(flatten)]
237        args: T,
238    }
239
240    #[test]
241    fn test_default_database_args() {
242        let default_args = DatabaseArgs::default();
243        let args = CommandParser::<DatabaseArgs>::parse_from(["reth"]).args;
244        assert_eq!(args, default_args);
245    }
246
247    #[test]
248    fn test_command_parser_disable_metrics() {
249        let args = CommandParser::<DatabaseArgs>::parse_from(["reth"]).args;
250        assert!(args.metrics_enabled());
251
252        let args = CommandParser::<DatabaseArgs>::parse_from(["reth", "--db.disable-metrics"]).args;
253        assert!(args.disable_metrics);
254        assert!(!args.metrics_enabled());
255    }
256
257    #[test]
258    fn test_command_parser_with_valid_max_size() {
259        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
260            "reth",
261            "--db.max-size",
262            "4398046511104",
263        ])
264        .unwrap();
265        assert_eq!(cmd.args.max_size, Some(TERABYTE * 4));
266    }
267
268    #[test]
269    fn test_command_parser_with_invalid_max_size() {
270        let result =
271            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "invalid"]);
272        assert!(result.is_err());
273    }
274
275    #[test]
276    fn test_command_parser_with_valid_growth_step() {
277        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
278            "reth",
279            "--db.growth-step",
280            "4294967296",
281        ])
282        .unwrap();
283        assert_eq!(cmd.args.growth_step, Some(GIGABYTE * 4));
284    }
285
286    #[test]
287    fn test_command_parser_with_invalid_growth_step() {
288        let result =
289            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "invalid"]);
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn test_command_parser_with_valid_max_size_and_growth_step_from_str() {
295        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
296            "reth",
297            "--db.max-size",
298            "2TB",
299            "--db.growth-step",
300            "1GB",
301        ])
302        .unwrap();
303        assert_eq!(cmd.args.max_size, Some(TERABYTE * 2));
304        assert_eq!(cmd.args.growth_step, Some(GIGABYTE));
305
306        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
307            "reth",
308            "--db.max-size",
309            "12MB",
310            "--db.growth-step",
311            "2KB",
312        ])
313        .unwrap();
314        assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
315        assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
316
317        // with spaces
318        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
319            "reth",
320            "--db.max-size",
321            "12 MB",
322            "--db.growth-step",
323            "2 KB",
324        ])
325        .unwrap();
326        assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
327        assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
328
329        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
330            "reth",
331            "--db.max-size",
332            "1073741824",
333            "--db.growth-step",
334            "1048576",
335        ])
336        .unwrap();
337        assert_eq!(cmd.args.max_size, Some(GIGABYTE));
338        assert_eq!(cmd.args.growth_step, Some(MEGABYTE));
339    }
340
341    #[test]
342    fn test_command_parser_max_size_and_growth_step_from_str_invalid_unit() {
343        let result =
344            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "1 PB"]);
345        assert!(result.is_err());
346
347        let result =
348            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "2PB"]);
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn test_command_parser_with_valid_page_size_from_str() {
354        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "8KB"])
355            .unwrap();
356        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 8));
357
358        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "1MB"])
359            .unwrap();
360        assert_eq!(cmd.args.page_size, Some(MEGABYTE));
361
362        // Test with spaces
363        let cmd =
364            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "16 KB"])
365                .unwrap();
366        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 16));
367
368        // Test with just a number (bytes)
369        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "4096"])
370            .unwrap();
371        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 4));
372    }
373
374    #[test]
375    fn test_command_parser_with_invalid_page_size() {
376        // Invalid text
377        let result =
378            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "invalid"]);
379        assert!(result.is_err());
380
381        // Invalid unit
382        let result =
383            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "7 ZB"]);
384        assert!(result.is_err());
385    }
386
387    #[test]
388    fn test_possible_values() {
389        // Initialize the LogLevelValueParser
390        let parser = LogLevelValueParser;
391
392        // Call the possible_values method
393        let possible_values: Vec<PossibleValue> = parser.possible_values().unwrap().collect();
394
395        // Expected possible values
396        let expected_values = vec![
397            PossibleValue::new("fatal")
398                .help("Enables logging for critical conditions, i.e. assertion failures"),
399            PossibleValue::new("error").help("Enables logging for error conditions"),
400            PossibleValue::new("warn").help("Enables logging for warning conditions"),
401            PossibleValue::new("notice")
402                .help("Enables logging for normal but significant condition"),
403            PossibleValue::new("verbose").help("Enables logging for verbose informational"),
404            PossibleValue::new("debug").help("Enables logging for debug-level messages"),
405            PossibleValue::new("trace").help("Enables logging for trace debug-level messages"),
406            PossibleValue::new("extra").help("Enables logging for extra debug-level messages"),
407        ];
408
409        // Check that the possible values match the expected values
410        assert_eq!(possible_values.len(), expected_values.len());
411        for (actual, expected) in possible_values.iter().zip(expected_values.iter()) {
412            assert_eq!(actual.get_name(), expected.get_name());
413            assert_eq!(actual.get_help(), expected.get_help());
414        }
415    }
416
417    #[test]
418    fn test_command_parser_with_valid_log_level() {
419        let cmd =
420            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "Debug"])
421                .unwrap();
422        assert_eq!(cmd.args.log_level, Some(LogLevel::Debug));
423    }
424
425    #[test]
426    fn test_command_parser_with_invalid_log_level() {
427        let result =
428            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "invalid"]);
429        assert!(result.is_err());
430    }
431
432    #[test]
433    fn test_command_parser_without_log_level() {
434        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
435        assert_eq!(cmd.args.log_level, None);
436    }
437
438    #[test]
439    fn test_command_parser_with_valid_default_sync_mode() {
440        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
441        assert!(cmd.args.sync_mode.is_none());
442    }
443
444    #[test]
445    fn test_command_parser_with_valid_sync_mode_durable() {
446        let cmd =
447            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "durable"])
448                .unwrap();
449        assert!(matches!(cmd.args.sync_mode, Some(SyncMode::Durable)));
450    }
451
452    #[test]
453    fn test_command_parser_with_valid_sync_mode_safe_no_sync() {
454        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
455            "reth",
456            "--db.sync-mode",
457            "safe-no-sync",
458        ])
459        .unwrap();
460        assert!(matches!(cmd.args.sync_mode, Some(SyncMode::SafeNoSync)));
461    }
462
463    #[test]
464    fn test_command_parser_with_invalid_sync_mode() {
465        let result =
466            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "ultra-fast"]);
467        assert!(result.is_err());
468    }
469
470    #[test]
471    fn test_command_parser_with_valid_balstore_cache_size() {
472        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
473            "reth",
474            "--db.balstore-cache-size",
475            "1234",
476        ])
477        .unwrap();
478        assert_eq!(cmd.args.balstore_cache_size, Some(1234));
479    }
480}