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