file_engine/planner/
config.rs1use crate::profiler::Entry;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum SortOrder {
5 Ascending,
6 Descending,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ErrorStrategy {
11 #[default]
12 ContinueAndCollect,
13 AbortOnError,
14 Undo,
15}
16
17pub const DEFAULT_MAX_BYTES_PER_BATCH: u64 = 8 * 1024 * 1024;
18
19#[derive(Debug, Clone)]
20pub struct BatchConfig {
21 pub max_bytes_per_batch: u64,
22 pub max_files_per_batch: Option<usize>,
23 pub sort_order: SortOrder,
24 pub error_strategy: ErrorStrategy,
25}
26
27impl Default for BatchConfig {
28 fn default() -> Self {
29 Self {
30 max_bytes_per_batch: DEFAULT_MAX_BYTES_PER_BATCH,
31 max_files_per_batch: None,
32 sort_order: SortOrder::Descending,
33 error_strategy: ErrorStrategy::default(),
34 }
35 }
36}
37
38const FALLBACK_MAX_FILES_PER_BATCH: usize = 1000;
42
43impl BatchConfig {
44 pub(crate) fn resolved_max_files_per_batch(&self, entries: &[Entry]) -> usize {
49 if let Some(n) = self.max_files_per_batch {
50 return n;
51 }
52
53 let median = median_size(entries);
54 if median == 0 {
55 return FALLBACK_MAX_FILES_PER_BATCH;
56 }
57
58 ((self.max_bytes_per_batch / median) as usize).max(1)
59 }
60}
61
62fn median_size(entries: &[Entry]) -> u64 {
63 if entries.is_empty() {
64 return 0;
65 }
66
67 let mut sizes: Vec<u64> = entries.iter().map(|e| e.size).collect();
68 sizes.sort_unstable();
69
70 let mid = sizes.len() / 2;
71 if sizes.len() % 2 == 0 {
72 (sizes[mid - 1] + sizes[mid]) / 2
73 } else {
74 sizes[mid]
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use std::path::PathBuf;
81
82 use super::*;
83
84 fn entry(size: u64) -> Entry {
85 Entry {
86 path: PathBuf::from("x"),
87 relative_path: PathBuf::from("x"),
88 size,
89 modified: None,
90 }
91 }
92
93 #[test]
94 fn explicit_override_wins_regardless_of_sizes() {
95 let config = BatchConfig {
96 max_files_per_batch: Some(7),
97 ..BatchConfig::default()
98 };
99 let entries: Vec<Entry> = vec![entry(1), entry(1_000_000)];
100 assert_eq!(config.resolved_max_files_per_batch(&entries), 7);
101 }
102
103 #[test]
104 fn derived_from_median_with_even_length_input() {
105 let entries: Vec<Entry> = vec![entry(40), entry(10), entry(30), entry(20)];
107 let config = BatchConfig {
108 max_bytes_per_batch: 250,
109 max_files_per_batch: None,
110 ..BatchConfig::default()
111 };
112 assert_eq!(config.resolved_max_files_per_batch(&entries), 10);
113 }
114
115 #[test]
116 fn derivation_on_empty_entries_does_not_panic() {
117 let config = BatchConfig {
118 max_files_per_batch: None,
119 ..BatchConfig::default()
120 };
121 assert_eq!(
122 config.resolved_max_files_per_batch(&[]),
123 FALLBACK_MAX_FILES_PER_BATCH
124 );
125 }
126
127 #[test]
128 fn derivation_on_all_zero_byte_entries_does_not_panic() {
129 let entries: Vec<Entry> = vec![entry(0), entry(0), entry(0)];
130 let config = BatchConfig {
131 max_files_per_batch: None,
132 ..BatchConfig::default()
133 };
134 assert_eq!(
135 config.resolved_max_files_per_batch(&entries),
136 FALLBACK_MAX_FILES_PER_BATCH
137 );
138 }
139
140 #[test]
141 fn error_strategy_defaults_to_continue_and_collect() {
142 assert_eq!(BatchConfig::default().error_strategy, ErrorStrategy::ContinueAndCollect);
143 }
144}