reth_node_core/args/
database.rs

1//! clap [Args](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        value_parser = value_parser!(SyncMode),
61    )]
62    pub sync_mode: Option<SyncMode>,
63}
64
65impl DatabaseArgs {
66    /// Returns default database arguments with configured log level and client version.
67    pub fn database_args(&self) -> reth_db::mdbx::DatabaseArguments {
68        self.get_database_args(default_client_version())
69    }
70
71    /// Returns the database arguments with configured log level, client version,
72    /// max read transaction duration, and geometry.
73    pub fn get_database_args(
74        &self,
75        client_version: ClientVersion,
76    ) -> reth_db::mdbx::DatabaseArguments {
77        let max_read_transaction_duration = match self.read_transaction_timeout {
78            None => None, // if not specified, use default value
79            Some(0) => Some(MaxReadTransactionDuration::Unbounded), // if 0, disable timeout
80            Some(secs) => Some(MaxReadTransactionDuration::Set(Duration::from_secs(secs))),
81        };
82
83        reth_db::mdbx::DatabaseArguments::new(client_version)
84            .with_log_level(self.log_level)
85            .with_exclusive(self.exclusive)
86            .with_max_read_transaction_duration(max_read_transaction_duration)
87            .with_geometry_max_size(self.max_size)
88            .with_geometry_page_size(self.page_size)
89            .with_growth_step(self.growth_step)
90            .with_max_readers(self.max_readers)
91            .with_sync_mode(self.sync_mode)
92    }
93}
94
95/// clap value parser for [`LogLevel`].
96#[derive(Clone, Debug, Default)]
97#[non_exhaustive]
98struct LogLevelValueParser;
99
100impl TypedValueParser for LogLevelValueParser {
101    type Value = LogLevel;
102
103    fn parse_ref(
104        &self,
105        _cmd: &Command,
106        arg: Option<&Arg>,
107        value: &std::ffi::OsStr,
108    ) -> Result<Self::Value, Error> {
109        let val =
110            value.to_str().ok_or_else(|| Error::raw(ErrorKind::InvalidUtf8, "Invalid UTF-8"))?;
111
112        val.parse::<LogLevel>().map_err(|err| {
113            let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
114            let possible_values = LogLevel::value_variants()
115                .iter()
116                .map(|v| format!("- {:?}: {}", v, v.help_message()))
117                .collect::<Vec<_>>()
118                .join("\n");
119            let msg = format!(
120                "Invalid value '{val}' for {arg}: {err}.\n    Possible values:\n{possible_values}"
121            );
122            clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
123        })
124    }
125
126    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
127        let values = LogLevel::value_variants()
128            .iter()
129            .map(|v| PossibleValue::new(v.variant_name()).help(v.help_message()));
130        Some(Box::new(values))
131    }
132}
133
134/// Size in bytes.
135#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
136pub struct ByteSize(pub usize);
137
138impl From<ByteSize> for usize {
139    fn from(s: ByteSize) -> Self {
140        s.0
141    }
142}
143
144impl FromStr for ByteSize {
145    type Err = String;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        let s = s.trim().to_uppercase();
149        let parts: Vec<&str> = s.split_whitespace().collect();
150
151        let (num_str, unit) = match parts.len() {
152            1 => {
153                let (num, unit) =
154                    s.split_at(s.find(|c: char| c.is_alphabetic()).unwrap_or(s.len()));
155                (num, unit)
156            }
157            2 => (parts[0], parts[1]),
158            _ => {
159                return Err("Invalid format. Use '<number><unit>' or '<number> <unit>'.".to_string())
160            }
161        };
162
163        let num: usize = num_str.parse().map_err(|_| "Invalid number".to_string())?;
164
165        let multiplier = match unit {
166            "B" | "" => 1, // Assume bytes if no unit is specified
167            "KB" => 1024,
168            "MB" => 1024 * 1024,
169            "GB" => 1024 * 1024 * 1024,
170            "TB" => 1024 * 1024 * 1024 * 1024,
171            _ => return Err(format!("Invalid unit: {unit}. Use B, KB, MB, GB, or TB.")),
172        };
173
174        Ok(Self(num * multiplier))
175    }
176}
177
178impl fmt::Display for ByteSize {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        const KB: usize = 1024;
181        const MB: usize = KB * 1024;
182        const GB: usize = MB * 1024;
183        const TB: usize = GB * 1024;
184
185        let (size, unit) = if self.0 >= TB {
186            (self.0 as f64 / TB as f64, "TB")
187        } else if self.0 >= GB {
188            (self.0 as f64 / GB as f64, "GB")
189        } else if self.0 >= MB {
190            (self.0 as f64 / MB as f64, "MB")
191        } else if self.0 >= KB {
192            (self.0 as f64 / KB as f64, "KB")
193        } else {
194            (self.0 as f64, "B")
195        };
196
197        write!(f, "{size:.2}{unit}")
198    }
199}
200
201/// Value parser function that supports various formats.
202fn parse_byte_size(s: &str) -> Result<usize, String> {
203    s.parse::<ByteSize>().map(Into::into)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use clap::Parser;
210    use reth_db::mdbx::{GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE};
211
212    /// A helper type to parse Args more easily
213    #[derive(Parser)]
214    struct CommandParser<T: Args> {
215        #[command(flatten)]
216        args: T,
217    }
218
219    #[test]
220    fn test_default_database_args() {
221        let default_args = DatabaseArgs::default();
222        let args = CommandParser::<DatabaseArgs>::parse_from(["reth"]).args;
223        assert_eq!(args, default_args);
224    }
225
226    #[test]
227    fn test_command_parser_with_valid_max_size() {
228        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
229            "reth",
230            "--db.max-size",
231            "4398046511104",
232        ])
233        .unwrap();
234        assert_eq!(cmd.args.max_size, Some(TERABYTE * 4));
235    }
236
237    #[test]
238    fn test_command_parser_with_invalid_max_size() {
239        let result =
240            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "invalid"]);
241        assert!(result.is_err());
242    }
243
244    #[test]
245    fn test_command_parser_with_valid_growth_step() {
246        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
247            "reth",
248            "--db.growth-step",
249            "4294967296",
250        ])
251        .unwrap();
252        assert_eq!(cmd.args.growth_step, Some(GIGABYTE * 4));
253    }
254
255    #[test]
256    fn test_command_parser_with_invalid_growth_step() {
257        let result =
258            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "invalid"]);
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn test_command_parser_with_valid_max_size_and_growth_step_from_str() {
264        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
265            "reth",
266            "--db.max-size",
267            "2TB",
268            "--db.growth-step",
269            "1GB",
270        ])
271        .unwrap();
272        assert_eq!(cmd.args.max_size, Some(TERABYTE * 2));
273        assert_eq!(cmd.args.growth_step, Some(GIGABYTE));
274
275        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
276            "reth",
277            "--db.max-size",
278            "12MB",
279            "--db.growth-step",
280            "2KB",
281        ])
282        .unwrap();
283        assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
284        assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
285
286        // with spaces
287        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
288            "reth",
289            "--db.max-size",
290            "12 MB",
291            "--db.growth-step",
292            "2 KB",
293        ])
294        .unwrap();
295        assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
296        assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
297
298        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
299            "reth",
300            "--db.max-size",
301            "1073741824",
302            "--db.growth-step",
303            "1048576",
304        ])
305        .unwrap();
306        assert_eq!(cmd.args.max_size, Some(GIGABYTE));
307        assert_eq!(cmd.args.growth_step, Some(MEGABYTE));
308    }
309
310    #[test]
311    fn test_command_parser_max_size_and_growth_step_from_str_invalid_unit() {
312        let result =
313            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "1 PB"]);
314        assert!(result.is_err());
315
316        let result =
317            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "2PB"]);
318        assert!(result.is_err());
319    }
320
321    #[test]
322    fn test_command_parser_with_valid_page_size_from_str() {
323        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "8KB"])
324            .unwrap();
325        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 8));
326
327        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "1MB"])
328            .unwrap();
329        assert_eq!(cmd.args.page_size, Some(MEGABYTE));
330
331        // Test with spaces
332        let cmd =
333            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "16 KB"])
334                .unwrap();
335        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 16));
336
337        // Test with just a number (bytes)
338        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "4096"])
339            .unwrap();
340        assert_eq!(cmd.args.page_size, Some(KILOBYTE * 4));
341    }
342
343    #[test]
344    fn test_command_parser_with_invalid_page_size() {
345        // Invalid text
346        let result =
347            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "invalid"]);
348        assert!(result.is_err());
349
350        // Invalid unit
351        let result =
352            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "7 ZB"]);
353        assert!(result.is_err());
354    }
355
356    #[test]
357    fn test_possible_values() {
358        // Initialize the LogLevelValueParser
359        let parser = LogLevelValueParser;
360
361        // Call the possible_values method
362        let possible_values: Vec<PossibleValue> = parser.possible_values().unwrap().collect();
363
364        // Expected possible values
365        let expected_values = vec![
366            PossibleValue::new("fatal")
367                .help("Enables logging for critical conditions, i.e. assertion failures"),
368            PossibleValue::new("error").help("Enables logging for error conditions"),
369            PossibleValue::new("warn").help("Enables logging for warning conditions"),
370            PossibleValue::new("notice")
371                .help("Enables logging for normal but significant condition"),
372            PossibleValue::new("verbose").help("Enables logging for verbose informational"),
373            PossibleValue::new("debug").help("Enables logging for debug-level messages"),
374            PossibleValue::new("trace").help("Enables logging for trace debug-level messages"),
375            PossibleValue::new("extra").help("Enables logging for extra debug-level messages"),
376        ];
377
378        // Check that the possible values match the expected values
379        assert_eq!(possible_values.len(), expected_values.len());
380        for (actual, expected) in possible_values.iter().zip(expected_values.iter()) {
381            assert_eq!(actual.get_name(), expected.get_name());
382            assert_eq!(actual.get_help(), expected.get_help());
383        }
384    }
385
386    #[test]
387    fn test_command_parser_with_valid_log_level() {
388        let cmd =
389            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "Debug"])
390                .unwrap();
391        assert_eq!(cmd.args.log_level, Some(LogLevel::Debug));
392    }
393
394    #[test]
395    fn test_command_parser_with_invalid_log_level() {
396        let result =
397            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "invalid"]);
398        assert!(result.is_err());
399    }
400
401    #[test]
402    fn test_command_parser_without_log_level() {
403        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
404        assert_eq!(cmd.args.log_level, None);
405    }
406
407    #[test]
408    fn test_command_parser_with_valid_default_sync_mode() {
409        let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
410        assert!(cmd.args.sync_mode.is_none());
411    }
412
413    #[test]
414    fn test_command_parser_with_valid_sync_mode_durable() {
415        let cmd =
416            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "durable"])
417                .unwrap();
418        assert!(matches!(cmd.args.sync_mode, Some(SyncMode::Durable)));
419    }
420
421    #[test]
422    fn test_command_parser_with_valid_sync_mode_safe_no_sync() {
423        let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
424            "reth",
425            "--db.sync-mode",
426            "safe-no-sync",
427        ])
428        .unwrap();
429        assert!(matches!(cmd.args.sync_mode, Some(SyncMode::SafeNoSync)));
430    }
431
432    #[test]
433    fn test_command_parser_with_invalid_sync_mode() {
434        let result =
435            CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "ultra-fast"]);
436        assert!(result.is_err());
437    }
438}