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