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 TracingConfig {
355 #[must_use]
359 pub fn local(trace_identifier: &str) -> Self {
360 Self {
361 remote_layer: None,
362 debug_log_file: None,
363 chrome_trace_prefix: None,
364 flamegraph_prefix: None,
365 trace_identifier: trace_identifier.to_string(),
366 profile_level: None,
367 tokio_console: false,
368 tokio_console_port: None,
369 }
370 }
371}
372
373impl Default for TracingConfig {
374 fn default() -> Self {
375 Self {
376 remote_layer: None,
377 debug_log_file: None,
378 chrome_trace_prefix: None,
379 flamegraph_prefix: None,
380 trace_identifier: "unknown".to_string(),
381 profile_level: None,
382 tokio_console: false,
383 tokio_console_port: None,
384 }
385 }
386}
387
388#[cfg(test)]
389mod auto_meta_validation_tests {
390 use super::*;
391
392 fn valid_auto_meta() -> AutoMetaThrottleConfig {
393 AutoMetaThrottleConfig {
394 initial_cwnd: 1,
395 min_cwnd: 1,
396 max_cwnd: 4096,
397 alpha: 1.3,
398 beta: 1.8,
399 increase_step: 1,
400 decrease_step: 1,
401 baseline_percentile: 0.1,
402 current_percentile: 0.5,
403 long_window: std::time::Duration::from_secs(10),
404 short_window: std::time::Duration::from_secs(1),
405 tick_interval: std::time::Duration::from_millis(50),
406 }
407 }
408
409 fn config_with(auto: AutoMetaThrottleConfig) -> ThrottleConfig {
410 ThrottleConfig {
411 max_open_files: None,
412 ops_throttle: 0,
413 iops_throttle: 0,
414 chunk_size: 0,
415 auto_meta: Some(auto),
416 histogram_enabled: false,
417 histogram_log_path: None,
418 histogram_interval: std::time::Duration::from_secs(1),
419 }
420 }
421
422 #[test]
423 fn defaults_validate() {
424 assert!(config_with(valid_auto_meta()).validate().is_ok());
425 }
426
427 #[test]
428 fn min_cwnd_zero_is_rejected() {
429 let mut auto = valid_auto_meta();
430 auto.min_cwnd = 0;
431 let err = config_with(auto).validate().unwrap_err();
432 assert!(err.contains("min-cwnd"), "got: {err}");
433 }
434
435 #[test]
436 fn alpha_at_or_below_zero_is_rejected() {
437 let mut auto = valid_auto_meta();
438 auto.alpha = 0.0;
439 assert!(config_with(auto).validate().is_err());
440 let mut auto = valid_auto_meta();
441 auto.alpha = -0.5;
442 assert!(config_with(auto).validate().is_err());
443 }
444
445 #[test]
446 fn alpha_below_one_is_accepted() {
447 let mut auto = valid_auto_meta();
451 auto.alpha = 0.9;
452 auto.beta = 1.1;
453 assert!(config_with(auto).validate().is_ok());
454 }
455
456 #[test]
457 fn beta_at_or_below_zero_is_rejected() {
458 let mut auto = valid_auto_meta();
459 auto.alpha = 0.5;
460 auto.beta = 0.0;
461 let err = config_with(auto).validate().unwrap_err();
462 assert!(err.contains("beta"), "got: {err}");
463 }
464
465 #[test]
466 fn cross_percentile_config_validates() {
467 let mut auto = valid_auto_meta();
471 auto.baseline_percentile = 0.4;
472 auto.current_percentile = 0.6;
473 assert!(config_with(auto).validate().is_ok());
474 }
475
476 #[test]
477 fn baseline_percentile_above_current_is_rejected() {
478 let mut auto = valid_auto_meta();
479 auto.baseline_percentile = 0.6;
480 auto.current_percentile = 0.4;
481 let err = config_with(auto).validate().unwrap_err();
482 assert!(
483 err.contains("baseline-percentile") && err.contains("current-percentile"),
484 "got: {err}",
485 );
486 }
487
488 #[test]
489 fn baseline_percentile_out_of_range_is_rejected() {
490 let mut auto = valid_auto_meta();
491 auto.baseline_percentile = 1.0;
492 let err = config_with(auto).validate().unwrap_err();
493 assert!(err.contains("baseline-percentile"), "got: {err}");
494 }
495
496 #[test]
497 fn non_finite_alpha_or_beta_is_rejected() {
498 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
503 let mut auto = valid_auto_meta();
504 auto.alpha = bad;
505 assert!(
506 config_with(auto).validate().is_err(),
507 "alpha={bad} must be rejected",
508 );
509 let mut auto = valid_auto_meta();
510 auto.beta = bad;
511 assert!(
512 config_with(auto).validate().is_err(),
513 "beta={bad} must be rejected",
514 );
515 }
516 }
517
518 #[test]
519 fn current_percentile_out_of_range_is_rejected() {
520 let mut auto = valid_auto_meta();
521 auto.current_percentile = 1.0;
522 let err = config_with(auto).validate().unwrap_err();
523 assert!(err.contains("current-percentile"), "got: {err}");
524 }
525
526 #[test]
527 fn ops_throttle_below_floor_is_rejected_under_auto_meta() {
528 let mut config = config_with(valid_auto_meta());
529 config.ops_throttle = 5;
530 let err = config.validate().unwrap_err();
531 assert!(
532 err.contains("ops-throttle") && err.contains("auto-meta-throttle"),
533 "got: {err}",
534 );
535 }
536
537 #[test]
538 fn ops_throttle_at_or_above_floor_is_accepted_under_auto_meta() {
539 let mut config = config_with(valid_auto_meta());
540 config.ops_throttle = AUTO_META_MIN_OPS_THROTTLE;
541 assert!(config.validate().is_ok());
542 config.ops_throttle = AUTO_META_MIN_OPS_THROTTLE + 100;
543 assert!(config.validate().is_ok());
544 }
545
546 #[test]
547 fn ops_throttle_below_floor_is_fine_without_auto_meta() {
548 let config = ThrottleConfig {
552 max_open_files: None,
553 ops_throttle: 5,
554 iops_throttle: 0,
555 chunk_size: 0,
556 auto_meta: None,
557 histogram_enabled: false,
558 histogram_log_path: None,
559 histogram_interval: std::time::Duration::from_secs(1),
560 };
561 assert!(config.validate().is_ok());
562 }
563
564 #[test]
565 fn alpha_greater_than_beta_is_rejected() {
566 let mut auto = valid_auto_meta();
567 auto.alpha = 1.6;
568 auto.beta = 1.5;
569 let err = config_with(auto).validate().unwrap_err();
570 assert!(err.contains("alpha") && err.contains("beta"), "got: {err}");
571 }
572
573 #[test]
574 fn histogram_log_without_throttle_is_rejected() {
575 let config = ThrottleConfig {
577 max_open_files: None,
578 ops_throttle: 0,
579 iops_throttle: 0,
580 chunk_size: 0,
581 auto_meta: None,
582 histogram_log_path: Some("/tmp/x.hdr".into()),
583 histogram_enabled: false,
584 histogram_interval: std::time::Duration::from_secs(1),
585 };
586 let err = config.validate().unwrap_err();
587 assert!(
588 err.contains("histogram") && err.contains("auto-meta-throttle"),
589 "got: {err}"
590 );
591 }
592
593 #[test]
594 fn histogram_enabled_without_throttle_is_rejected() {
595 let config = ThrottleConfig {
598 max_open_files: None,
599 ops_throttle: 0,
600 iops_throttle: 0,
601 chunk_size: 0,
602 auto_meta: None,
603 histogram_enabled: true,
604 histogram_log_path: None,
605 histogram_interval: std::time::Duration::from_secs(1),
606 };
607 let err = config.validate().unwrap_err();
608 assert!(
609 err.contains("histogram") && err.contains("auto-meta-throttle"),
610 "got: {err}"
611 );
612 }
613
614 #[test]
615 fn histogram_interval_below_floor_is_rejected() {
616 let mut config = config_with(valid_auto_meta());
617 config.histogram_enabled = true;
618 config.histogram_interval = std::time::Duration::from_millis(50);
619 let err = config.validate().unwrap_err();
620 assert!(err.contains("histogram-interval"), "got: {err}");
621 }
622
623 #[test]
624 fn histogram_interval_above_ceiling_is_rejected() {
625 let mut config = config_with(valid_auto_meta());
626 config.histogram_enabled = true;
627 config.histogram_interval = std::time::Duration::from_secs(120);
628 let err = config.validate().unwrap_err();
629 assert!(err.contains("histogram-interval"), "got: {err}");
630 }
631
632 #[test]
633 fn histogram_defaults_pass_validation() {
634 let mut config = config_with(valid_auto_meta());
635 config.histogram_enabled = true;
636 config.histogram_interval = std::time::Duration::from_secs(1);
637 assert!(config.validate().is_ok());
638 }
639
640 #[test]
641 fn histogram_log_with_missing_parent_is_rejected() {
642 let mut config = config_with(valid_auto_meta());
643 config.histogram_log_path = Some("/nonexistent-dir-12345/foo.hdr".into());
644 let err = config.validate().unwrap_err();
645 assert!(
646 err.contains("histogram-log") && err.contains("parent"),
647 "got: {err}",
648 );
649 }
650
651 #[test]
652 fn histogram_log_with_writable_parent_is_accepted() {
653 let dir = tempfile::tempdir().unwrap();
654 let mut config = config_with(valid_auto_meta());
655 config.histogram_log_path = Some(dir.path().join("foo.hdr"));
656 assert!(config.validate().is_ok());
657 }
658
659 #[test]
660 fn histogram_log_with_bare_filename_is_accepted() {
661 let mut config = config_with(valid_auto_meta());
665 config.histogram_log_path = Some("bare-filename.hdr".into());
666 assert!(
667 config.validate().is_ok(),
668 "validate err: {:?}",
669 config.validate(),
670 );
671 }
672
673 #[test]
674 fn histogram_log_validation_uses_unique_probe_per_call() {
675 let dir = tempfile::tempdir().unwrap();
679 let mut config = config_with(valid_auto_meta());
680 config.histogram_log_path = Some(dir.path().join("log.hdr"));
681 assert!(config.validate().is_ok());
682 assert!(config.validate().is_ok());
683 }
684}