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