1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)]
7pub enum DryRunMode {
8 #[value(name = "brief")]
10 Brief,
11 #[value(name = "all")]
13 All,
14 #[value(name = "explain")]
16 Explain,
17}
18
19#[derive(Debug, Clone, Copy, Default)]
21pub struct RuntimeConfig {
22 pub max_workers: usize,
24 pub max_blocking_threads: usize,
26}
27
28#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
35pub struct AutoMetaThrottleConfig {
36 pub initial_cwnd: u32,
37 pub min_cwnd: u32,
38 pub max_cwnd: u32,
39 pub alpha: f64,
40 pub beta: f64,
41 pub increase_step: u32,
42 pub decrease_step: u32,
43 pub baseline_percentile: f64,
46 pub current_percentile: f64,
49 pub long_window: std::time::Duration,
51 pub short_window: std::time::Duration,
53 pub tick_interval: std::time::Duration,
54}
55
56#[derive(Debug, Clone)]
58pub struct ThrottleConfig {
59 pub max_open_files: Option<usize>,
61 pub ops_throttle: usize,
63 pub iops_throttle: usize,
65 pub chunk_size: u64,
67 pub auto_meta: Option<AutoMetaThrottleConfig>,
69 pub histogram_enabled: bool,
73 pub histogram_log_path: Option<std::path::PathBuf>,
77 pub histogram_interval: std::time::Duration,
80}
81
82impl Default for ThrottleConfig {
83 fn default() -> Self {
84 Self {
85 max_open_files: None,
86 ops_throttle: 0,
87 iops_throttle: 0,
88 chunk_size: 0,
89 auto_meta: None,
90 histogram_enabled: false,
91 histogram_log_path: None,
92 histogram_interval: std::time::Duration::from_secs(1),
93 }
94 }
95}
96
97pub const AUTO_META_MIN_OPS_THROTTLE: usize = 10;
107
108impl ThrottleConfig {
109 pub fn validate(&self) -> Result<(), String> {
111 if self.iops_throttle > 0 && self.chunk_size == 0 {
112 return Err("chunk_size must be specified when using iops_throttle".to_string());
113 }
114 if let Some(auto) = &self.auto_meta {
115 if auto.max_cwnd == 0 {
116 return Err("auto-meta-max-cwnd must be > 0".to_string());
117 }
118 if auto.min_cwnd == 0 {
119 return Err("auto-meta-min-cwnd must be >= 1".to_string());
120 }
121 if auto.min_cwnd > auto.max_cwnd {
122 return Err("auto-meta-min-cwnd must be <= auto-meta-max-cwnd".to_string());
123 }
124 if !(0.0..1.0).contains(&auto.baseline_percentile) {
125 return Err("auto-meta-baseline-percentile must be in [0.0, 1.0)".to_string());
126 }
127 if !(0.0..1.0).contains(&auto.current_percentile) {
128 return Err("auto-meta-current-percentile must be in [0.0, 1.0)".to_string());
129 }
130 if auto.baseline_percentile > auto.current_percentile {
131 return Err(
132 "auto-meta-baseline-percentile must be <= auto-meta-current-percentile"
133 .to_string(),
134 );
135 }
136 if !auto.alpha.is_finite() || auto.alpha <= 0.0 {
148 return Err("auto-meta-alpha must be a finite value > 0".to_string());
149 }
150 if !auto.beta.is_finite() || auto.beta <= 0.0 {
151 return Err("auto-meta-beta must be a finite value > 0".to_string());
152 }
153 if auto.alpha >= auto.beta {
154 return Err("auto-meta-alpha must be < auto-meta-beta".to_string());
155 }
156 if auto.tick_interval.is_zero() {
157 return Err("auto-meta-tick-interval must be > 0".to_string());
158 }
159 if auto.long_window.is_zero() {
160 return Err("auto-meta-long-window must be > 0".to_string());
161 }
162 if auto.short_window.is_zero() {
163 return Err("auto-meta-short-window must be > 0".to_string());
164 }
165 if auto.short_window >= auto.long_window {
166 return Err("auto-meta-short-window must be < auto-meta-long-window".to_string());
167 }
168 if self.ops_throttle > 0 && self.ops_throttle < AUTO_META_MIN_OPS_THROTTLE {
169 return Err(format!(
170 "--auto-meta-throttle is incompatible with --ops-throttle={} \
171 (auto-meta uses a fixed 100ms replenish interval; rates below \
172 {} ops/sec round to zero tokens per interval and would pause \
173 the throttle after the initial token). Either raise ops-throttle \
174 to >= {} or drop --auto-meta-throttle to get the legacy adaptive \
175 interval.",
176 self.ops_throttle, AUTO_META_MIN_OPS_THROTTLE, AUTO_META_MIN_OPS_THROTTLE,
177 ));
178 }
179 }
180 let histogram_active = self.histogram_enabled || self.histogram_log_path.is_some();
181 if histogram_active && self.auto_meta.is_none() {
182 return Err(
183 "--auto-meta-histogram and --auto-meta-histogram-log require \
184 --auto-meta-throttle to be enabled"
185 .into(),
186 );
187 }
188 if histogram_active {
189 let min = std::time::Duration::from_millis(100);
190 let max = std::time::Duration::from_secs(60);
191 if self.histogram_interval < min || self.histogram_interval > max {
192 return Err(format!(
193 "--auto-meta-histogram-interval must be in [{}ms, {}s]",
194 min.as_millis(),
195 max.as_secs(),
196 ));
197 }
198 if let Some(path) = &self.histogram_log_path {
199 let parent = match path.parent() {
202 Some(p) if p.as_os_str().is_empty() => std::path::Path::new("."),
203 Some(p) => p,
204 None => std::path::Path::new("."),
205 };
206 if !parent.exists() {
207 return Err(format!(
208 "--auto-meta-histogram-log parent directory does not exist: {parent:?}",
209 ));
210 }
211 if !parent.is_dir() {
212 return Err(format!(
213 "--auto-meta-histogram-log parent is not a directory: {parent:?}",
214 ));
215 }
216 let suffix: u64 = rand::random();
225 let probe = parent.join(format!(
226 ".rcp-auto-meta-probe-{}-{:016x}",
227 std::process::id(),
228 suffix,
229 ));
230 match std::fs::OpenOptions::new()
231 .create_new(true)
232 .write(true)
233 .open(&probe)
234 {
235 Ok(_) => {
236 let _ = std::fs::remove_file(&probe);
237 }
238 Err(err) => {
239 return Err(format!(
240 "--auto-meta-histogram-log parent {parent:?} is not writable: {err:#}",
241 ));
242 }
243 }
244 }
245 }
246 Ok(())
247 }
248}
249
250#[derive(Debug, Clone, Copy, Default)]
252pub struct OutputConfig {
253 pub quiet: bool,
255 pub verbose: u8,
257 pub print_summary: bool,
259 pub suppress_runtime_stats: bool,
262}
263
264pub struct DryRunWarnings {
271 warnings: Vec<String>,
272}
273impl DryRunWarnings {
274 #[must_use]
285 pub fn new(
286 has_progress: bool,
287 has_summary: bool,
288 verbose: u8,
289 has_overwrite: bool,
290 has_filters: bool,
291 has_destination: bool,
292 has_ignore_existing: bool,
293 ) -> Self {
294 let mut warnings = Vec::new();
295 if has_progress {
296 warnings.push("dry-run: --progress was ignored".to_string());
297 }
298 if has_summary && verbose == 0 {
299 warnings.push("dry-run: --summary was ignored".to_string());
300 }
301 if has_overwrite {
302 warnings.push(
303 "dry-run: --overwrite was ignored; dry-run does not check destination state"
304 .to_string(),
305 );
306 }
307 if !has_filters && !has_ignore_existing {
308 if has_destination {
309 warnings.push(
310 "dry-run: no filtering specified. dry-run is primarily useful to preview \
311 --include/--exclude/--filter-file filtering; it does not check whether \
312 files already exist at the destination."
313 .to_string(),
314 );
315 } else {
316 warnings.push(
317 "dry-run: no filtering specified. dry-run is primarily useful to preview \
318 --include/--exclude/--filter-file filtering."
319 .to_string(),
320 );
321 }
322 }
323 Self { warnings }
324 }
325 pub fn print(&self) {
327 for warning in &self.warnings {
328 eprintln!("{warning}");
329 }
330 }
331}
332#[derive(Debug)]
334pub struct TracingConfig {
335 pub remote_layer: Option<crate::remote_tracing::RemoteTracingLayer>,
337 pub debug_log_file: Option<String>,
339 pub chrome_trace_prefix: Option<String>,
341 pub flamegraph_prefix: Option<String>,
343 pub trace_identifier: String,
345 pub profile_level: Option<String>,
348 pub tokio_console: bool,
350 pub tokio_console_port: Option<u16>,
352}
353
354impl Default for TracingConfig {
355 fn default() -> Self {
356 Self {
357 remote_layer: None,
358 debug_log_file: None,
359 chrome_trace_prefix: None,
360 flamegraph_prefix: None,
361 trace_identifier: "unknown".to_string(),
362 profile_level: None,
363 tokio_console: false,
364 tokio_console_port: None,
365 }
366 }
367}
368
369#[cfg(test)]
370mod auto_meta_validation_tests {
371 use super::*;
372
373 fn valid_auto_meta() -> AutoMetaThrottleConfig {
374 AutoMetaThrottleConfig {
375 initial_cwnd: 1,
376 min_cwnd: 1,
377 max_cwnd: 4096,
378 alpha: 1.3,
379 beta: 1.8,
380 increase_step: 1,
381 decrease_step: 1,
382 baseline_percentile: 0.1,
383 current_percentile: 0.5,
384 long_window: std::time::Duration::from_secs(10),
385 short_window: std::time::Duration::from_secs(1),
386 tick_interval: std::time::Duration::from_millis(50),
387 }
388 }
389
390 fn config_with(auto: AutoMetaThrottleConfig) -> ThrottleConfig {
391 ThrottleConfig {
392 max_open_files: None,
393 ops_throttle: 0,
394 iops_throttle: 0,
395 chunk_size: 0,
396 auto_meta: Some(auto),
397 histogram_enabled: false,
398 histogram_log_path: None,
399 histogram_interval: std::time::Duration::from_secs(1),
400 }
401 }
402
403 #[test]
404 fn defaults_validate() {
405 assert!(config_with(valid_auto_meta()).validate().is_ok());
406 }
407
408 #[test]
409 fn min_cwnd_zero_is_rejected() {
410 let mut auto = valid_auto_meta();
411 auto.min_cwnd = 0;
412 let err = config_with(auto).validate().unwrap_err();
413 assert!(err.contains("min-cwnd"), "got: {err}");
414 }
415
416 #[test]
417 fn alpha_at_or_below_zero_is_rejected() {
418 let mut auto = valid_auto_meta();
419 auto.alpha = 0.0;
420 assert!(config_with(auto).validate().is_err());
421 let mut auto = valid_auto_meta();
422 auto.alpha = -0.5;
423 assert!(config_with(auto).validate().is_err());
424 }
425
426 #[test]
427 fn alpha_below_one_is_accepted() {
428 let mut auto = valid_auto_meta();
432 auto.alpha = 0.9;
433 auto.beta = 1.1;
434 assert!(config_with(auto).validate().is_ok());
435 }
436
437 #[test]
438 fn beta_at_or_below_zero_is_rejected() {
439 let mut auto = valid_auto_meta();
440 auto.alpha = 0.5;
441 auto.beta = 0.0;
442 let err = config_with(auto).validate().unwrap_err();
443 assert!(err.contains("beta"), "got: {err}");
444 }
445
446 #[test]
447 fn cross_percentile_config_validates() {
448 let mut auto = valid_auto_meta();
452 auto.baseline_percentile = 0.4;
453 auto.current_percentile = 0.6;
454 assert!(config_with(auto).validate().is_ok());
455 }
456
457 #[test]
458 fn baseline_percentile_above_current_is_rejected() {
459 let mut auto = valid_auto_meta();
460 auto.baseline_percentile = 0.6;
461 auto.current_percentile = 0.4;
462 let err = config_with(auto).validate().unwrap_err();
463 assert!(
464 err.contains("baseline-percentile") && err.contains("current-percentile"),
465 "got: {err}",
466 );
467 }
468
469 #[test]
470 fn baseline_percentile_out_of_range_is_rejected() {
471 let mut auto = valid_auto_meta();
472 auto.baseline_percentile = 1.0;
473 let err = config_with(auto).validate().unwrap_err();
474 assert!(err.contains("baseline-percentile"), "got: {err}");
475 }
476
477 #[test]
478 fn non_finite_alpha_or_beta_is_rejected() {
479 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
484 let mut auto = valid_auto_meta();
485 auto.alpha = bad;
486 assert!(
487 config_with(auto).validate().is_err(),
488 "alpha={bad} must be rejected",
489 );
490 let mut auto = valid_auto_meta();
491 auto.beta = bad;
492 assert!(
493 config_with(auto).validate().is_err(),
494 "beta={bad} must be rejected",
495 );
496 }
497 }
498
499 #[test]
500 fn current_percentile_out_of_range_is_rejected() {
501 let mut auto = valid_auto_meta();
502 auto.current_percentile = 1.0;
503 let err = config_with(auto).validate().unwrap_err();
504 assert!(err.contains("current-percentile"), "got: {err}");
505 }
506
507 #[test]
508 fn ops_throttle_below_floor_is_rejected_under_auto_meta() {
509 let mut config = config_with(valid_auto_meta());
510 config.ops_throttle = 5;
511 let err = config.validate().unwrap_err();
512 assert!(
513 err.contains("ops-throttle") && err.contains("auto-meta-throttle"),
514 "got: {err}",
515 );
516 }
517
518 #[test]
519 fn ops_throttle_at_or_above_floor_is_accepted_under_auto_meta() {
520 let mut config = config_with(valid_auto_meta());
521 config.ops_throttle = AUTO_META_MIN_OPS_THROTTLE;
522 assert!(config.validate().is_ok());
523 config.ops_throttle = AUTO_META_MIN_OPS_THROTTLE + 100;
524 assert!(config.validate().is_ok());
525 }
526
527 #[test]
528 fn ops_throttle_below_floor_is_fine_without_auto_meta() {
529 let config = ThrottleConfig {
533 max_open_files: None,
534 ops_throttle: 5,
535 iops_throttle: 0,
536 chunk_size: 0,
537 auto_meta: None,
538 histogram_enabled: false,
539 histogram_log_path: None,
540 histogram_interval: std::time::Duration::from_secs(1),
541 };
542 assert!(config.validate().is_ok());
543 }
544
545 #[test]
546 fn alpha_greater_than_beta_is_rejected() {
547 let mut auto = valid_auto_meta();
548 auto.alpha = 1.6;
549 auto.beta = 1.5;
550 let err = config_with(auto).validate().unwrap_err();
551 assert!(err.contains("alpha") && err.contains("beta"), "got: {err}");
552 }
553
554 #[test]
555 fn histogram_log_without_throttle_is_rejected() {
556 let config = ThrottleConfig {
558 max_open_files: None,
559 ops_throttle: 0,
560 iops_throttle: 0,
561 chunk_size: 0,
562 auto_meta: None,
563 histogram_log_path: Some("/tmp/x.hdr".into()),
564 histogram_enabled: false,
565 histogram_interval: std::time::Duration::from_secs(1),
566 };
567 let err = config.validate().unwrap_err();
568 assert!(
569 err.contains("histogram") && err.contains("auto-meta-throttle"),
570 "got: {err}"
571 );
572 }
573
574 #[test]
575 fn histogram_enabled_without_throttle_is_rejected() {
576 let config = ThrottleConfig {
579 max_open_files: None,
580 ops_throttle: 0,
581 iops_throttle: 0,
582 chunk_size: 0,
583 auto_meta: None,
584 histogram_enabled: true,
585 histogram_log_path: None,
586 histogram_interval: std::time::Duration::from_secs(1),
587 };
588 let err = config.validate().unwrap_err();
589 assert!(
590 err.contains("histogram") && err.contains("auto-meta-throttle"),
591 "got: {err}"
592 );
593 }
594
595 #[test]
596 fn histogram_interval_below_floor_is_rejected() {
597 let mut config = config_with(valid_auto_meta());
598 config.histogram_enabled = true;
599 config.histogram_interval = std::time::Duration::from_millis(50);
600 let err = config.validate().unwrap_err();
601 assert!(err.contains("histogram-interval"), "got: {err}");
602 }
603
604 #[test]
605 fn histogram_interval_above_ceiling_is_rejected() {
606 let mut config = config_with(valid_auto_meta());
607 config.histogram_enabled = true;
608 config.histogram_interval = std::time::Duration::from_secs(120);
609 let err = config.validate().unwrap_err();
610 assert!(err.contains("histogram-interval"), "got: {err}");
611 }
612
613 #[test]
614 fn histogram_defaults_pass_validation() {
615 let mut config = config_with(valid_auto_meta());
616 config.histogram_enabled = true;
617 config.histogram_interval = std::time::Duration::from_secs(1);
618 assert!(config.validate().is_ok());
619 }
620
621 #[test]
622 fn histogram_log_with_missing_parent_is_rejected() {
623 let mut config = config_with(valid_auto_meta());
624 config.histogram_log_path = Some("/nonexistent-dir-12345/foo.hdr".into());
625 let err = config.validate().unwrap_err();
626 assert!(
627 err.contains("histogram-log") && err.contains("parent"),
628 "got: {err}",
629 );
630 }
631
632 #[test]
633 fn histogram_log_with_writable_parent_is_accepted() {
634 let dir = tempfile::tempdir().unwrap();
635 let mut config = config_with(valid_auto_meta());
636 config.histogram_log_path = Some(dir.path().join("foo.hdr"));
637 assert!(config.validate().is_ok());
638 }
639
640 #[test]
641 fn histogram_log_with_bare_filename_is_accepted() {
642 let mut config = config_with(valid_auto_meta());
646 config.histogram_log_path = Some("bare-filename.hdr".into());
647 assert!(
648 config.validate().is_ok(),
649 "validate err: {:?}",
650 config.validate(),
651 );
652 }
653
654 #[test]
655 fn histogram_log_validation_uses_unique_probe_per_call() {
656 let dir = tempfile::tempdir().unwrap();
660 let mut config = config_with(valid_auto_meta());
661 config.histogram_log_path = Some(dir.path().join("log.hdr"));
662 assert!(config.validate().is_ok());
663 assert!(config.validate().is_ok());
664 }
665}