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)]
35 pub max_size: Option<usize>,
36 #[arg(long = "db.page-size", value_parser = parse_byte_size)]
47 pub page_size: Option<usize>,
48 #[arg(long = "db.growth-step", value_parser = parse_byte_size)]
50 pub growth_step: Option<usize>,
51 #[arg(long = "db.read-transaction-timeout")]
53 pub read_transaction_timeout: Option<u64>,
54 #[arg(long = "db.max-readers")]
56 pub max_readers: Option<u64>,
57 #[arg(
59 long = "db.sync-mode",
60 value_parser = value_parser!(SyncMode),
61 )]
62 pub sync_mode: Option<SyncMode>,
63}
64
65impl DatabaseArgs {
66 pub fn database_args(&self) -> reth_db::mdbx::DatabaseArguments {
68 self.get_database_args(default_client_version())
69 }
70
71 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, Some(0) => Some(MaxReadTransactionDuration::Unbounded), 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#[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#[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, "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
201fn 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 #[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 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 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 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 let result =
347 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "invalid"]);
348 assert!(result.is_err());
349
350 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 let parser = LogLevelValueParser;
360
361 let possible_values: Vec<PossibleValue> = parser.possible_values().unwrap().collect();
363
364 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 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}