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 #[arg(long = "db.rocksdb-block-cache-size", value_parser = parse_byte_size)]
69 pub rocksdb_block_cache_size: Option<usize>,
70 #[arg(long = "db.balstore-cache-size")]
72 pub balstore_cache_size: Option<u64>,
73 #[arg(long = "db.disable-metrics")]
75 pub disable_metrics: bool,
76}
77
78impl DatabaseArgs {
79 pub fn database_args(&self) -> reth_db::mdbx::DatabaseArguments {
81 self.get_database_args(default_client_version())
82 }
83
84 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, Some(0) => Some(MaxReadTransactionDuration::Unbounded), 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 pub const fn metrics_enabled(&self) -> bool {
109 !self.disable_metrics
110 }
111}
112
113#[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#[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, "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 num.checked_mul(multiplier)
193 .map(Self)
194 .ok_or_else(|| "Byte size exceeds the maximum supported value".to_string())
195 }
196}
197
198impl fmt::Display for ByteSize {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 const KB: usize = 1024;
201 const MB: usize = KB * 1024;
202 const GB: usize = MB * 1024;
203 const TB: usize = GB * 1024;
204
205 let (size, unit) = if self.0 >= TB {
206 (self.0 as f64 / TB as f64, "TB")
207 } else if self.0 >= GB {
208 (self.0 as f64 / GB as f64, "GB")
209 } else if self.0 >= MB {
210 (self.0 as f64 / MB as f64, "MB")
211 } else if self.0 >= KB {
212 (self.0 as f64 / KB as f64, "KB")
213 } else {
214 (self.0 as f64, "B")
215 };
216
217 write!(f, "{size:.2}{unit}")
218 }
219}
220
221pub(super) fn parse_byte_size(s: &str) -> Result<usize, String> {
223 s.parse::<ByteSize>().map(Into::into)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use clap::Parser;
230 use reth_db::mdbx::{GIGABYTE, KILOBYTE, MEGABYTE, TERABYTE};
231
232 #[derive(Parser)]
234 struct CommandParser<T: Args> {
235 #[command(flatten)]
236 args: T,
237 }
238
239 #[test]
240 fn test_default_database_args() {
241 let default_args = DatabaseArgs::default();
242 let args = CommandParser::<DatabaseArgs>::parse_from(["reth"]).args;
243 assert_eq!(args, default_args);
244 }
245
246 #[test]
247 fn test_command_parser_disable_metrics() {
248 let args = CommandParser::<DatabaseArgs>::parse_from(["reth"]).args;
249 assert!(args.metrics_enabled());
250
251 let args = CommandParser::<DatabaseArgs>::parse_from(["reth", "--db.disable-metrics"]).args;
252 assert!(args.disable_metrics);
253 assert!(!args.metrics_enabled());
254 }
255
256 #[test]
257 fn test_command_parser_with_valid_max_size() {
258 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
259 "reth",
260 "--db.max-size",
261 "4398046511104",
262 ])
263 .unwrap();
264 assert_eq!(cmd.args.max_size, Some(TERABYTE * 4));
265 }
266
267 #[test]
268 fn test_command_parser_with_invalid_max_size() {
269 let result =
270 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "invalid"]);
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_command_parser_with_valid_growth_step() {
276 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
277 "reth",
278 "--db.growth-step",
279 "4294967296",
280 ])
281 .unwrap();
282 assert_eq!(cmd.args.growth_step, Some(GIGABYTE * 4));
283 }
284
285 #[test]
286 fn test_command_parser_with_invalid_growth_step() {
287 let result =
288 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "invalid"]);
289 assert!(result.is_err());
290 }
291
292 #[test]
293 fn test_command_parser_with_valid_max_size_and_growth_step_from_str() {
294 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
295 "reth",
296 "--db.max-size",
297 "2TB",
298 "--db.growth-step",
299 "1GB",
300 ])
301 .unwrap();
302 assert_eq!(cmd.args.max_size, Some(TERABYTE * 2));
303 assert_eq!(cmd.args.growth_step, Some(GIGABYTE));
304
305 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
306 "reth",
307 "--db.max-size",
308 "12MB",
309 "--db.growth-step",
310 "2KB",
311 ])
312 .unwrap();
313 assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
314 assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
315
316 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
318 "reth",
319 "--db.max-size",
320 "12 MB",
321 "--db.growth-step",
322 "2 KB",
323 ])
324 .unwrap();
325 assert_eq!(cmd.args.max_size, Some(MEGABYTE * 12));
326 assert_eq!(cmd.args.growth_step, Some(KILOBYTE * 2));
327
328 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
329 "reth",
330 "--db.max-size",
331 "1073741824",
332 "--db.growth-step",
333 "1048576",
334 ])
335 .unwrap();
336 assert_eq!(cmd.args.max_size, Some(GIGABYTE));
337 assert_eq!(cmd.args.growth_step, Some(MEGABYTE));
338 }
339
340 #[test]
341 fn test_command_parser_max_size_and_growth_step_from_str_invalid_unit() {
342 let result =
343 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.growth-step", "1 PB"]);
344 assert!(result.is_err());
345
346 let result =
347 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.max-size", "2PB"]);
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_command_parser_with_valid_page_size_from_str() {
353 let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "8KB"])
354 .unwrap();
355 assert_eq!(cmd.args.page_size, Some(KILOBYTE * 8));
356
357 let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "1MB"])
358 .unwrap();
359 assert_eq!(cmd.args.page_size, Some(MEGABYTE));
360
361 let cmd =
363 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "16 KB"])
364 .unwrap();
365 assert_eq!(cmd.args.page_size, Some(KILOBYTE * 16));
366
367 let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "4096"])
369 .unwrap();
370 assert_eq!(cmd.args.page_size, Some(KILOBYTE * 4));
371 }
372
373 #[test]
374 fn test_command_parser_with_invalid_page_size() {
375 let result =
377 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "invalid"]);
378 assert!(result.is_err());
379
380 let result =
382 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.page-size", "7 ZB"]);
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn test_possible_values() {
388 let parser = LogLevelValueParser;
390
391 let possible_values: Vec<PossibleValue> = parser.possible_values().unwrap().collect();
393
394 let expected_values = vec![
396 PossibleValue::new("fatal")
397 .help("Enables logging for critical conditions, i.e. assertion failures"),
398 PossibleValue::new("error").help("Enables logging for error conditions"),
399 PossibleValue::new("warn").help("Enables logging for warning conditions"),
400 PossibleValue::new("notice")
401 .help("Enables logging for normal but significant condition"),
402 PossibleValue::new("verbose").help("Enables logging for verbose informational"),
403 PossibleValue::new("debug").help("Enables logging for debug-level messages"),
404 PossibleValue::new("trace").help("Enables logging for trace debug-level messages"),
405 PossibleValue::new("extra").help("Enables logging for extra debug-level messages"),
406 ];
407
408 assert_eq!(possible_values.len(), expected_values.len());
410 for (actual, expected) in possible_values.iter().zip(expected_values.iter()) {
411 assert_eq!(actual.get_name(), expected.get_name());
412 assert_eq!(actual.get_help(), expected.get_help());
413 }
414 }
415
416 #[test]
417 fn test_command_parser_with_valid_log_level() {
418 let cmd =
419 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "Debug"])
420 .unwrap();
421 assert_eq!(cmd.args.log_level, Some(LogLevel::Debug));
422 }
423
424 #[test]
425 fn test_command_parser_with_invalid_log_level() {
426 let result =
427 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.log-level", "invalid"]);
428 assert!(result.is_err());
429 }
430
431 #[test]
432 fn test_command_parser_without_log_level() {
433 let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
434 assert_eq!(cmd.args.log_level, None);
435 }
436
437 #[test]
438 fn test_command_parser_with_valid_default_sync_mode() {
439 let cmd = CommandParser::<DatabaseArgs>::try_parse_from(["reth"]).unwrap();
440 assert!(cmd.args.sync_mode.is_none());
441 }
442
443 #[test]
444 fn test_command_parser_with_valid_sync_mode_durable() {
445 let cmd =
446 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "durable"])
447 .unwrap();
448 assert!(matches!(cmd.args.sync_mode, Some(SyncMode::Durable)));
449 }
450
451 #[test]
452 fn test_command_parser_with_valid_sync_mode_safe_no_sync() {
453 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
454 "reth",
455 "--db.sync-mode",
456 "safe-no-sync",
457 ])
458 .unwrap();
459 assert!(matches!(cmd.args.sync_mode, Some(SyncMode::SafeNoSync)));
460 }
461
462 #[test]
463 fn test_command_parser_with_invalid_sync_mode() {
464 let result =
465 CommandParser::<DatabaseArgs>::try_parse_from(["reth", "--db.sync-mode", "ultra-fast"]);
466 assert!(result.is_err());
467 }
468
469 #[test]
470 fn test_command_parser_with_valid_balstore_cache_size() {
471 let cmd = CommandParser::<DatabaseArgs>::try_parse_from([
472 "reth",
473 "--db.balstore-cache-size",
474 "1234",
475 ])
476 .unwrap();
477 assert_eq!(cmd.args.balstore_cache_size, Some(1234));
478 }
479}