Skip to main content

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