Skip to main content

file_engine/planner/
config.rs

1use 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
38/// Used when there's no median to derive a count cap from (empty input,
39/// or every entry is zero bytes) — still bounds a batch by count even
40/// though the byte budget alone wouldn't constrain it.
41const FALLBACK_MAX_FILES_PER_BATCH: usize = 1000;
42
43impl BatchConfig {
44    /// `max_files_per_batch`, resolved: the explicit override if set,
45    /// otherwise `max_bytes_per_batch / median(entries' sizes)`, floored
46    /// at 1 so a median larger than the byte budget still allows a batch
47    /// of one rather than zero.
48    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        // Sizes 10, 20, 30, 40 -> median = (20 + 30) / 2 = 25.
106        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}