1use 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#[derive(Debug, Args, PartialEq, Eq, Default, Clone, Copy)]
19#[command(next_help_heading = "Database")]
20pub struct DatabaseArgs {
21 #[arg(long = "db.log-level", value_parser = LogLevelValueParser::default())]
23 pub log_level: Option<LogLevel>,
24 #[arg(long = "db.exclusive")]
27 pub exclusive: Option<bool>,
28 #[arg(long = "db.max-size", value_parser = parse_byte_size)]
30 pub max_size: Option<usize>,
31 #[arg(long = "db.growth-step", value_parser = parse_byte_size)]
33 pub growth_step: Option<usize>,
34 #[arg(long = "db.read-transaction-timeout")]
36 pub read_transaction_timeout: Option<u64>,
37 #[arg(long = "db.max-readers")]
39 pub max_readers: Option<u64>,
40 #[arg(
42 long = "db.sync-mode",
43 value_parser = value_parser!(SyncMode),
44 )]
45 pub sync_mode: Option<SyncMode>,
46}
47
48impl DatabaseArgs {
49 pub fn database_args(&self) -> reth_db::mdbx::DatabaseArguments {
51 self.get_database_args(default_client_version())
52 }
53
54 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, Some(0) => Some(MaxReadTransactionDuration::Unbounded), 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#[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#[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, "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
183fn 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 #[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 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 let parser = LogLevelValueParser;
307
308 let possible_values: Vec<PossibleValue> = parser.possible_values().unwrap().collect();
310
311 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 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}