1use std::collections::hash_map::Entry;
16use std::collections::{BTreeMap, HashMap, HashSet};
17use std::fmt::Debug;
18use std::sync::{Arc, LazyLock};
19
20use common_base::readable_size::ReadableSize;
21use common_telemetry::{debug, info};
22use common_time::Timestamp;
23use common_time::range::TimestampRange;
24use common_time::timestamp::TimeUnit;
25use common_time::timestamp_millis::BucketAligned;
26use snafu::ResultExt;
27use store_api::storage::RegionId;
28
29use crate::compaction::CompactionOutput;
30use crate::compaction::buckets::infer_time_bucket;
31use crate::compaction::compactor::CompactionRegion;
32use crate::compaction::picker::{Picker, PickerOutput, get_expired_ssts};
33use crate::compaction::run::{
34 Ranged, SortedRun, files_overlap, files_overlap_inclusive, find_sorted_runs,
35 find_sorted_runs_by_time_range, merge_primary_key_ranges, primary_key_ranges_overlap,
36};
37use crate::error::{JoinSnafu, Result};
38use crate::sst::file::{FileHandle, Level, overlaps};
39use crate::sst::primary_key::PrimaryKeyRangeMapper;
40use crate::sst::version::LevelMeta;
41
42const LEVEL_COMPACTED: Level = 1;
43
44#[derive(Clone, Copy, Debug)]
45enum PickPhase {
46 HasL0,
47 L1FileReduction,
48 L1OverlapOnly,
49}
50
51impl PickPhase {
52 fn applies_to(self, window: &Window) -> bool {
53 match self {
54 Self::HasL0 => window.files().any(|file| file.level() == 0),
55 Self::L1FileReduction | Self::L1OverlapOnly => {
56 window.files().any(|file| file.level() != 0)
57 }
58 }
59 }
60}
61
62const PICK_PHASES: [PickPhase; 3] = [
63 PickPhase::HasL0,
64 PickPhase::L1FileReduction,
65 PickPhase::L1OverlapOnly,
66];
67
68struct WindowPickContext<'a> {
69 active_window: Option<i64>,
70 files: &'a Window,
71 windows: &'a BTreeMap<i64, Window>,
72 phase: PickPhase,
73 primary_key_mapper: &'a PrimaryKeyRangeMapper,
74}
75
76struct WindowOutputContext {
77 active_window: Option<i64>,
78 time_window_size: Option<i64>,
79 max_outputs: Option<usize>,
80 primary_key_mapper: Arc<PrimaryKeyRangeMapper>,
81}
82
83const MAX_L1_L0_ROW_RATIO: usize = 2;
85
86const DEFAULT_MAX_INPUT_FILES: usize = 16;
88
89const MAX_INPUT_FILES_ENV: &str = "GREPTIME_TWCS_MAX_INPUT_FILES";
90
91static MAX_INPUT_FILES: LazyLock<usize> = LazyLock::new(|| {
94 let env_value = std::env::var(MAX_INPUT_FILES_ENV).ok();
95 parse_max_input_files(env_value.as_deref())
96});
97
98fn parse_max_input_files(env_value: Option<&str>) -> usize {
99 env_value
100 .and_then(|env_value| env_value.parse().ok())
101 .filter(|max_input_files| *max_input_files >= 2)
102 .unwrap_or(DEFAULT_MAX_INPUT_FILES)
103}
104
105#[derive(Clone, Debug)]
108pub struct TwcsPicker {
109 pub trigger_file_num: usize,
111 pub active_window_l1_merge_trigger: usize,
113 pub inactive_window_trigger_file_num: usize,
115 pub inactive_window_l1_merge_trigger: usize,
117 pub time_window_seconds: Option<i64>,
119 pub max_output_file_size: Option<u64>,
122 pub append_mode: bool,
124 pub max_background_tasks: Option<usize>,
126 pub(crate) time_range: Option<TimestampRange>,
128}
129
130impl TwcsPicker {
131 async fn build_output(
132 &self,
133 region_id: RegionId,
134 time_windows: BTreeMap<i64, Window>,
135 context: WindowOutputContext,
136 ) -> Result<Vec<CompactionOutput>> {
137 let WindowOutputContext {
138 active_window,
139 time_window_size,
140 max_outputs,
141 primary_key_mapper,
142 } = context;
143 let mut output = vec![];
144 let windows = time_windows
145 .values()
146 .rev()
147 .filter(|window| {
148 !window.files.is_empty()
149 && self.time_range.as_ref().is_none_or(|time_range| {
150 time_window_size.is_none_or(|time_window_size| {
151 time_window_intersects_range(
152 window.time_window,
153 time_window_size,
154 time_range,
155 )
156 })
157 })
158 })
159 .map(|window| window.time_window)
160 .collect::<Vec<_>>();
161 let time_windows = Arc::new(time_windows);
162 let chunk_size = self.max_background_tasks.unwrap_or(windows.len()).max(1);
165 let mut selected_windows = HashSet::new();
166 'phases: for phase in PICK_PHASES {
167 for chunk in windows.chunks(chunk_size) {
168 let mut handles = Vec::with_capacity(chunk.len());
169 for window in chunk {
170 if selected_windows.contains(window) {
171 continue;
172 }
173 if !time_windows
174 .get(window)
175 .is_some_and(|files| phase.applies_to(files))
176 {
177 continue;
178 }
179 let picker = self.clone();
180 let time_windows = time_windows.clone();
181 let window = *window;
182 let primary_key_mapper = primary_key_mapper.clone();
183 handles.push(common_runtime::spawn_blocking_compact(move || {
184 time_windows.get(&window).map(|files| {
185 (
186 window,
187 picker.find_inputs(
188 region_id,
189 WindowPickContext {
190 active_window,
191 files,
192 windows: &time_windows,
193 phase,
194 primary_key_mapper: &primary_key_mapper,
195 },
196 ),
197 )
198 })
199 }));
200 tokio::task::yield_now().await;
201 }
202 for result in futures::future::join_all(handles).await {
203 let Some((window, (inputs, filter_deleted))) = result.context(JoinSnafu)?
204 else {
205 continue;
206 };
207 if inputs.is_empty() {
208 continue;
209 }
210
211 selected_windows.insert(window);
212 output.push(CompactionOutput {
213 output_level: LEVEL_COMPACTED, inputs,
215 filter_deleted,
216 output_time_range: None, });
218
219 if let Some(max_outputs) = max_outputs
220 && output.len() >= max_outputs
221 {
222 debug!(
223 "Region ({:?}) compaction output limit ({}) reached, remaining candidates discarded",
224 region_id, max_outputs
225 );
226 break 'phases;
227 }
228 }
229 }
230 }
231 output.reverse();
234 Ok(output)
235 }
236
237 fn find_inputs(
238 &self,
239 region_id: RegionId,
240 context: WindowPickContext<'_>,
241 ) -> (Vec<FileHandle>, bool) {
242 let WindowPickContext {
243 active_window,
244 files,
245 windows,
246 phase,
247 primary_key_mapper,
248 } = context;
249 let is_active_window = active_window == Some(files.time_window);
250 let window = &files.time_window;
251 let mut files_to_merge: Vec<_> = files.files().cloned().collect();
252
253 if self.append_mode
255 && let Some(max_size) = self.max_output_file_size
256 {
257 let (kept_files, ignored_files) = files_to_merge
258 .into_iter()
259 .partition(|file| file.size() <= max_size);
260 files_to_merge = kept_files;
261 if !ignored_files.is_empty() {
262 info!(
263 "Skipped {} large files in append mode for region {}, window {}, max_size: {}",
264 ignored_files.len(),
265 region_id,
266 window,
267 max_size
268 );
269 }
270 }
271
272 let (l0_files, l1_files): (Vec<_>, Vec<_>) = files_to_merge
273 .into_iter()
274 .partition(|file| file.level() == 0);
275 let num_l0_files = l0_files.len();
276 let num_l1_files = l1_files.len();
277 if !is_active_window
278 && files.files.len() < self.inactive_window_trigger_file_num
279 && num_l1_files < self.inactive_window_l1_merge_trigger
280 {
281 return (vec![], false);
282 }
283 let (inputs, found_runs) = if is_active_window {
284 match phase {
285 PickPhase::HasL0 if num_l0_files >= self.trigger_file_num => pick_candidate_files(
286 l0_files,
287 self.max_output_file_size,
288 pick_count_first,
289 primary_key_mapper,
290 ),
291 PickPhase::L1FileReduction | PickPhase::L1OverlapOnly
292 if num_l1_files >= self.active_window_l1_merge_trigger =>
293 {
294 pick_l1_candidate_files(
295 l1_files,
296 self.max_output_file_size,
297 phase,
298 primary_key_mapper,
299 )
300 }
301 _ => (vec![], 0),
302 }
303 } else {
304 pick_inactive_window_files(
305 l0_files,
306 l1_files,
307 InactiveWindowPick {
308 l0_file_num: self.inactive_window_trigger_file_num,
309 l1_file_num: self.inactive_window_l1_merge_trigger,
310 phase,
311 primary_key_mapper,
312 },
313 self.max_output_file_size,
314 )
315 };
316 if inputs.is_empty() {
317 return (inputs, false);
318 }
319
320 let filter_deleted = !self.append_mode
321 && !window_has_overlap(files, windows)
322 && !selected_overlaps_unselected(&inputs, files, primary_key_mapper);
323
324 if inputs.len() > 1 {
325 log_pick_result(
327 region_id,
328 *window,
329 active_window,
330 found_runs,
331 files.files.len(),
332 self.max_output_file_size,
333 filter_deleted,
334 &inputs,
335 );
336 }
337 (inputs, filter_deleted)
338 }
339}
340
341#[derive(Debug, Clone, Copy)]
355struct InactiveWindowPick<'a> {
356 l0_file_num: usize,
357 l1_file_num: usize,
358 phase: PickPhase,
359 primary_key_mapper: &'a PrimaryKeyRangeMapper,
360}
361
362fn pick_inactive_window_files(
363 l0_files: Vec<FileHandle>,
364 l1_files: Vec<FileHandle>,
365 pick: InactiveWindowPick<'_>,
366 max_output_file_size: Option<u64>,
367) -> (Vec<FileHandle>, usize) {
368 if !matches!(pick.phase, PickPhase::HasL0) {
369 return if l1_files.len() >= pick.l1_file_num {
370 pick_l1_candidate_files(
371 l1_files,
372 max_output_file_size,
373 pick.phase,
374 pick.primary_key_mapper,
375 )
376 } else {
377 (vec![], 0)
378 };
379 }
380
381 if l0_files.len() >= pick.l0_file_num {
382 let candidate = pick_candidate_files(
383 l0_files.clone(),
384 max_output_file_size,
385 pick_count_first,
386 pick.primary_key_mapper,
387 );
388 if !candidate.0.is_empty() {
389 return candidate;
390 }
391 }
392
393 let candidate = pick_candidate_files(
394 l0_files.clone(),
395 max_output_file_size,
396 pick_count_first,
397 pick.primary_key_mapper,
398 );
399 if !candidate.0.is_empty() {
400 return candidate;
401 }
402 let mut all_files = l0_files.clone();
403 all_files.extend(l1_files);
404 let candidate = pick_candidate_files(
405 all_files,
406 max_output_file_size,
407 pick_mixed_within_budget,
408 pick.primary_key_mapper,
409 );
410 if !candidate.0.is_empty() {
411 return candidate;
412 }
413
414 pick_candidate_files(
415 l0_files,
416 max_output_file_size,
417 pick_unbalanced_count_first,
418 pick.primary_key_mapper,
419 )
420}
421
422fn pick_l1_candidate_files(
423 l1_files: Vec<FileHandle>,
424 max_output_file_size: Option<u64>,
425 phase: PickPhase,
426 mapper: &PrimaryKeyRangeMapper,
427) -> (Vec<FileHandle>, usize) {
428 let picker = match phase {
429 PickPhase::L1FileReduction => pick_l1_file_reduction,
430 PickPhase::L1OverlapOnly => pick_l1_overlap_only,
431 PickPhase::HasL0 => return (vec![], 0),
432 };
433 pick_candidate_files(l1_files, max_output_file_size, picker, mapper)
434}
435
436type CandidatePicker =
437 fn(Vec<SortedRun<FileHandle>>, Option<u64>, &PrimaryKeyRangeMapper) -> Vec<FileHandle>;
438
439fn pick_candidate_files(
440 mut files: Vec<FileHandle>,
441 max_output_file_size: Option<u64>,
442 picker: CandidatePicker,
443 mapper: &PrimaryKeyRangeMapper,
444) -> (Vec<FileHandle>, usize) {
445 let sorted_runs = if files.len() < 1024 {
446 find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, mapper))
447 } else {
448 find_sorted_runs_by_time_range(&mut files)
449 };
450 let found_runs = sorted_runs.len();
451 (
452 picker(sorted_runs, max_output_file_size, mapper),
453 found_runs,
454 )
455}
456
457#[derive(Debug)]
458struct OrderedFile<'a> {
459 file: &'a FileHandle,
460 run_id: usize,
461 position_in_run: usize,
462}
463
464#[derive(Debug, Default)]
467struct Candidate {
468 num_files: usize,
470 total_size: usize,
472 largest_file_size: usize,
474 overlap_participants: usize,
477 l0_rows: usize,
479 l1_rows: usize,
481 has_l0: bool,
482 has_l1: bool,
483 has_unknown_rows: bool,
485}
486
487impl Candidate {
488 fn absorb(
492 &mut self,
493 file: &OrderedFile,
494 preceding: &[OrderedFile],
495 participations: &mut Vec<bool>,
496 mapper: &PrimaryKeyRangeMapper,
497 ) {
498 self.num_files += 1;
499 let file_size = file.file.size() as usize;
500 self.total_size += file_size;
501 self.largest_file_size = self.largest_file_size.max(file_size);
502 self.absorb_level_rows(file.file);
503
504 let mut participates = false;
505 for (offset, other) in preceding.iter().enumerate() {
506 if file.run_id != other.run_id && files_overlap_inclusive(file.file, other.file, mapper)
507 {
508 if !participations[offset] {
509 participations[offset] = true;
510 self.overlap_participants += 1;
511 }
512 participates = true;
513 }
514 }
515 participations.push(participates);
516 if participates {
517 self.overlap_participants += 1;
518 }
519 }
520
521 fn absorb_level_rows(&mut self, file: &FileHandle) {
522 let num_rows = file.num_rows();
523 self.has_unknown_rows |= num_rows == 0;
524 if file.level() == 0 {
525 self.has_l0 = true;
526 self.l0_rows = self.l0_rows.saturating_add(num_rows);
527 } else {
528 self.has_l1 = true;
529 self.l1_rows = self.l1_rows.saturating_add(num_rows);
530 }
531 }
532
533 fn predicted_output_files(&self, max_output_file_size: Option<u64>) -> usize {
538 match max_output_file_size {
539 Some(max) if max > 0 => self.total_size.div_ceil(max as usize).max(1),
540 _ => 1,
541 }
542 }
543
544 fn file_reduction(&self, max_output_file_size: Option<u64>) -> usize {
548 self.num_files
549 .saturating_sub(self.predicted_output_files(max_output_file_size))
550 }
551
552 fn is_balanced(&self) -> bool {
555 self.largest_file_size <= self.total_size - self.largest_file_size
556 }
557
558 fn has_balanced_level_rows(&self) -> bool {
561 !self.has_l0
562 || !self.has_l1
563 || self.has_unknown_rows
564 || self.l1_rows <= self.l0_rows.saturating_mul(MAX_L1_L0_ROW_RATIO)
565 }
566
567 fn has_mixed_levels(&self) -> bool {
568 self.has_l0 && self.has_l1
569 }
570
571 fn within_rewrite_budget(&self, max_output_file_size: Option<u64>) -> bool {
576 match max_output_file_size {
577 Some(limit) if limit > 0 => self.total_size <= limit as usize,
578 _ => true,
579 }
580 }
581
582 fn makes_progress(&self, max_output_file_size: Option<u64>) -> bool {
587 self.file_reduction(max_output_file_size) > 0 || self.overlap_participants > 0
588 }
589
590 fn score(&self, max_output_file_size: Option<u64>) -> CandidateScore {
591 CandidateScore {
592 file_reduction: self.file_reduction(max_output_file_size),
593 overlap_participants: self.overlap_participants,
594 total_size: self.total_size,
595 }
596 }
597}
598
599#[derive(Debug)]
603struct CandidateScore {
604 file_reduction: usize,
605 overlap_participants: usize,
606 total_size: usize,
607}
608
609impl CandidateScore {
610 fn is_better_than(&self, other: &Self) -> bool {
611 self.file_reduction
612 .cmp(&other.file_reduction)
613 .then_with(|| self.overlap_participants.cmp(&other.overlap_participants))
614 .then_with(|| other.total_size.cmp(&self.total_size))
615 .is_gt()
616 }
617}
618
619fn pick_count_first(
635 sorted_runs: Vec<SortedRun<FileHandle>>,
636 max_output_file_size: Option<u64>,
637 mapper: &PrimaryKeyRangeMapper,
638) -> Vec<FileHandle> {
639 pick_count_first_where(
640 sorted_runs,
641 max_output_file_size,
642 mapper,
643 is_balanced_candidate,
644 )
645}
646
647fn pick_l1_file_reduction(
648 sorted_runs: Vec<SortedRun<FileHandle>>,
649 max_output_file_size: Option<u64>,
650 mapper: &PrimaryKeyRangeMapper,
651) -> Vec<FileHandle> {
652 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
653 is_balanced_candidate(candidate) && candidate.file_reduction(max_output_file_size) > 0
654 })
655}
656
657fn pick_l1_overlap_only(
658 sorted_runs: Vec<SortedRun<FileHandle>>,
659 max_output_file_size: Option<u64>,
660 mapper: &PrimaryKeyRangeMapper,
661) -> Vec<FileHandle> {
662 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
663 is_balanced_candidate(candidate)
664 && candidate.file_reduction(max_output_file_size) == 0
665 && candidate.overlap_participants > 0
666 })
667}
668
669#[cfg(test)]
670fn pick_mixed_count_first(
671 sorted_runs: Vec<SortedRun<FileHandle>>,
672 max_output_file_size: Option<u64>,
673 mapper: &PrimaryKeyRangeMapper,
674) -> Vec<FileHandle> {
675 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
676 candidate.has_mixed_levels() && is_balanced_candidate(candidate)
677 })
678}
679
680fn pick_mixed_within_budget(
683 sorted_runs: Vec<SortedRun<FileHandle>>,
684 max_output_file_size: Option<u64>,
685 mapper: &PrimaryKeyRangeMapper,
686) -> Vec<FileHandle> {
687 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
688 candidate.has_mixed_levels() && candidate.within_rewrite_budget(max_output_file_size)
689 })
690}
691
692fn pick_unbalanced_count_first(
696 sorted_runs: Vec<SortedRun<FileHandle>>,
697 max_output_file_size: Option<u64>,
698 mapper: &PrimaryKeyRangeMapper,
699) -> Vec<FileHandle> {
700 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |_| true)
701}
702
703fn is_balanced_candidate(candidate: &Candidate) -> bool {
704 candidate.is_balanced() && candidate.has_balanced_level_rows()
705}
706
707fn pick_count_first_where(
708 sorted_runs: Vec<SortedRun<FileHandle>>,
709 max_output_file_size: Option<u64>,
710 mapper: &PrimaryKeyRangeMapper,
711 is_eligible: impl Fn(&Candidate) -> bool,
712) -> Vec<FileHandle> {
713 let files = ordered_files(&sorted_runs);
714
715 let mut best = None;
716 for left in 0..files.len() {
717 let mut candidate = Candidate::default();
718 let right_bound = left.saturating_add(*MAX_INPUT_FILES).min(files.len());
719 let mut participations: Vec<bool> = Vec::with_capacity(right_bound - left);
720 for right in left..right_bound {
721 candidate.absorb(
722 &files[right],
723 &files[left..right],
724 &mut participations,
725 mapper,
726 );
727 if candidate.num_files < 2
728 || !is_eligible(&candidate)
729 || !candidate.makes_progress(max_output_file_size)
730 {
731 continue;
732 }
733
734 let score = candidate.score(max_output_file_size);
735 if best
736 .as_ref()
737 .is_none_or(|(best_score, _)| score.is_better_than(best_score))
738 {
739 best = Some((score, &files[left..=right]));
740 }
741 }
742 }
743
744 let Some((_, best)) = best else {
745 return vec![];
746 };
747 best.iter().map(|file| file.file.clone()).collect()
748}
749
750fn ordered_files(sorted_runs: &[SortedRun<FileHandle>]) -> Vec<OrderedFile<'_>> {
753 let mut files = sorted_runs
754 .iter()
755 .enumerate()
756 .flat_map(|(run_id, run)| {
757 run.items()
758 .iter()
759 .enumerate()
760 .map(move |(position_in_run, file)| OrderedFile {
761 file,
762 run_id,
763 position_in_run,
764 })
765 })
766 .collect::<Vec<_>>();
767 files.sort_unstable_by(|lhs, rhs| {
768 let (lhs_start, lhs_end) = lhs.file.range();
769 let (rhs_start, rhs_end) = rhs.file.range();
770 lhs_start
771 .cmp(&rhs_start)
772 .then_with(|| rhs_end.cmp(&lhs_end))
773 .then_with(|| lhs.run_id.cmp(&rhs.run_id))
774 .then_with(|| lhs.position_in_run.cmp(&rhs.position_in_run))
775 });
776 files
777}
778
779fn selected_overlaps_unselected(
780 selected: &[FileHandle],
781 window: &Window,
782 mapper: &PrimaryKeyRangeMapper,
783) -> bool {
784 let Some((span_start, span_end)) = selected
787 .iter()
788 .map(Ranged::range)
789 .reduce(|(start_a, end_a), (start_b, end_b)| (start_a.min(start_b), end_a.max(end_b)))
790 else {
791 return false;
792 };
793 let selected_file_ids = selected
794 .iter()
795 .map(FileHandle::file_id)
796 .collect::<HashSet<_>>();
797 window
798 .files()
799 .filter(|file| {
800 let (start, end) = file.range();
801 start <= span_end && span_start <= end
802 })
803 .filter(|file| !selected_file_ids.contains(&file.file_id()))
804 .any(|unselected| {
805 selected
806 .iter()
807 .any(|selected| files_overlap_inclusive(selected, unselected, mapper))
808 })
809}
810
811#[allow(clippy::too_many_arguments)]
812fn log_pick_result(
813 region_id: RegionId,
814 window: i64,
815 active_window: Option<i64>,
816 found_runs: usize,
817 file_num: usize,
818 max_output_file_size: Option<u64>,
819 filter_deleted: bool,
820 inputs: &[FileHandle],
821) {
822 let input_file_str: Vec<String> = inputs
823 .iter()
824 .map(|f| {
825 let range = f.range();
826 let start = range.0.to_iso8601_string();
827 let end = range.1.to_iso8601_string();
828 let num_rows = f.num_rows();
829 format!(
830 "File{{id: {:?}, range: ({}, {}), size: {}, num rows: {} }}",
831 f.file_id(),
832 start,
833 end,
834 ReadableSize(f.size()),
835 num_rows
836 )
837 })
838 .collect();
839 let window_str = Timestamp::new_second(window).to_iso8601_string();
840 let active_window_str = active_window.map(|s| Timestamp::new_second(s).to_iso8601_string());
841 let max_output_file_size = max_output_file_size.map(|size| ReadableSize(size).to_string());
842 info!(
843 "Region ({:?}) compaction pick result: current window: {}, active window: {:?}, \
844 found runs: {}, file num: {}, max output file size: {:?}, filter deleted: {}, \
845 input files: {:?}",
846 region_id,
847 window_str,
848 active_window_str,
849 found_runs,
850 file_num,
851 max_output_file_size,
852 filter_deleted,
853 input_file_str
854 );
855}
856
857#[async_trait::async_trait]
858impl Picker for TwcsPicker {
859 async fn pick(&self, compaction_region: &CompactionRegion) -> Result<Option<PickerOutput>> {
860 self.pick_with_output_limit(compaction_region, self.max_background_tasks)
861 .await
862 }
863}
864
865impl TwcsPicker {
866 pub(crate) async fn pick_with_output_limit(
867 &self,
868 compaction_region: &CompactionRegion,
869 max_outputs: Option<usize>,
870 ) -> Result<Option<PickerOutput>> {
871 let region_id = compaction_region.region_id;
872 let picker = self.clone();
873 let compaction_region = compaction_region.clone();
874 let primary_key_mapper = compaction_region.current_version.ssts.primary_key_mapper();
875 let window_mapper = primary_key_mapper.clone();
876 let (expired_ssts, time_window_size, active_window, windows) =
877 common_runtime::spawn_blocking_compact(move || {
878 let levels = compaction_region.current_version.ssts.levels();
879 let expired_ssts = get_expired_ssts(
880 levels,
881 compaction_region.ttl,
882 Timestamp::current_millis(),
883 );
884 if !expired_ssts.is_empty() {
885 info!("Expired SSTs in region {}: {:?}", region_id, expired_ssts);
886 }
887 let expired_file_ids = expired_ssts
888 .iter()
889 .map(|file| file.file_id())
890 .collect::<HashSet<_>>();
891
892 let compaction_time_window = compaction_region
893 .current_version
894 .compaction_time_window
895 .map(|window| window.as_secs() as i64);
896 let time_window_size = compaction_time_window
897 .or(picker.time_window_seconds)
898 .unwrap_or_else(|| {
899 let inferred = infer_time_bucket(levels[0].files());
900 info!(
901 "Compaction window for region {} is not present, inferring from files: {:?}",
902 region_id, inferred
903 );
904 inferred
905 });
906
907 let windows = assign_to_windows(
908 levels
909 .iter()
910 .flat_map(LevelMeta::files)
911 .filter(|file| !expired_file_ids.contains(&file.file_id())),
912 time_window_size,
913 &window_mapper,
914 );
915 let active_window = find_active_window_by_sequence(
918 windows.values().flat_map(Window::files),
919 time_window_size,
920 )
921 .or_else(|| {
922 find_latest_window_in_seconds(
923 windows
924 .values()
925 .flat_map(Window::files)
926 .filter(|file| file.level() == 0),
927 time_window_size,
928 )
929 });
930
931 (expired_ssts, time_window_size, active_window, windows)
932 })
933 .await
934 .context(JoinSnafu)?;
935
936 let outputs = self
937 .build_output(
938 region_id,
939 windows,
940 WindowOutputContext {
941 active_window,
942 time_window_size: Some(time_window_size),
943 max_outputs,
944 primary_key_mapper,
945 },
946 )
947 .await?;
948
949 if outputs.is_empty() && expired_ssts.is_empty() {
950 return Ok(None);
951 }
952
953 let max_file_size = self
955 .max_output_file_size
956 .filter(|size| *size > 0)
957 .map(|size| size as usize);
958 Ok(Some(PickerOutput {
959 outputs,
960 expired_ssts,
961 time_window_size,
962 max_file_size,
963 }))
964 }
965}
966
967#[derive(Clone)]
968struct Window {
969 start: Timestamp,
970 end: Timestamp,
971 files: Vec<FileHandle>,
972 time_window: i64,
973 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
974}
975
976impl Window {
977 fn new_with_file(file: FileHandle, mapper: &PrimaryKeyRangeMapper) -> Self {
979 let (start, end) = file.time_range();
980 let primary_key_range = file.primary_key_range(mapper);
981 Self {
982 start,
983 end,
984 files: vec![file],
985 time_window: 0,
986 primary_key_range,
987 }
988 }
989
990 fn range(&self) -> (Timestamp, Timestamp) {
992 (self.start, self.end)
993 }
994
995 fn add_file(&mut self, file: FileHandle, mapper: &PrimaryKeyRangeMapper) {
997 let (start, end) = file.time_range();
998 self.start = self.start.min(start);
999 self.end = self.end.max(end);
1000 self.primary_key_range = merge_primary_key_ranges(
1001 self.primary_key_range.take(),
1002 file.primary_key_range(mapper),
1003 );
1004 self.files.push(file);
1005 }
1006
1007 fn files(&self) -> impl Iterator<Item = &FileHandle> {
1008 self.files.iter()
1009 }
1010}
1011
1012fn assign_to_windows<'a>(
1014 files: impl Iterator<Item = &'a FileHandle>,
1015 time_window_size: i64,
1016 mapper: &PrimaryKeyRangeMapper,
1017) -> BTreeMap<i64, Window> {
1018 let mut windows: HashMap<i64, Window> = HashMap::new();
1019 for f in files {
1021 if f.compacting() {
1022 continue;
1023 }
1024 let (_, end) = f.time_range();
1025 let time_window = end
1026 .convert_to(TimeUnit::Second)
1027 .unwrap()
1028 .value()
1029 .align_to_ceil_by_bucket(time_window_size)
1030 .unwrap_or(i64::MIN);
1031
1032 match windows.entry(time_window) {
1033 Entry::Occupied(mut e) => {
1034 e.get_mut().add_file(f.clone(), mapper);
1035 }
1036 Entry::Vacant(e) => {
1037 let mut window = Window::new_with_file(f.clone(), mapper);
1038 window.time_window = time_window;
1039 e.insert(window);
1040 }
1041 }
1042 }
1043 windows.into_iter().collect()
1044}
1045
1046fn time_window_intersects_range(
1047 window_end: i64,
1048 time_window_size: i64,
1049 time_range: &TimestampRange,
1050) -> bool {
1051 let first_window = match time_range.start() {
1052 None => i64::MIN,
1053 Some(start) => {
1054 let Some(first_window) = start
1055 .convert_to(TimeUnit::Second)
1056 .and_then(|timestamp| timestamp.value().align_to_ceil_by_bucket(time_window_size))
1057 else {
1058 return false;
1059 };
1060 first_window
1061 }
1062 };
1063 let last_window = match time_range.end() {
1064 None => i64::MAX,
1065 Some(end) => {
1066 let Some(last_window) = end
1067 .convert_to_ceil(TimeUnit::Second)
1068 .and_then(|timestamp| timestamp.value().checked_sub(1))
1069 .and_then(|timestamp| timestamp.align_to_ceil_by_bucket(time_window_size))
1070 else {
1071 return false;
1072 };
1073 last_window
1074 }
1075 };
1076 (first_window..=last_window).contains(&window_end)
1077}
1078
1079fn window_has_overlap(this: &Window, windows: &BTreeMap<i64, Window>) -> bool {
1080 windows
1081 .values()
1082 .filter(|that| this.time_window != that.time_window)
1083 .any(|that| {
1084 overlaps(&this.range(), &that.range()) && {
1085 match (&this.primary_key_range, &that.primary_key_range) {
1086 (Some(l), Some(r)) => primary_key_ranges_overlap(l, r),
1087 _ => true,
1088 }
1089 }
1090 })
1091}
1092
1093fn find_latest_window_in_seconds<'a>(
1096 files: impl Iterator<Item = &'a FileHandle>,
1097 time_window_size: i64,
1098) -> Option<i64> {
1099 let mut latest_timestamp = None;
1100 for f in files {
1101 let (_, end) = f.time_range();
1102 if let Some(latest) = latest_timestamp {
1103 if end > latest {
1104 latest_timestamp = Some(end);
1105 }
1106 } else {
1107 latest_timestamp = Some(end);
1108 }
1109 }
1110 latest_timestamp
1111 .and_then(|ts| ts.convert_to_ceil(TimeUnit::Second))
1112 .and_then(|ts| ts.value().align_to_ceil_by_bucket(time_window_size))
1113}
1114
1115fn find_active_window_by_sequence<'a>(
1126 files: impl Iterator<Item = &'a FileHandle>,
1127 time_window_size: i64,
1128) -> Option<i64> {
1129 files
1130 .filter(|f| f.meta_ref().sequence.is_some())
1131 .max_by_key(|f| f.meta_ref().sequence)
1132 .and_then(|f| {
1133 f.time_range()
1134 .1
1135 .convert_to(TimeUnit::Second)
1136 .and_then(|ts| ts.value().align_to_ceil_by_bucket(time_window_size))
1137 })
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use std::collections::HashSet;
1143 use std::num::NonZeroU64;
1144 use std::sync::Arc;
1145 use std::time::Duration;
1146
1147 use common_base::Plugins;
1148 use common_time::range::TimestampRange;
1149 use store_api::storage::FileId;
1150
1151 use super::*;
1152 use crate::cache::CacheManager;
1153 use crate::compaction::compactor::CompactionVersion;
1154 use crate::compaction::test_util::{
1155 compaction_region_with_ssts, new_file_handle, new_file_handle_with_sequence,
1156 new_file_handle_with_size_and_sequence,
1157 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
1158 primary_key_mapper_for_test,
1159 };
1160 use crate::config::MitoConfig;
1161 use crate::region::options::RegionOptions;
1162 use crate::sst::file::{FileMeta, Level};
1163 use crate::sst::version::SstVersion;
1164 use crate::test_util::memtable_util::metadata_for_test;
1165 use crate::test_util::scheduler_util::SchedulerEnv;
1166
1167 fn assign_to_windows<'a>(
1168 files: impl Iterator<Item = &'a FileHandle>,
1169 time_window_size: i64,
1170 ) -> BTreeMap<i64, Window> {
1171 super::assign_to_windows(files, time_window_size, &primary_key_mapper_for_test())
1172 }
1173
1174 fn pick_count_first(
1175 sorted_runs: Vec<SortedRun<FileHandle>>,
1176 max_output_file_size: Option<u64>,
1177 ) -> Vec<FileHandle> {
1178 super::pick_count_first(
1179 sorted_runs,
1180 max_output_file_size,
1181 &primary_key_mapper_for_test(),
1182 )
1183 }
1184
1185 fn pick_mixed_count_first(
1186 sorted_runs: Vec<SortedRun<FileHandle>>,
1187 max_output_file_size: Option<u64>,
1188 ) -> Vec<FileHandle> {
1189 super::pick_mixed_count_first(
1190 sorted_runs,
1191 max_output_file_size,
1192 &primary_key_mapper_for_test(),
1193 )
1194 }
1195
1196 impl TwcsPicker {
1197 async fn build_output_with_time_range(
1198 &self,
1199 region_id: RegionId,
1200 time_windows: BTreeMap<i64, Window>,
1201 active_window: Option<i64>,
1202 time_window_size: Option<i64>,
1203 ) -> Result<Vec<CompactionOutput>> {
1204 self.build_output(
1205 region_id,
1206 time_windows,
1207 WindowOutputContext {
1208 active_window,
1209 time_window_size,
1210 max_outputs: self.max_background_tasks,
1211 primary_key_mapper: Arc::new(primary_key_mapper_for_test()),
1212 },
1213 )
1214 .await
1215 }
1216 }
1217
1218 #[test]
1219 fn test_cross_schema_pk_overlap_before_window_aggregation() {
1220 use crate::test_util::sst_util::{new_primary_key, sst_region_metadata};
1221
1222 let metadata = Arc::new(sst_region_metadata());
1223 let mut old = FileMeta {
1224 region_id: metadata.region_id,
1225 file_id: FileId::random(),
1226 time_range: (
1227 Timestamp::new_millisecond(0),
1228 Timestamp::new_millisecond(1000),
1229 ),
1230 primary_key_min: Some(new_primary_key(&["a"]).into()),
1231 primary_key_max: Some(new_primary_key(&["b"]).into()),
1232 ..Default::default()
1233 };
1234 let mut completed = new_primary_key(&["b"]);
1235 completed.push(0); let new = FileMeta {
1237 file_id: FileId::random(),
1238 time_range: (
1239 Timestamp::new_millisecond(500),
1240 Timestamp::new_millisecond(2000),
1241 ),
1242 primary_key_min: Some(completed.clone().into()),
1243 primary_key_max: Some(completed.clone().into()),
1244 ..old.clone()
1245 };
1246 assert!(old.primary_key_max < new.primary_key_min);
1247 let mut ssts = SstVersion::new(metadata);
1248 let old_id = old.file_id;
1249 let new_id = new.file_id;
1250 ssts.add_files(
1251 crate::test_util::new_noop_file_purger(),
1252 [old.clone(), new].into_iter(),
1253 );
1254 let old_file = &ssts.levels()[0].files[&old_id];
1255 let new_file = &ssts.levels()[0].files[&new_id];
1256 let ranges = ssts.primary_key_mapper();
1257 let windows = super::assign_to_windows([old_file, new_file].into_iter(), 1, &ranges);
1258 assert_eq!(2, windows.len());
1259 assert!(
1260 windows
1261 .values()
1262 .all(|window| window_has_overlap(window, &windows))
1263 );
1264
1265 let mut window = Window::new_with_file(old_file.clone(), &ranges);
1266 window.add_file(new_file.clone(), &ranges);
1267 assert!(selected_overlaps_unselected(
1268 std::slice::from_ref(new_file),
1269 &window,
1270 &ranges,
1271 ));
1272 assert_eq!(
1273 Some(completed.into()),
1274 window
1275 .primary_key_range
1276 .as_ref()
1277 .map(|range| range.1.clone())
1278 );
1279
1280 old.file_id = FileId::random();
1282 old.primary_key_max = old.primary_key_min.clone();
1283 let disjoint_id = old.file_id;
1284 ssts.add_files(crate::test_util::new_noop_file_purger(), [old].into_iter());
1285 let files = &ssts.levels()[0].files;
1286 assert!(!files_overlap_inclusive(
1287 &files[&disjoint_id],
1288 &files[&new_id],
1289 &ranges
1290 ));
1291 }
1292
1293 #[test]
1294 fn test_valid_max_input_files_env_overrides_default() {
1295 assert_eq!(64, parse_max_input_files(Some("64")));
1296 }
1297
1298 #[test]
1299 fn test_invalid_max_input_files_env_falls_back_to_default() {
1300 for env_value in [None, Some(""), Some("invalid"), Some("0"), Some("1")] {
1301 assert_eq!(16, parse_max_input_files(env_value));
1302 }
1303 }
1304
1305 async fn compaction_region_with_expired_sst() -> CompactionRegion {
1306 compaction_region_with_ssts(
1307 (1..=4).map(|sequence| FileMeta {
1308 file_id: FileId::random(),
1309 time_range: (
1310 Timestamp::new_millisecond(0),
1311 Timestamp::new_millisecond(10),
1312 ),
1313 level: 0,
1314 sequence: NonZeroU64::new(sequence),
1315 ..Default::default()
1316 }),
1317 Duration::from_millis(1),
1318 )
1319 .await
1320 }
1321
1322 #[tokio::test]
1323 async fn test_pick_normalizes_zero_output_size_to_unlimited() {
1324 let mut compaction_region = compaction_region_with_ssts(
1325 (1..=4).map(|sequence| new_file(0, 10, sequence, 100).meta_ref().clone()),
1326 Duration::from_secs(3600),
1327 )
1328 .await;
1329 compaction_region.ttl = None;
1330
1331 for (max_output_file_size, expected_max_file_size) in
1332 [(Some(0), None), (None, None), (Some(1024), Some(1024))]
1333 {
1334 let picker = TwcsPicker {
1335 trigger_file_num: 4,
1336 active_window_l1_merge_trigger: 8,
1337 inactive_window_trigger_file_num: 4,
1338 inactive_window_l1_merge_trigger: 8,
1339 time_window_seconds: Some(3),
1340 max_output_file_size,
1341 append_mode: false,
1342 max_background_tasks: None,
1343 time_range: None,
1344 };
1345
1346 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1347
1348 assert_eq!(output.outputs.len(), 1);
1349 assert_eq!(output.outputs[0].inputs.len(), 4);
1350 assert!(output.expired_ssts.is_empty());
1351 assert_eq!(output.max_file_size, expected_max_file_size);
1352 }
1353 }
1354
1355 #[tokio::test]
1356 async fn test_pick_expired_ssts_without_marking_compacting() {
1357 let picker = TwcsPicker {
1358 trigger_file_num: 4,
1359 active_window_l1_merge_trigger: 8,
1360 inactive_window_trigger_file_num: 4,
1361 inactive_window_l1_merge_trigger: 8,
1362 time_window_seconds: Some(3),
1363 max_output_file_size: None,
1364 append_mode: false,
1365 max_background_tasks: None,
1366 time_range: None,
1367 };
1368 let compaction_region = compaction_region_with_expired_sst().await;
1369
1370 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1371
1372 assert!(output.outputs.is_empty());
1373 assert!(!output.expired_ssts.is_empty());
1374 assert!(output.expired_ssts.iter().all(|file| !file.compacting()));
1375 }
1376
1377 #[tokio::test]
1378 async fn test_expired_sst_does_not_determine_active_window() {
1379 let now = Timestamp::current_millis().value();
1380 let files = [
1381 FileMeta {
1382 file_id: FileId::random(),
1383 time_range: (
1384 Timestamp::new_millisecond(0),
1385 Timestamp::new_millisecond(10),
1386 ),
1387 level: 0,
1388 sequence: NonZeroU64::new(100),
1389 ..Default::default()
1390 },
1391 FileMeta {
1392 file_id: FileId::random(),
1393 time_range: (
1394 Timestamp::new_millisecond(now - 1000),
1395 Timestamp::new_millisecond(now),
1396 ),
1397 level: 0,
1398 sequence: NonZeroU64::new(1),
1399 ..Default::default()
1400 },
1401 FileMeta {
1402 file_id: FileId::random(),
1403 time_range: (
1404 Timestamp::new_millisecond(now - 1000),
1405 Timestamp::new_millisecond(now),
1406 ),
1407 level: 0,
1408 sequence: NonZeroU64::new(2),
1409 ..Default::default()
1410 },
1411 ];
1412 let compaction_region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
1413 let picker = TwcsPicker {
1414 trigger_file_num: 4,
1415 active_window_l1_merge_trigger: 8,
1416 inactive_window_trigger_file_num: 2,
1417 inactive_window_l1_merge_trigger: 8,
1418 time_window_seconds: Some(3),
1419 max_output_file_size: None,
1420 append_mode: false,
1421 max_background_tasks: None,
1422 time_range: None,
1423 };
1424
1425 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1426
1427 assert_eq!(1, output.expired_ssts.len());
1428 assert!(output.outputs.is_empty());
1429 }
1430
1431 #[tokio::test]
1432 async fn test_active_window_survives_l0_consumption() {
1433 let env = SchedulerEnv::new().await;
1438 let metadata = metadata_for_test();
1439 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
1440 let mut ssts = SstVersion::new(metadata.clone());
1441 ssts.add_files(
1442 Arc::new(crate::sst::file_purger::NoopFilePurger),
1443 [100, 101].into_iter().map(|sequence| FileMeta {
1444 region_id: metadata.region_id,
1445 file_id: FileId::random(),
1446 time_range: (
1447 Timestamp::new_millisecond(0),
1448 Timestamp::new_millisecond(10),
1449 ),
1450 level: 1,
1451 sequence: NonZeroU64::new(sequence),
1452 ..Default::default()
1453 }),
1454 );
1455 let compaction_region = CompactionRegion {
1456 region_id: metadata.region_id,
1457 region_options: RegionOptions::default(),
1458 engine_config: Arc::new(MitoConfig::default()),
1459 region_metadata: metadata.clone(),
1460 cache_manager: Arc::new(CacheManager::default()),
1461 access_layer: env.access_layer,
1462 manifest_ctx,
1463 current_version: CompactionVersion {
1464 metadata,
1465 options: RegionOptions::default(),
1466 ssts: Arc::new(ssts),
1467 memtable_min_sequence: None,
1468 compaction_time_window: None,
1469 },
1470 file_purger: None,
1471 ttl: None,
1472 max_parallelism: 1,
1473 plugins: Plugins::new(),
1474 };
1475 let picker = TwcsPicker {
1476 trigger_file_num: 4,
1477 active_window_l1_merge_trigger: 8,
1478 inactive_window_trigger_file_num: 2,
1479 inactive_window_l1_merge_trigger: 8,
1480 time_window_seconds: Some(3600),
1481 max_output_file_size: None,
1482 append_mode: false,
1483 max_background_tasks: None,
1484 time_range: None,
1485 };
1486
1487 assert!(picker.pick(&compaction_region).await.unwrap().is_none());
1488 }
1489
1490 #[test]
1491 fn test_find_active_window_by_sequence() {
1492 let files = [
1497 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 1),
1498 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 1, 10),
1499 ];
1500 assert_eq!(Some(2), find_active_window_by_sequence(files.iter(), 1));
1501
1502 let files = [
1505 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 10),
1506 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 1),
1507 ];
1508 assert_eq!(Some(0), find_active_window_by_sequence(files.iter(), 1));
1509
1510 let files = [new_file_handle_with_sequence(
1513 FileId::random(),
1514 0,
1515 999,
1516 1,
1517 0,
1518 )];
1519 assert_eq!(None, find_active_window_by_sequence(files.iter(), 1));
1520 assert!(find_active_window_by_sequence(Vec::<FileHandle>::new().iter(), 1).is_none());
1521 }
1522
1523 #[test]
1524 fn test_active_window_falls_back_to_l0_rule_without_sequence() {
1525 let active_window_of = |files: &[FileHandle]| {
1526 find_active_window_by_sequence(files.iter(), 1).or_else(|| {
1527 find_latest_window_in_seconds(files.iter().filter(|f| f.level() == 0), 1)
1528 })
1529 };
1530
1531 let files = [
1533 new_file_handle_with_sequence(FileId::random(), 0, 999, 1, 0),
1534 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 0),
1535 ];
1536 assert_eq!(Some(3), active_window_of(&files));
1537
1538 let files = [new_file_handle_with_sequence(
1540 FileId::random(),
1541 0,
1542 999,
1543 1,
1544 0,
1545 )];
1546 assert_eq!(None, active_window_of(&files));
1547 }
1548
1549 #[test]
1550 fn test_get_latest_window_in_seconds() {
1551 assert_eq!(
1552 Some(1),
1553 find_latest_window_in_seconds([new_file_handle(FileId::random(), 0, 999, 0)].iter(), 1)
1554 );
1555 assert_eq!(
1556 Some(1),
1557 find_latest_window_in_seconds(
1558 [new_file_handle(FileId::random(), 0, 1000, 0)].iter(),
1559 1
1560 )
1561 );
1562
1563 assert_eq!(
1564 Some(-9223372036854000),
1565 find_latest_window_in_seconds(
1566 [new_file_handle(FileId::random(), i64::MIN, i64::MIN + 1, 0)].iter(),
1567 3600,
1568 )
1569 );
1570
1571 assert_eq!(
1572 (i64::MAX / 10000000 + 1) * 10000,
1573 find_latest_window_in_seconds(
1574 [new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0)].iter(),
1575 10000,
1576 )
1577 .unwrap()
1578 );
1579
1580 assert_eq!(
1581 Some((i64::MAX / 3600000 + 1) * 3600),
1582 find_latest_window_in_seconds(
1583 [
1584 new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0),
1585 new_file_handle(FileId::random(), 0, 1000, 0)
1586 ]
1587 .iter(),
1588 3600
1589 )
1590 );
1591 }
1592
1593 #[test]
1594 fn test_assign_to_windows() {
1595 let windows = assign_to_windows(
1596 [
1597 new_file_handle(FileId::random(), 0, 999, 0),
1598 new_file_handle(FileId::random(), 0, 999, 0),
1599 new_file_handle(FileId::random(), 0, 999, 0),
1600 new_file_handle(FileId::random(), 0, 999, 0),
1601 new_file_handle(FileId::random(), 0, 999, 0),
1602 ]
1603 .iter(),
1604 3,
1605 );
1606 let fgs = &windows.get(&0).unwrap().files;
1607 assert_eq!(5, fgs.len());
1608
1609 let files = [FileId::random(); 3];
1610 let windows = assign_to_windows(
1611 [
1612 new_file_handle(files[0], -2000, -3, 0),
1613 new_file_handle(files[1], 0, 2999, 0),
1614 new_file_handle(files[2], 50, 10001, 0),
1615 ]
1616 .iter(),
1617 3,
1618 );
1619 assert_eq!(
1620 files[0],
1621 windows
1622 .get(&0)
1623 .unwrap()
1624 .files()
1625 .next()
1626 .unwrap()
1627 .file_id()
1628 .file_id()
1629 );
1630 assert_eq!(
1631 files[1],
1632 windows
1633 .get(&3)
1634 .unwrap()
1635 .files()
1636 .next()
1637 .unwrap()
1638 .file_id()
1639 .file_id()
1640 );
1641 assert_eq!(
1642 files[2],
1643 windows
1644 .get(&12)
1645 .unwrap()
1646 .files()
1647 .next()
1648 .unwrap()
1649 .file_id()
1650 .file_id()
1651 );
1652 }
1653
1654 #[test]
1655 fn test_assign_files_to_windows() {
1656 let files = [
1657 FileId::random(),
1658 FileId::random(),
1659 FileId::random(),
1660 FileId::random(),
1661 ];
1662 let windows = assign_to_windows(
1663 [
1664 new_file_handle_with_sequence(files[0], 0, 999, 0, 1),
1665 new_file_handle_with_sequence(files[1], 0, 999, 0, 1),
1666 new_file_handle_with_sequence(files[2], 0, 999, 0, 2),
1667 new_file_handle_with_sequence(files[3], 0, 999, 0, 2),
1668 ]
1669 .iter(),
1670 3,
1671 );
1672 assert_eq!(windows.len(), 1);
1673 let window_files = &windows.get(&0).unwrap().files;
1674 assert_eq!(4, window_files.len());
1675 assert_eq!(
1676 window_files
1677 .iter()
1678 .map(|f| f.file_id().file_id())
1679 .collect::<HashSet<_>>(),
1680 files.into_iter().collect()
1681 );
1682 }
1683
1684 #[test]
1685 fn test_assign_compacting_to_windows() {
1686 let files = [
1687 new_file_handle(FileId::random(), 0, 999, 0),
1688 new_file_handle(FileId::random(), 0, 999, 0),
1689 new_file_handle(FileId::random(), 0, 999, 0),
1690 new_file_handle(FileId::random(), 0, 999, 0),
1691 new_file_handle(FileId::random(), 0, 999, 0),
1692 ];
1693 files[0].set_compacting(true);
1694 files[2].set_compacting(true);
1695 let mut windows = assign_to_windows(files.iter(), 3);
1696 let window0 = windows.remove(&0).unwrap();
1697 assert_eq!(3, window0.files.len());
1698 let candidates = window0
1699 .files
1700 .iter()
1701 .map(|f| f.file_id().file_id())
1702 .collect::<HashSet<_>>();
1703 assert_eq!(candidates.len(), 3);
1704 assert_eq!(
1705 candidates,
1706 [
1707 files[1].file_id().file_id(),
1708 files[3].file_id().file_id(),
1709 files[4].file_id().file_id()
1710 ]
1711 .into_iter()
1712 .collect::<HashSet<_>>()
1713 );
1714 }
1715
1716 type ExpectedWindowSpec = (i64, bool, Vec<(i64, i64)>);
1718
1719 fn check_assign_to_windows_with_overlapping(
1720 file_time_ranges: &[(i64, i64)],
1721 time_window: i64,
1722 expected_files: &[ExpectedWindowSpec],
1723 ) {
1724 let files: Vec<_> = (0..file_time_ranges.len())
1725 .map(|_| FileId::random())
1726 .collect();
1727
1728 let file_handles = files
1729 .iter()
1730 .zip(file_time_ranges.iter())
1731 .map(|(file_id, range)| new_file_handle(*file_id, range.0, range.1, 0))
1732 .collect::<Vec<_>>();
1733
1734 let windows = assign_to_windows(file_handles.iter(), time_window);
1735
1736 for (expected_window, overlapping, window_files) in expected_files {
1737 let actual_window = windows.get(expected_window).unwrap();
1738 let actual_overlapping = window_has_overlap(actual_window, &windows);
1739 assert_eq!(*overlapping, actual_overlapping);
1740 let mut file_ranges = actual_window
1741 .files
1742 .iter()
1743 .map(|f| {
1744 let (s, e) = f.time_range();
1745 (s.value(), e.value())
1746 })
1747 .collect::<Vec<_>>();
1748 file_ranges.sort_unstable_by(|l, r| l.0.cmp(&r.0).then(l.1.cmp(&r.1)));
1749 assert_eq!(window_files, &file_ranges);
1750 }
1751 }
1752
1753 #[test]
1754 fn test_assign_to_windows_with_overlapping() {
1755 check_assign_to_windows_with_overlapping(
1756 &[(0, 999), (1000, 1999), (2000, 2999)],
1757 2,
1758 &[
1759 (0, false, vec![(0, 999)]),
1760 (2, false, vec![(1000, 1999), (2000, 2999)]),
1761 ],
1762 );
1763
1764 check_assign_to_windows_with_overlapping(
1765 &[(0, 1), (0, 999), (100, 2999)],
1766 2,
1767 &[
1768 (0, true, vec![(0, 1), (0, 999)]),
1769 (2, true, vec![(100, 2999)]),
1770 ],
1771 );
1772
1773 check_assign_to_windows_with_overlapping(
1774 &[(0, 999), (1000, 1999), (2000, 2999), (3000, 3999)],
1775 2,
1776 &[
1777 (0, false, vec![(0, 999)]),
1778 (2, false, vec![(1000, 1999), (2000, 2999)]),
1779 (4, false, vec![(3000, 3999)]),
1780 ],
1781 );
1782
1783 check_assign_to_windows_with_overlapping(
1784 &[
1785 (0, 999),
1786 (1000, 1999),
1787 (2000, 2999),
1788 (3000, 3999),
1789 (0, 3999),
1790 ],
1791 2,
1792 &[
1793 (0, true, vec![(0, 999)]),
1794 (2, true, vec![(1000, 1999), (2000, 2999)]),
1795 (4, true, vec![(0, 3999), (3000, 3999)]),
1796 ],
1797 );
1798
1799 check_assign_to_windows_with_overlapping(
1800 &[
1801 (0, 999),
1802 (1000, 1999),
1803 (2000, 2999),
1804 (3000, 3999),
1805 (1999, 3999),
1806 ],
1807 2,
1808 &[
1809 (0, false, vec![(0, 999)]),
1810 (2, true, vec![(1000, 1999), (2000, 2999)]),
1811 (4, true, vec![(1999, 3999), (3000, 3999)]),
1812 ],
1813 );
1814
1815 check_assign_to_windows_with_overlapping(
1816 &[
1817 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (2999, 3999), ],
1823 2,
1824 &[
1825 (0, false, vec![(0, 999)]),
1827 (2, true, vec![(1000, 1999), (2000, 2999)]),
1828 (4, true, vec![(2999, 3999), (3000, 3999)]),
1829 ],
1830 );
1831
1832 check_assign_to_windows_with_overlapping(
1833 &[
1834 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (0, 1000), ],
1840 2,
1841 &[
1842 (0, true, vec![(0, 999)]),
1844 (2, true, vec![(0, 1000), (1000, 1999), (2000, 2999)]),
1845 (4, false, vec![(3000, 3999)]),
1846 ],
1847 );
1848 }
1849
1850 #[test]
1851 fn test_assign_to_windows_not_overlapping_when_pk_disjoint() {
1852 let files = [
1853 new_file_handle_with_size_sequence_and_primary_key_range(
1854 FileId::random(),
1855 0,
1856 1000,
1857 0,
1858 1,
1859 10,
1860 pk_range(b"a", b"f"),
1861 ),
1862 new_file_handle_with_size_sequence_and_primary_key_range(
1863 FileId::random(),
1864 500,
1865 1999,
1866 0,
1867 2,
1868 10,
1869 pk_range(b"x", b"z"),
1870 ),
1871 ];
1872
1873 let windows = assign_to_windows(files.iter(), 2);
1874
1875 let overlapping = window_has_overlap(windows.get(&2).unwrap(), &windows);
1876 assert!(!overlapping);
1877 }
1878
1879 #[test]
1880 fn test_assign_to_windows_pk_unknown_in_earlier_window_does_not_poison_later_windows() {
1881 let files = [
1882 new_file_handle(FileId::random(), 0, 1999, 0),
1883 new_file_handle_with_size_sequence_and_primary_key_range(
1884 FileId::random(),
1885 2000,
1886 3999,
1887 0,
1888 1,
1889 10,
1890 pk_range(b"a", b"f"),
1891 ),
1892 new_file_handle_with_size_sequence_and_primary_key_range(
1893 FileId::random(),
1894 3000,
1895 4999,
1896 0,
1897 2,
1898 10,
1899 pk_range(b"x", b"z"),
1900 ),
1901 ];
1902
1903 let windows = assign_to_windows(files.iter(), 2);
1904
1905 let overlapping = window_has_overlap(windows.get(&4).unwrap(), &windows);
1906 assert!(!overlapping);
1907 }
1908
1909 struct CompactionPickerTestCase {
1910 window_size: i64,
1911 input_files: Vec<FileHandle>,
1912 expected_outputs: Vec<ExpectedOutput>,
1913 }
1914
1915 impl CompactionPickerTestCase {
1916 async fn check(&self) {
1917 let file_id_to_idx = self
1918 .input_files
1919 .iter()
1920 .enumerate()
1921 .map(|(idx, file)| (file.file_id(), idx))
1922 .collect::<HashMap<_, _>>();
1923 let windows = assign_to_windows(self.input_files.iter(), self.window_size);
1924 let active_window =
1925 find_active_window_by_sequence(self.input_files.iter(), self.window_size).or_else(
1926 || find_latest_window_in_seconds(self.input_files.iter(), self.window_size),
1927 );
1928 let output = TwcsPicker {
1929 trigger_file_num: 2,
1930 active_window_l1_merge_trigger: 8,
1931 inactive_window_trigger_file_num: 2,
1932 inactive_window_l1_merge_trigger: 2,
1933 time_window_seconds: None,
1934 max_output_file_size: None,
1935 append_mode: false,
1936 max_background_tasks: None,
1937 time_range: None,
1938 }
1939 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
1940 .await
1941 .unwrap();
1942
1943 let output = output
1944 .iter()
1945 .map(|o| {
1946 let input_file_ids = o
1947 .inputs
1948 .iter()
1949 .map(|f| file_id_to_idx.get(&f.file_id()).copied().unwrap())
1950 .collect::<HashSet<_>>();
1951 (input_file_ids, o.output_level)
1952 })
1953 .collect::<Vec<_>>();
1954
1955 let expected = self
1956 .expected_outputs
1957 .iter()
1958 .map(|o| {
1959 let input_file_ids = o.input_files.iter().copied().collect::<HashSet<_>>();
1960 (input_file_ids, o.output_level)
1961 })
1962 .collect::<Vec<_>>();
1963 assert_eq!(expected, output);
1964 }
1965 }
1966
1967 struct ExpectedOutput {
1968 input_files: Vec<usize>,
1969 output_level: Level,
1970 }
1971
1972 #[tokio::test]
1973 async fn test_newer_windows_are_placed_last_for_execution() {
1974 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
1975
1976 CompactionPickerTestCase {
1978 window_size: 3,
1979 input_files: [
1980 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
1981 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
1982 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3), new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4), ]
1985 .to_vec(),
1986 expected_outputs: vec![
1987 ExpectedOutput {
1988 input_files: vec![0, 1],
1989 output_level: 1,
1990 },
1991 ExpectedOutput {
1992 input_files: vec![2, 3],
1993 output_level: 1,
1994 },
1995 ],
1996 }
1997 .check()
1998 .await;
1999
2000 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2007 CompactionPickerTestCase {
2008 window_size: 3,
2009 input_files: [
2010 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2011 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2012 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3),
2013 new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4),
2014 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 5),
2015 ]
2016 .to_vec(),
2017 expected_outputs: vec![
2018 ExpectedOutput {
2019 input_files: vec![0, 1],
2020 output_level: 1,
2021 },
2022 ExpectedOutput {
2023 input_files: vec![2, 3, 4],
2024 output_level: 1,
2025 },
2026 ],
2027 }
2028 .check()
2029 .await;
2030
2031 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2036 CompactionPickerTestCase {
2037 window_size: 3,
2038 input_files: [
2039 new_file_handle_with_sequence(file_ids[0], 0, 2999, 1, 1),
2040 new_file_handle_with_sequence(file_ids[1], 0, 2998, 1, 1),
2041 new_file_handle_with_sequence(file_ids[2], 3000, 5999, 1, 2),
2042 new_file_handle_with_sequence(file_ids[3], 3000, 5000, 1, 2),
2043 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 3),
2044 ]
2045 .to_vec(),
2046 expected_outputs: vec![ExpectedOutput {
2047 input_files: vec![2, 3],
2048 output_level: 1,
2049 }],
2050 }
2051 .check()
2052 .await;
2053 }
2054
2055 #[tokio::test]
2056 async fn test_build_output_skips_pk_disjoint_files() {
2057 let files = [
2058 new_file_handle_with_size_sequence_and_primary_key_range(
2059 FileId::random(),
2060 0,
2061 2999,
2062 0,
2063 1,
2064 10,
2065 pk_range(b"a", b"f"),
2066 ),
2067 new_file_handle_with_size_sequence_and_primary_key_range(
2068 FileId::random(),
2069 50,
2070 2998,
2071 0,
2072 2,
2073 10,
2074 pk_range(b"x", b"z"),
2075 ),
2076 ];
2077 let windows = assign_to_windows(files.iter(), 3);
2078 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2079 let output = TwcsPicker {
2080 trigger_file_num: 4,
2081 active_window_l1_merge_trigger: 8,
2082 inactive_window_trigger_file_num: 4,
2083 inactive_window_l1_merge_trigger: 8,
2084 time_window_seconds: None,
2085 max_output_file_size: None,
2086 append_mode: false,
2087 max_background_tasks: None,
2088 time_range: None,
2089 }
2090 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2091 .await
2092 .unwrap();
2093
2094 assert!(output.is_empty());
2095 }
2096
2097 #[test]
2098 fn test_append_mode_filter_large_files() {
2099 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2100 let max_output_file_size = 1000u64;
2101
2102 let small_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 500);
2104 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 1500);
2105 let small_file_2 = new_file_handle_with_size_and_sequence(file_ids[2], 0, 999, 0, 3, 800);
2106 let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[3], 0, 999, 0, 4, 2000);
2107
2108 let mut files_to_merge = vec![small_file_1, large_file_1, small_file_2, large_file_2];
2109
2110 let original_count = files_to_merge.len();
2112
2113 files_to_merge.retain(|file| file.size() <= max_output_file_size);
2115
2116 assert_eq!(files_to_merge.len(), 2);
2118 assert_eq!(original_count, 4);
2119
2120 for file in &files_to_merge {
2122 assert!(
2123 file.size() <= max_output_file_size,
2124 "File size {} should be <= {}",
2125 file.size(),
2126 max_output_file_size
2127 );
2128 }
2129 }
2130
2131 #[tokio::test]
2132 async fn test_build_output_multiple_windows_with_zero_runs() {
2133 let file_ids = (0..7).map(|_| FileId::random()).collect::<Vec<_>>();
2134
2135 let files = [
2136 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2138 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2139 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2140 new_file_handle_with_sequence(file_ids[3], 3000, 3999, 0, 4),
2142 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2143 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2144 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2145 ];
2146
2147 let windows = assign_to_windows(files.iter(), 3);
2148
2149 let picker = TwcsPicker {
2151 trigger_file_num: 4, active_window_l1_merge_trigger: 8,
2153 inactive_window_trigger_file_num: 4,
2154 inactive_window_l1_merge_trigger: 8,
2155 time_window_seconds: Some(3),
2156 max_output_file_size: None,
2157 append_mode: false,
2158 max_background_tasks: None,
2159 time_range: None,
2160 };
2161
2162 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2163 let output = picker
2164 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2165 .await
2166 .unwrap();
2167
2168 assert!(
2169 !output.is_empty(),
2170 "Should have output from windows with runs, even when one window has 0 runs"
2171 );
2172
2173 let all_output_files: Vec<_> = output
2174 .iter()
2175 .flat_map(|o| o.inputs.iter())
2176 .map(|f| f.file_id().file_id())
2177 .collect();
2178
2179 assert!(
2180 all_output_files.contains(&file_ids[3])
2181 || all_output_files.contains(&file_ids[4])
2182 || all_output_files.contains(&file_ids[5]),
2183 "Output should contain files from the window with runs"
2184 );
2185 }
2186
2187 #[tokio::test]
2188 async fn test_build_output_single_window_zero_runs() {
2189 let file_ids = (0..2).map(|_| FileId::random()).collect::<Vec<_>>();
2190
2191 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 2000); let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 2500); let files = [large_file_1, large_file_2];
2195
2196 let windows = assign_to_windows(files.iter(), 3);
2197
2198 let picker = TwcsPicker {
2199 trigger_file_num: 2,
2200 active_window_l1_merge_trigger: 8,
2201 inactive_window_trigger_file_num: 2,
2202 inactive_window_l1_merge_trigger: 8,
2203 time_window_seconds: Some(3),
2204 max_output_file_size: Some(1000),
2205 append_mode: true,
2206 max_background_tasks: None,
2207 time_range: None,
2208 };
2209
2210 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2211 let output = picker
2212 .build_output_with_time_range(RegionId::from_u64(456), windows, active_window, None)
2213 .await
2214 .unwrap();
2215
2216 assert!(
2218 output.is_empty(),
2219 "Should return empty output when no runs are found after filtering"
2220 );
2221 }
2222
2223 #[tokio::test]
2224 async fn test_append_mode_can_pick_remaining_single_level_files() {
2225 let files = [
2226 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100),
2227 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 100),
2228 new_file_handle_with_size_and_sequence(FileId::random(), 40, 49, 0, 3, 100),
2229 new_file_handle_with_size_and_sequence(FileId::random(), 60, 69, 0, 4, 2_000),
2230 ];
2231 let windows = assign_to_windows(files.iter(), 1);
2232 let picker = TwcsPicker {
2233 trigger_file_num: 4,
2234 active_window_l1_merge_trigger: 8,
2235 inactive_window_trigger_file_num: 4,
2236 inactive_window_l1_merge_trigger: 8,
2237 time_window_seconds: Some(1),
2238 max_output_file_size: Some(1_000),
2239 append_mode: true,
2240 max_background_tasks: None,
2241 time_range: None,
2242 };
2243
2244 let output = picker
2245 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
2246 .await
2247 .unwrap();
2248
2249 assert_eq!(1, output.len());
2250 assert_eq!(3, output[0].inputs.len());
2251 }
2252
2253 #[tokio::test]
2254 async fn test_max_background_tasks_truncation() {
2255 let file_ids = (0..10).map(|_| FileId::random()).collect::<Vec<_>>();
2256 let max_background_tasks = 3;
2257
2258 let files = [
2260 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2262 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2263 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2264 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2265 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2267 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2268 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2269 new_file_handle_with_sequence(file_ids[7], 3000, 3999, 0, 8),
2270 new_file_handle_with_sequence(file_ids[8], 6000, 6999, 0, 9),
2272 new_file_handle_with_sequence(file_ids[9], 6000, 6999, 0, 10),
2273 ];
2274
2275 let windows = assign_to_windows(files.iter(), 3);
2276
2277 let picker = TwcsPicker {
2278 trigger_file_num: 4,
2279 active_window_l1_merge_trigger: 8,
2280 inactive_window_trigger_file_num: 4,
2281 inactive_window_l1_merge_trigger: 8,
2282 time_window_seconds: Some(3),
2283 max_output_file_size: None,
2284 append_mode: false,
2285 max_background_tasks: Some(max_background_tasks),
2286 time_range: None,
2287 };
2288
2289 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2290 let output = picker
2291 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2292 .await
2293 .unwrap();
2294
2295 assert!(
2297 output.len() <= max_background_tasks,
2298 "Output should be truncated to max_background_tasks: expected <= {}, got {}",
2299 max_background_tasks,
2300 output.len()
2301 );
2302
2303 let picker_no_limit = TwcsPicker {
2305 trigger_file_num: 4,
2306 active_window_l1_merge_trigger: 8,
2307 inactive_window_trigger_file_num: 4,
2308 inactive_window_l1_merge_trigger: 8,
2309 time_window_seconds: Some(3),
2310 max_output_file_size: None,
2311 append_mode: false,
2312 max_background_tasks: None,
2313 time_range: None,
2314 };
2315
2316 let windows_no_limit = assign_to_windows(files.iter(), 3);
2317 let output_no_limit = picker_no_limit
2318 .build_output_with_time_range(
2319 RegionId::from_u64(123),
2320 windows_no_limit,
2321 active_window,
2322 None,
2323 )
2324 .await
2325 .unwrap();
2326
2327 if output_no_limit.len() > max_background_tasks {
2329 assert!(
2330 output_no_limit.len() > output.len(),
2331 "Without limit should have more outputs than with limit"
2332 );
2333 }
2334 }
2335
2336 #[tokio::test]
2337 async fn test_max_background_tasks_no_truncation_when_under_limit() {
2338 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2339 let max_background_tasks = 10; let files = [
2343 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2344 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2345 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2346 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2347 ];
2348
2349 let windows = assign_to_windows(files.iter(), 3);
2350
2351 let picker = TwcsPicker {
2352 trigger_file_num: 4,
2353 active_window_l1_merge_trigger: 8,
2354 inactive_window_trigger_file_num: 4,
2355 inactive_window_l1_merge_trigger: 8,
2356 time_window_seconds: Some(3),
2357 max_output_file_size: None,
2358 append_mode: false,
2359 max_background_tasks: Some(max_background_tasks),
2360 time_range: None,
2361 };
2362
2363 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2364 let output = picker
2365 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2366 .await
2367 .unwrap();
2368
2369 assert!(
2371 output.len() <= max_background_tasks,
2372 "Output should be within limit"
2373 );
2374 assert!(!output.is_empty(), "Should have at least one output");
2376 }
2377
2378 #[tokio::test]
2379 async fn test_pick_multiple_runs() {
2380 common_telemetry::init_default_ut_logging();
2381
2382 let num_files = 8;
2383 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2384
2385 let files: Vec<_> = file_ids
2387 .iter()
2388 .enumerate()
2389 .map(|(idx, file_id)| {
2390 new_file_handle_with_size_and_sequence(
2391 *file_id,
2392 0,
2393 999,
2394 0,
2395 (idx + 1) as u64,
2396 1024 * 1024,
2397 )
2398 })
2399 .collect();
2400
2401 let windows = assign_to_windows(files.iter(), 3);
2402
2403 let picker = TwcsPicker {
2404 trigger_file_num: 4,
2405 active_window_l1_merge_trigger: 8,
2406 inactive_window_trigger_file_num: 4,
2407 inactive_window_l1_merge_trigger: 8,
2408 time_window_seconds: Some(3),
2409 max_output_file_size: None,
2410 append_mode: false,
2411 max_background_tasks: None,
2412 time_range: None,
2413 };
2414
2415 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2416 let output = picker
2417 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2418 .await
2419 .unwrap();
2420
2421 assert_eq!(1, output.len());
2422 assert_eq!(output[0].inputs.len(), num_files);
2423 }
2424
2425 #[tokio::test]
2426 async fn test_window_trigger_can_exceed_input_limit() {
2427 common_telemetry::init_default_ut_logging();
2428
2429 let num_files = 50;
2430 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2431
2432 let files: Vec<_> = file_ids
2434 .iter()
2435 .enumerate()
2436 .map(|(idx, file_id)| {
2437 new_file_handle_with_size_and_sequence(
2438 *file_id,
2439 (idx / 2 * 10) as i64,
2440 (idx / 2 * 10 + 5) as i64,
2441 0,
2442 (idx + 1) as u64,
2443 1024 * 1024,
2444 )
2445 })
2446 .collect();
2447
2448 let windows = assign_to_windows(files.iter(), 3);
2449
2450 let picker = TwcsPicker {
2451 trigger_file_num: num_files,
2452 active_window_l1_merge_trigger: 8,
2453 inactive_window_trigger_file_num: num_files,
2454 inactive_window_l1_merge_trigger: 8,
2455 time_window_seconds: Some(3),
2456 max_output_file_size: None,
2457 append_mode: false,
2458 max_background_tasks: None,
2459 time_range: None,
2460 };
2461
2462 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2463 let output = picker
2464 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2465 .await
2466 .unwrap();
2467
2468 assert_eq!(1, output.len());
2469 assert_eq!(output[0].inputs.len(), num_files.min(*MAX_INPUT_FILES));
2470 }
2471
2472 #[tokio::test]
2473 async fn test_limit_max_input_files_keeps_deletion_markers() {
2474 common_telemetry::init_default_ut_logging();
2475
2476 let mut files = vec![new_file_handle_with_size_and_sequence(
2479 FileId::random(),
2480 0,
2481 3_000_000,
2482 0,
2483 1,
2484 1024 * 1024 * 1024,
2485 )];
2486 files.extend((0..DEFAULT_MAX_INPUT_FILES as i64).map(|idx| {
2487 new_file_handle_with_size_and_sequence(
2488 FileId::random(),
2489 (idx + 1) * 10_000,
2490 (idx + 1) * 10_000 + 1_000,
2491 0,
2492 (idx + 2) as u64,
2493 1024,
2494 )
2495 }));
2496
2497 let windows = assign_to_windows(files.iter(), 3600);
2498
2499 let picker = TwcsPicker {
2500 trigger_file_num: 4,
2501 active_window_l1_merge_trigger: 8,
2502 inactive_window_trigger_file_num: 4,
2503 inactive_window_l1_merge_trigger: 8,
2504 time_window_seconds: Some(3600),
2505 max_output_file_size: None,
2506 append_mode: false,
2507 max_background_tasks: None,
2508 time_range: None,
2509 };
2510
2511 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2512 let output = picker
2513 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2514 .await
2515 .unwrap();
2516
2517 assert_eq!(1, output.len());
2518 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2521 assert!(
2522 !output[0].filter_deleted,
2523 "deletion markers must be kept once the file num limit drops files they may mask"
2524 );
2525 }
2526
2527 #[tokio::test]
2528 async fn test_limit_max_input_files_still_filters_without_overlap() {
2529 common_telemetry::init_default_ut_logging();
2530
2531 let files: Vec<_> = (0..DEFAULT_MAX_INPUT_FILES as i64 + 8)
2534 .map(|idx| {
2535 new_file_handle_with_size_and_sequence(
2536 FileId::random(),
2537 (idx + 1) * 10_000,
2538 (idx + 1) * 10_000 + 1_000,
2539 0,
2540 (idx + 1) as u64,
2541 1024,
2542 )
2543 })
2544 .collect();
2545
2546 let windows = assign_to_windows(files.iter(), 3600);
2547
2548 let picker = TwcsPicker {
2549 trigger_file_num: 4,
2550 active_window_l1_merge_trigger: 8,
2551 inactive_window_trigger_file_num: 4,
2552 inactive_window_l1_merge_trigger: 8,
2553 time_window_seconds: Some(3600),
2554 max_output_file_size: Some(1024 * 1024 * 1024),
2555 append_mode: false,
2556 max_background_tasks: None,
2557 time_range: None,
2558 };
2559
2560 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2561 let output = picker
2562 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2563 .await
2564 .unwrap();
2565
2566 assert_eq!(1, output.len());
2567 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2568 assert!(output[0].filter_deleted);
2569 }
2570
2571 #[tokio::test]
2572 async fn test_newer_windows_have_priority() {
2573 let older_file_ids = [FileId::random(), FileId::random()];
2574 let newer_file_ids = [FileId::random(), FileId::random()];
2575 let files = [
2576 new_file_handle_with_sequence(older_file_ids[0], 1_000, 1_999, 0, 1),
2577 new_file_handle_with_sequence(older_file_ids[1], 1_000, 1_999, 0, 2),
2578 new_file_handle_with_sequence(newer_file_ids[0], 7_000, 7_999, 0, 3),
2579 new_file_handle_with_sequence(newer_file_ids[1], 7_000, 7_999, 0, 4),
2580 ];
2581 let windows = assign_to_windows(files.iter(), 3);
2582 let picker = TwcsPicker {
2583 trigger_file_num: 2,
2584 active_window_l1_merge_trigger: 8,
2585 inactive_window_trigger_file_num: 2,
2586 inactive_window_l1_merge_trigger: 8,
2587 time_window_seconds: Some(3),
2588 max_output_file_size: None,
2589 append_mode: false,
2590 max_background_tasks: Some(1),
2591 time_range: None,
2592 };
2593
2594 let output = picker
2595 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(9), None)
2596 .await
2597 .unwrap();
2598
2599 assert_eq!(1, output.len());
2600 assert_eq!(
2601 newer_file_ids.into_iter().collect::<HashSet<_>>(),
2602 output[0]
2603 .inputs
2604 .iter()
2605 .map(|file| file.file_id().file_id())
2606 .collect::<HashSet<_>>()
2607 );
2608 }
2609
2610 #[test]
2611 fn test_filter_time_windows_by_time_range() {
2612 let time_range = TimestampRange::new(
2613 Timestamp::new_millisecond(1_200),
2614 Timestamp::new_millisecond(1_800),
2615 )
2616 .unwrap();
2617
2618 assert!(time_window_intersects_range(3, 3, &time_range));
2619 assert!(!time_window_intersects_range(9, 3, &time_range));
2620
2621 let boundary_range =
2622 TimestampRange::new(Timestamp::new_second(0), Timestamp::new_second(3)).unwrap();
2623 assert!(time_window_intersects_range(0, 3, &boundary_range));
2624 assert!(time_window_intersects_range(3, 3, &boundary_range));
2625 assert!(!time_window_intersects_range(6, 3, &boundary_range));
2626
2627 let overflowing_range = TimestampRange::new(
2628 Timestamp::new_second(i64::MAX - 1),
2629 Timestamp::new_second(i64::MAX),
2630 )
2631 .unwrap();
2632 assert!(!time_window_intersects_range(0, 4, &overflowing_range));
2633 }
2634
2635 #[tokio::test]
2636 async fn test_time_range_filter_precedes_background_task_limit() {
2637 let early_file_ids = [FileId::random(), FileId::random()];
2638 let selected_file_ids = [FileId::random(), FileId::random()];
2639 let files = [
2640 new_file_handle_with_sequence(early_file_ids[0], 1_000, 1_999, 0, 1),
2641 new_file_handle_with_sequence(early_file_ids[1], 1_000, 1_999, 0, 2),
2642 new_file_handle_with_sequence(selected_file_ids[0], 7_000, 7_999, 0, 3),
2643 new_file_handle_with_sequence(selected_file_ids[1], 7_000, 7_999, 0, 4),
2644 ];
2645 let windows = assign_to_windows(files.iter(), 3);
2646 let picker = TwcsPicker {
2647 trigger_file_num: 2,
2648 active_window_l1_merge_trigger: 8,
2649 inactive_window_trigger_file_num: 2,
2650 inactive_window_l1_merge_trigger: 8,
2651 time_window_seconds: Some(3),
2652 max_output_file_size: None,
2653 append_mode: false,
2654 max_background_tasks: Some(1),
2655 time_range: TimestampRange::new(
2656 Timestamp::new_millisecond(7_200),
2657 Timestamp::new_millisecond(7_800),
2658 ),
2659 };
2660
2661 let output = picker
2662 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(9), Some(3))
2663 .await
2664 .unwrap();
2665
2666 assert_eq!(1, output.len());
2667 assert_eq!(
2668 selected_file_ids.into_iter().collect::<HashSet<_>>(),
2669 output[0]
2670 .inputs
2671 .iter()
2672 .map(|file| file.file_id().file_id())
2673 .collect::<HashSet<_>>()
2674 );
2675 }
2676
2677 #[tokio::test]
2678 async fn test_inactive_window_uses_its_trigger_file_num() {
2679 let files = [
2680 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2681 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2682 ];
2683 let windows = assign_to_windows(files.iter(), 100);
2684 let picker = TwcsPicker {
2685 trigger_file_num: 4,
2686 active_window_l1_merge_trigger: 8,
2687 inactive_window_trigger_file_num: 2,
2688 inactive_window_l1_merge_trigger: 8,
2689 time_window_seconds: Some(100),
2690 max_output_file_size: None,
2691 append_mode: false,
2692 max_background_tasks: None,
2693 time_range: None,
2694 };
2695
2696 let output = picker
2697 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
2698 .await
2699 .unwrap();
2700
2701 assert_eq!(1, output.len());
2702 assert_eq!(2, output[0].inputs.len());
2703 }
2704
2705 #[test]
2706 fn test_inactive_window_l1_trigger_is_independent_of_l0_trigger() {
2707 let l0_files = (0..6)
2708 .map(|idx| {
2709 new_file_handle_with_size_and_sequence(
2710 FileId::random(),
2711 idx * 20,
2712 idx * 20 + 10,
2713 0,
2714 idx as u64 + 1,
2715 10,
2716 )
2717 })
2718 .collect();
2719 let l1_files = (0..2)
2720 .map(|idx| {
2721 new_file_handle_with_size_and_sequence(
2722 FileId::random(),
2723 idx * 20,
2724 idx * 20 + 10,
2725 1,
2726 idx as u64 + 10,
2727 10,
2728 )
2729 })
2730 .collect();
2731
2732 let (inputs, _) = pick_inactive_window_files(
2733 l0_files,
2734 l1_files,
2735 InactiveWindowPick {
2736 l0_file_num: 8,
2737 l1_file_num: 2,
2738 phase: PickPhase::L1FileReduction,
2739 primary_key_mapper: &primary_key_mapper_for_test(),
2740 },
2741 None,
2742 );
2743
2744 assert_eq!(2, inputs.len());
2745 assert!(inputs.iter().all(|file| file.level() == 1));
2746 }
2747
2748 #[test]
2749 fn test_inactive_window_does_not_fallback_to_l1_below_trigger() {
2750 let l1_files = (0..2)
2751 .map(|idx| {
2752 new_file_handle_with_size_and_sequence(
2753 FileId::random(),
2754 idx * 20,
2755 idx * 20 + 10,
2756 1,
2757 idx as u64 + 1,
2758 10,
2759 )
2760 })
2761 .collect();
2762
2763 let (inputs, _) = pick_inactive_window_files(
2764 vec![],
2765 l1_files,
2766 InactiveWindowPick {
2767 l0_file_num: 2,
2768 l1_file_num: 8,
2769 phase: PickPhase::L1FileReduction,
2770 primary_key_mapper: &primary_key_mapper_for_test(),
2771 },
2772 None,
2773 );
2774
2775 assert!(inputs.is_empty());
2776 }
2777
2778 #[tokio::test]
2779 async fn test_inactive_window_requires_a_level_trigger_before_mixed_fallback() {
2780 let files = [
2781 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2782 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 1, 2, 10),
2783 ];
2784 let windows = assign_to_windows(files.iter(), 100);
2785 let picker = TwcsPicker {
2786 trigger_file_num: 4,
2787 active_window_l1_merge_trigger: 8,
2788 inactive_window_trigger_file_num: 8,
2789 inactive_window_l1_merge_trigger: 2,
2790 time_window_seconds: Some(100),
2791 max_output_file_size: Some(1_000),
2792 append_mode: false,
2793 max_background_tasks: None,
2794 time_range: None,
2795 };
2796
2797 let output = picker
2798 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
2799 .await
2800 .unwrap();
2801
2802 assert!(output.is_empty());
2803 }
2804
2805 #[tokio::test]
2806 async fn test_count_first_prefers_more_files_over_smaller_overlap() {
2807 let files = [
2808 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2809 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2810 new_file_handle_with_size_and_sequence(FileId::random(), 40, 50, 0, 3, 10),
2811 new_file_handle_with_size_and_sequence(FileId::random(), 5, 15, 0, 4, 10),
2812 ];
2813 let windows = assign_to_windows(files.iter(), 100);
2814 let picker = TwcsPicker {
2815 trigger_file_num: 2,
2816 active_window_l1_merge_trigger: 8,
2817 inactive_window_trigger_file_num: 2,
2818 inactive_window_l1_merge_trigger: 8,
2819 time_window_seconds: Some(100),
2820 max_output_file_size: None,
2821 append_mode: false,
2822 max_background_tasks: None,
2823 time_range: None,
2824 };
2825
2826 let output = picker
2827 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2828 .await
2829 .unwrap();
2830
2831 assert_eq!(1, output.len());
2832 assert_eq!(4, output[0].inputs.len());
2833 }
2834
2835 #[tokio::test]
2836 async fn test_count_first_trigger_counts_physical_ssts() {
2837 let files = [
2838 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2839 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2840 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2841 ];
2842 let windows = assign_to_windows(files.iter(), 100);
2843 let picker = TwcsPicker {
2844 trigger_file_num: 3,
2845 active_window_l1_merge_trigger: 8,
2846 inactive_window_trigger_file_num: 3,
2847 inactive_window_l1_merge_trigger: 8,
2848 time_window_seconds: Some(100),
2849 max_output_file_size: None,
2850 append_mode: false,
2851 max_background_tasks: None,
2852 time_range: None,
2853 };
2854
2855 let output = picker
2856 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2857 .await
2858 .unwrap();
2859
2860 assert_eq!(1, output.len());
2861 assert_eq!(3, output[0].inputs.len());
2862 }
2863
2864 #[tokio::test]
2865 async fn test_count_first_does_not_compact_overlap_below_trigger() {
2866 let files = [
2867 new_file_handle_with_size_and_sequence(FileId::random(), 0, 20, 0, 1, 10),
2868 new_file_handle_with_size_and_sequence(FileId::random(), 10, 30, 0, 2, 10),
2869 ];
2870 let windows = assign_to_windows(files.iter(), 100);
2871 let picker = TwcsPicker {
2872 trigger_file_num: 3,
2873 active_window_l1_merge_trigger: 8,
2874 inactive_window_trigger_file_num: 3,
2875 inactive_window_l1_merge_trigger: 8,
2876 time_window_seconds: Some(100),
2877 max_output_file_size: None,
2878 append_mode: false,
2879 max_background_tasks: None,
2880 time_range: None,
2881 };
2882
2883 let output = picker
2884 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2885 .await
2886 .unwrap();
2887
2888 assert!(output.is_empty());
2889 }
2890
2891 #[tokio::test]
2892 async fn test_filter_deleted_is_false_when_selected_files_overlap_unselected_file() {
2893 let mut files = (0..32)
2894 .map(|idx| {
2895 new_file_handle_with_size_and_sequence(
2896 FileId::random(),
2897 idx * 10,
2898 idx * 10 + 9,
2899 0,
2900 idx as u64 + 1,
2901 10,
2902 )
2903 })
2904 .collect::<Vec<_>>();
2905 files.push(new_file_handle_with_size_and_sequence(
2906 FileId::random(),
2907 0,
2908 320,
2909 0,
2910 33,
2911 10,
2912 ));
2913 let windows = assign_to_windows(files.iter(), 1000);
2914 let picker = TwcsPicker {
2915 trigger_file_num: 2,
2916 active_window_l1_merge_trigger: 8,
2917 inactive_window_trigger_file_num: 2,
2918 inactive_window_l1_merge_trigger: 8,
2919 time_window_seconds: Some(1000),
2920 max_output_file_size: None,
2921 append_mode: false,
2922 max_background_tasks: None,
2923 time_range: None,
2924 };
2925
2926 let output = picker
2927 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2928 .await
2929 .unwrap();
2930
2931 assert_eq!(1, output.len());
2932 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2933 assert!(!output[0].filter_deleted);
2934 }
2935
2936 fn new_file(start: i64, end: i64, sequence: u64, file_size: u64) -> FileHandle {
2937 new_file_handle_with_size_and_sequence(FileId::random(), start, end, 0, sequence, file_size)
2938 }
2939
2940 fn new_file_with_level_and_rows(
2941 start: i64,
2942 end: i64,
2943 level: Level,
2944 sequence: u64,
2945 file_size: u64,
2946 num_rows: u64,
2947 ) -> FileHandle {
2948 let file = new_file_handle_with_size_and_sequence(
2949 FileId::random(),
2950 start,
2951 end,
2952 level,
2953 sequence,
2954 file_size,
2955 );
2956 let mut meta = file.meta_ref().clone();
2957 meta.num_rows = num_rows;
2958 FileHandle::new(meta, crate::test_util::new_noop_file_purger())
2959 }
2960
2961 fn picked_ranges(files: &[FileHandle]) -> Vec<(i64, i64)> {
2962 files
2963 .iter()
2964 .map(|file| {
2965 let (start, end) = file.range();
2966 (start.value(), end.value())
2967 })
2968 .collect()
2969 }
2970
2971 #[test]
2972 fn test_count_first_rejects_dominant_historical_file() {
2973 let picked = pick_count_first(
2974 vec![SortedRun::from(vec![
2975 new_file(0, 9, 1, 400),
2976 new_file(20, 29, 2, 100),
2977 ])],
2978 None,
2979 );
2980
2981 assert!(picked.is_empty());
2982 }
2983
2984 #[test]
2985 fn test_count_first_accepts_balanced_historical_file() {
2986 let picked = pick_count_first(
2987 vec![SortedRun::from(vec![
2988 new_file(0, 9, 1, 400),
2989 new_file(20, 29, 2, 100),
2990 new_file(40, 49, 3, 100),
2991 new_file(60, 69, 4, 100),
2992 new_file(80, 89, 5, 100),
2993 ])],
2994 None,
2995 );
2996
2997 assert_eq!(
2998 vec![(0, 9), (20, 29), (40, 49), (60, 69), (80, 89)],
2999 picked_ranges(&picked)
3000 );
3001 }
3002
3003 #[test]
3004 fn test_count_first_finds_smaller_balanced_interval_when_larger_one_is_unbalanced() {
3005 let picked = pick_count_first(
3006 vec![SortedRun::from(vec![
3007 new_file(0, 9, 1, 1000),
3008 new_file(20, 29, 2, 100),
3009 new_file(40, 49, 3, 100),
3010 new_file(60, 69, 4, 100),
3011 new_file(80, 89, 5, 100),
3012 ])],
3013 None,
3014 );
3015
3016 assert_eq!(
3017 vec![(20, 29), (40, 49), (60, 69), (80, 89)],
3018 picked_ranges(&picked)
3019 );
3020 }
3021
3022 #[test]
3023 fn test_count_first_prefers_overlap_participants_when_file_counts_match() {
3024 let first_run = (0..DEFAULT_MAX_INPUT_FILES)
3025 .map(|idx| {
3026 let start = idx as i64 * 20;
3027 let end = if idx + 1 == DEFAULT_MAX_INPUT_FILES {
3028 700
3029 } else {
3030 start + 9
3031 };
3032 new_file(start, end, idx as u64 + 1, 10)
3033 })
3034 .collect::<Vec<_>>();
3035 let overlapping = new_file(690, 710, 100, 10);
3036
3037 let picked = pick_count_first(
3038 vec![
3039 SortedRun::from(first_run),
3040 SortedRun::from(vec![overlapping]),
3041 ],
3042 None,
3043 );
3044 let ranges = picked_ranges(&picked);
3045
3046 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3047 assert!(!ranges.contains(&(0, 9)));
3048 assert!(ranges.contains(&(690, 710)));
3049 }
3050
3051 #[tokio::test]
3052 async fn test_picker_avoids_chained_l1_rewrites_by_compacting_levels_separately() {
3053 let mut enough_l0 = (0..DEFAULT_MAX_INPUT_FILES)
3054 .map(|idx| {
3055 let start = idx as i64 * 20;
3056 new_file_handle_with_size_and_sequence(
3057 FileId::random(),
3058 start,
3059 start + 9,
3060 0,
3061 idx as u64 + 1,
3062 10,
3063 )
3064 })
3065 .collect::<Vec<_>>();
3066 enough_l0.push(new_file_handle_with_size_and_sequence(
3067 FileId::random(),
3068 0,
3069 700,
3070 1,
3071 100,
3072 100,
3073 ));
3074 let mut enough_l1 = (0..4)
3075 .map(|idx| {
3076 new_file_handle_with_size_and_sequence(
3077 FileId::random(),
3078 idx * 100,
3079 idx * 100 + 99,
3080 1,
3081 idx as u64 + 1,
3082 100,
3083 )
3084 })
3085 .collect::<Vec<_>>();
3086 enough_l1.extend((0..3).map(|idx| {
3087 new_file_handle_with_size_and_sequence(
3088 FileId::random(),
3089 idx * 20,
3090 idx * 20 + 9,
3091 0,
3092 idx as u64 + 10,
3093 10,
3094 )
3095 }));
3096 let picker = TwcsPicker {
3097 trigger_file_num: 4,
3098 active_window_l1_merge_trigger: 8,
3099 inactive_window_trigger_file_num: 4,
3100 inactive_window_l1_merge_trigger: 4,
3101 time_window_seconds: Some(1),
3102 max_output_file_size: None,
3103 append_mode: false,
3104 max_background_tasks: None,
3105 time_range: None,
3106 };
3107
3108 for (case, files, expected_level, expected_len) in [
3109 ("L0 reaches trigger", enough_l0, 0, DEFAULT_MAX_INPUT_FILES),
3110 ("L0 fallback precedes triggered L1", enough_l1, 0, 3),
3111 ] {
3112 let windows = assign_to_windows(files.iter(), 1);
3113 let output = picker
3114 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3115 .await
3116 .unwrap();
3117
3118 assert_eq!(1, output.len(), "{case}");
3119 assert_eq!(expected_len, output[0].inputs.len(), "{case}");
3120 assert!(
3121 output[0]
3122 .inputs
3123 .iter()
3124 .all(|file| file.level() == expected_level),
3125 "{case}"
3126 );
3127 }
3128 }
3129
3130 #[tokio::test]
3131 async fn test_active_window_defers_l1_below_safety_trigger() {
3132 let files = (0..7)
3133 .map(|idx| {
3134 let start = idx * 100;
3135 new_file_handle_with_size_and_sequence(
3136 FileId::random(),
3137 start,
3138 start + 99,
3139 1,
3140 idx as u64 + 1,
3141 100,
3142 )
3143 })
3144 .collect::<Vec<_>>();
3145 let windows = assign_to_windows(files.iter(), 1);
3146 let active_window = windows.keys().next().copied();
3147 let picker = TwcsPicker {
3148 trigger_file_num: 4,
3149 active_window_l1_merge_trigger: 8,
3150 inactive_window_trigger_file_num: 2,
3151 inactive_window_l1_merge_trigger: 8,
3152 time_window_seconds: Some(1),
3153 max_output_file_size: None,
3154 append_mode: false,
3155 max_background_tasks: None,
3156 time_range: None,
3157 };
3158
3159 let output = picker
3160 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3161 .await
3162 .unwrap();
3163
3164 assert!(output.is_empty());
3165 }
3166
3167 #[tokio::test]
3168 async fn test_active_window_l1_safety_trigger_is_independent_of_l0_trigger() {
3169 let files = (0..8)
3170 .map(|idx| {
3171 let start = idx * 100;
3172 new_file_handle_with_size_and_sequence(
3173 FileId::random(),
3174 start,
3175 start + 99,
3176 1,
3177 idx as u64 + 1,
3178 100,
3179 )
3180 })
3181 .collect::<Vec<_>>();
3182 let windows = assign_to_windows(files.iter(), 1);
3183 let active_window = windows.keys().next().copied();
3184 let picker = TwcsPicker {
3185 trigger_file_num: 16,
3186 active_window_l1_merge_trigger: 8,
3187 inactive_window_trigger_file_num: 2,
3188 inactive_window_l1_merge_trigger: 8,
3189 time_window_seconds: Some(1),
3190 max_output_file_size: None,
3191 append_mode: false,
3192 max_background_tasks: None,
3193 time_range: None,
3194 };
3195
3196 let output = picker
3197 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3198 .await
3199 .unwrap();
3200
3201 assert_eq!(1, output.len());
3202 assert_eq!(8, output[0].inputs.len());
3203 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3204 }
3205
3206 #[tokio::test]
3207 async fn test_picker_falls_back_to_l1_when_triggered_l0_cannot_make_progress() {
3208 let mut files = (0..4)
3209 .map(|idx| {
3210 let start = idx * 20;
3211 new_file_handle_with_size_and_sequence(
3212 FileId::random(),
3213 start,
3214 start + 9,
3215 0,
3216 idx as u64 + 1,
3217 600,
3218 )
3219 })
3220 .collect::<Vec<_>>();
3221 files.extend((0..8).map(|idx| {
3222 let start = idx * 20 + 100;
3223 new_file_handle_with_size_and_sequence(
3224 FileId::random(),
3225 start,
3226 start + 9,
3227 1,
3228 idx as u64 + 10,
3229 50,
3230 )
3231 }));
3232 let windows = assign_to_windows(files.iter(), 1);
3233 let active_window = windows.keys().next().copied();
3234 let picker = TwcsPicker {
3235 trigger_file_num: 4,
3236 active_window_l1_merge_trigger: 8,
3237 inactive_window_trigger_file_num: 4,
3238 inactive_window_l1_merge_trigger: 8,
3239 time_window_seconds: Some(1),
3240 max_output_file_size: Some(512),
3241 append_mode: false,
3242 max_background_tasks: None,
3243 time_range: None,
3244 };
3245
3246 let output = picker
3247 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3248 .await
3249 .unwrap();
3250
3251 assert_eq!(1, output.len());
3252 assert_eq!(8, output[0].inputs.len());
3253 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3254 }
3255
3256 fn priority_test_files(
3257 window: i64,
3258 level: Level,
3259 file_size: u64,
3260 overlap: bool,
3261 ) -> [FileHandle; 2] {
3262 let start = window * 1_000;
3263 let ranges = if overlap {
3264 [(start, start + 499), (start + 250, start + 749)]
3265 } else {
3266 [(start, start + 99), (start + 200, start + 299)]
3267 };
3268 ranges.map(|(start, end)| {
3269 new_file_handle_with_size_and_sequence(
3270 FileId::random(),
3271 start,
3272 end,
3273 level,
3274 window as u64 + 1,
3275 file_size,
3276 )
3277 })
3278 }
3279
3280 fn priority_test_picker(max_background_tasks: usize) -> TwcsPicker {
3281 TwcsPicker {
3282 trigger_file_num: 2,
3283 active_window_l1_merge_trigger: 2,
3284 inactive_window_trigger_file_num: 2,
3285 inactive_window_l1_merge_trigger: 2,
3286 time_window_seconds: Some(1),
3287 max_output_file_size: Some(100),
3288 append_mode: false,
3289 max_background_tasks: Some(max_background_tasks),
3290 time_range: None,
3291 }
3292 }
3293
3294 fn output_window(output: &CompactionOutput) -> i64 {
3295 output.inputs[0]
3296 .time_range()
3297 .1
3298 .convert_to(TimeUnit::Second)
3299 .unwrap()
3300 .value()
3301 }
3302
3303 #[tokio::test]
3304 async fn test_older_l0_candidate_has_priority_over_newer_pure_l1_candidate() {
3305 let files = priority_test_files(0, 0, 40, false)
3306 .into_iter()
3307 .chain(priority_test_files(1, 1, 40, false))
3308 .collect::<Vec<_>>();
3309 let windows = assign_to_windows(files.iter(), 1);
3310
3311 let output = priority_test_picker(1)
3312 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3313 .await
3314 .unwrap();
3315
3316 assert_eq!(1, output.len());
3317 assert_eq!(0, output_window(&output[0]));
3318 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3319 }
3320
3321 #[tokio::test]
3322 async fn test_older_l1_file_reduction_has_priority_over_newer_overlap_only_l1() {
3323 let files = priority_test_files(0, 1, 40, false)
3324 .into_iter()
3325 .chain(priority_test_files(1, 1, 100, true))
3326 .collect::<Vec<_>>();
3327 let windows = assign_to_windows(files.iter(), 1);
3328
3329 let output = priority_test_picker(1)
3330 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3331 .await
3332 .unwrap();
3333
3334 assert_eq!(1, output.len());
3335 assert_eq!(0, output_window(&output[0]));
3336 }
3337
3338 #[tokio::test]
3339 async fn test_multiple_slots_follow_class_priority_and_newer_first_within_class() {
3340 let files = [
3341 priority_test_files(0, 0, 40, false),
3342 priority_test_files(1, 0, 40, false),
3343 priority_test_files(2, 1, 40, false),
3344 priority_test_files(3, 1, 40, false),
3345 priority_test_files(4, 1, 100, true),
3346 priority_test_files(5, 1, 100, true),
3347 ]
3348 .into_iter()
3349 .flatten()
3350 .collect::<Vec<_>>();
3351 let windows = assign_to_windows(files.iter(), 1);
3352
3353 let output = priority_test_picker(5)
3354 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(5), None)
3355 .await
3356 .unwrap();
3357 let pop_priority = output
3358 .iter()
3359 .rev()
3360 .map(|output| (output.inputs[0].level(), output_window(output)))
3361 .collect::<Vec<_>>();
3362
3363 assert_eq!(vec![(0, 1), (0, 0), (1, 3), (1, 2), (1, 5)], pop_priority);
3364 }
3365
3366 #[tokio::test]
3367 async fn test_inactive_window_mixed_fallback_merges_within_rewrite_budget() {
3368 let files = [
3369 new_file_with_level_and_rows(0, 99, 1, 1, 1_000, 1_000_000),
3370 new_file_with_level_and_rows(0, 9, 0, 2, 10, 1),
3371 ];
3372 let windows = assign_to_windows(files.iter(), 100);
3373 let picker = TwcsPicker {
3374 trigger_file_num: 4,
3375 active_window_l1_merge_trigger: 8,
3376 inactive_window_trigger_file_num: 2,
3377 inactive_window_l1_merge_trigger: 8,
3378 time_window_seconds: Some(100),
3379 max_output_file_size: Some(2_000),
3380 append_mode: false,
3381 max_background_tasks: None,
3382 time_range: None,
3383 };
3384
3385 let output = picker
3386 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3387 .await
3388 .unwrap();
3389
3390 assert_eq!(1, output.len());
3391 assert_eq!(2, output[0].inputs.len());
3392 }
3393
3394 #[tokio::test]
3395 async fn test_inactive_window_over_budget_merges_l0_only() {
3396 let files = [
3399 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10_000),
3400 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3401 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 1_000_000),
3402 ];
3403 let windows = assign_to_windows(files.iter(), 100);
3404 let picker = TwcsPicker {
3405 trigger_file_num: 4,
3406 active_window_l1_merge_trigger: 8,
3407 inactive_window_trigger_file_num: 2,
3408 inactive_window_l1_merge_trigger: 8,
3409 time_window_seconds: Some(100),
3410 max_output_file_size: Some(100_000),
3411 append_mode: false,
3412 max_background_tasks: None,
3413 time_range: None,
3414 };
3415
3416 let output = picker
3417 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3418 .await
3419 .unwrap();
3420
3421 assert_eq!(1, output.len());
3422 assert_eq!(2, output[0].inputs.len());
3423 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3424 }
3425
3426 #[tokio::test]
3427 async fn test_inactive_window_over_budget_without_l0_pair_does_not_merge() {
3428 let files = [
3431 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
3432 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 2, 1_000_000),
3433 ];
3434 let windows = assign_to_windows(files.iter(), 100);
3435 let picker = TwcsPicker {
3436 trigger_file_num: 4,
3437 active_window_l1_merge_trigger: 8,
3438 inactive_window_trigger_file_num: 2,
3439 inactive_window_l1_merge_trigger: 8,
3440 time_window_seconds: Some(100),
3441 max_output_file_size: Some(100_000),
3442 append_mode: false,
3443 max_background_tasks: None,
3444 time_range: None,
3445 };
3446
3447 let output = picker
3448 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3449 .await
3450 .unwrap();
3451
3452 assert!(output.is_empty());
3453 }
3454
3455 #[tokio::test]
3456 async fn test_inactive_window_falls_through_when_triggered_l0_pick_fails() {
3457 let files = [
3461 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100_000),
3462 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3463 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 50_000),
3464 ];
3465 let windows = assign_to_windows(files.iter(), 100);
3466 let picker = TwcsPicker {
3467 trigger_file_num: 4,
3468 active_window_l1_merge_trigger: 8,
3469 inactive_window_trigger_file_num: 2,
3470 inactive_window_l1_merge_trigger: 8,
3471 time_window_seconds: Some(100),
3472 max_output_file_size: Some(1_000_000),
3473 append_mode: false,
3474 max_background_tasks: None,
3475 time_range: None,
3476 };
3477
3478 let output = picker
3479 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3480 .await
3481 .unwrap();
3482
3483 assert_eq!(1, output.len());
3484 assert_eq!(3, output[0].inputs.len());
3485 }
3486
3487 #[test]
3488 fn test_mixed_candidate_row_balance() {
3489 for (case, l1_rows, l0_rows, expected_len) in [
3490 ("L1 rows dominate", 1_000_000, 10_000, 0),
3491 ("L1 rows meet ratio", 60_000, 10_000, 4),
3492 ("L0 rows are unknown", 1_000_000, 0, 4),
3493 ] {
3494 let files = vec![
3495 new_file_with_level_and_rows(0, 99, 1, 1, 100, l1_rows),
3496 new_file_with_level_and_rows(0, 9, 0, 2, 100, l0_rows),
3497 new_file_with_level_and_rows(20, 29, 0, 3, 100, l0_rows),
3498 new_file_with_level_and_rows(40, 49, 0, 4, 100, l0_rows),
3499 ];
3500
3501 let picked = pick_mixed_count_first(vec![SortedRun::from(files)], None);
3502
3503 assert_eq!(expected_len, picked.len(), "{case}");
3504 }
3505 }
3506
3507 #[test]
3508 fn test_count_first_prefers_smaller_bytes_when_file_counts_match() {
3509 let files = (0..=DEFAULT_MAX_INPUT_FILES)
3510 .map(|idx| {
3511 let start = idx as i64 * 20;
3512 let size = if idx == 0 {
3513 100
3514 } else if idx == DEFAULT_MAX_INPUT_FILES {
3515 1
3516 } else {
3517 10
3518 };
3519 new_file(start, start + 9, idx as u64 + 1, size)
3520 })
3521 .collect::<Vec<_>>();
3522
3523 let picked = pick_count_first(vec![SortedRun::from(files)], None);
3524 let ranges = picked_ranges(&picked);
3525
3526 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3527 assert_eq!(Some(&(20, 29)), ranges.first());
3528 let last_start = DEFAULT_MAX_INPUT_FILES as i64 * 20;
3529 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
3530 }
3531
3532 #[test]
3533 fn test_count_first_skips_pure_rewrite_without_progress() {
3534 let picked = pick_count_first(
3538 vec![SortedRun::from(vec![
3539 new_file(0, 9, 1, 600),
3540 new_file(20, 29, 2, 600),
3541 new_file(40, 49, 3, 600),
3542 new_file(60, 69, 4, 600),
3543 ])],
3544 Some(512),
3545 );
3546
3547 assert!(picked.is_empty());
3548 }
3549
3550 #[test]
3551 fn test_count_first_allows_overlap_resolution_without_file_reduction() {
3552 let picked = pick_count_first(
3556 vec![
3557 SortedRun::from(vec![new_file(0, 19, 1, 600)]),
3558 SortedRun::from(vec![new_file(10, 29, 2, 600)]),
3559 ],
3560 Some(512),
3561 );
3562
3563 assert_eq!(vec![(0, 19), (10, 29)], picked_ranges(&picked));
3564 }
3565
3566 #[test]
3567 fn test_count_first_prefers_guaranteed_reduction_over_pure_rewrite() {
3568 let picked = pick_count_first(
3572 vec![SortedRun::from(vec![
3573 new_file(0, 9, 1, 600),
3574 new_file(10, 19, 2, 600),
3575 new_file(20, 29, 3, 600),
3576 new_file(30, 39, 4, 10),
3577 new_file(40, 49, 5, 10),
3578 new_file(50, 59, 6, 10),
3579 ])],
3580 Some(512),
3581 );
3582
3583 assert_eq!(vec![(30, 39), (40, 49), (50, 59)], picked_ranges(&picked));
3584 }
3585
3586 #[test]
3587 fn test_count_first_prefers_earlier_time_on_exact_tie() {
3588 let files = (0..=DEFAULT_MAX_INPUT_FILES)
3589 .map(|idx| {
3590 let start = idx as i64 * 20;
3591 new_file(start, start + 9, idx as u64 + 1, 10)
3592 })
3593 .collect::<Vec<_>>();
3594
3595 let picked = pick_count_first(vec![SortedRun::from(files)], None);
3596 let ranges = picked_ranges(&picked);
3597
3598 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3599 assert_eq!(Some(&(0, 9)), ranges.first());
3600 let last_start = (DEFAULT_MAX_INPUT_FILES as i64 - 1) * 20;
3601 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
3602 }
3603
3604 #[test]
3605 fn test_count_first_keeps_each_interleaved_run_contiguous() {
3606 let picked = pick_count_first(
3607 vec![
3608 SortedRun::from(vec![
3609 new_file(0, 9, 1, 1),
3610 new_file(20, 29, 2, 1),
3611 new_file(40, 49, 3, 1),
3612 ]),
3613 SortedRun::from(vec![new_file(10, 19, 4, 1), new_file(30, 39, 5, 1)]),
3614 ],
3615 None,
3616 );
3617
3618 assert_eq!(
3619 vec![(0, 9), (10, 19), (20, 29), (30, 39), (40, 49)],
3620 picked_ranges(&picked)
3621 );
3622 }
3623
3624 #[test]
3631 fn test_count_first_candidate_spans_same_sequence_files() {
3632 let picked = pick_count_first(
3633 vec![SortedRun::from(vec![
3634 new_file(0, 9, 1, 10),
3635 new_file(10, 19, 2, 10),
3637 new_file(10, 19, 2, 10),
3638 new_file(10, 19, 2, 10),
3639 new_file(20, 29, 3, 10),
3640 ])],
3641 None,
3642 );
3643
3644 assert_eq!(
3645 vec![(0, 9), (10, 19), (10, 19), (10, 19), (20, 29)],
3646 picked_ranges(&picked)
3647 );
3648 }
3649
3650 #[tokio::test]
3656 async fn test_count_first_merges_window_with_interleaved_flush_groups() {
3657 let files = [
3658 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
3660 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3662 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3663 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3664 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 3, 10),
3666 ];
3667 let windows = assign_to_windows(files.iter(), 100);
3668 let picker = TwcsPicker {
3669 trigger_file_num: 3,
3670 active_window_l1_merge_trigger: 8,
3671 inactive_window_trigger_file_num: 3,
3672 inactive_window_l1_merge_trigger: 8,
3673 time_window_seconds: Some(100),
3674 max_output_file_size: None,
3675 append_mode: false,
3676 max_background_tasks: None,
3677 time_range: None,
3678 };
3679
3680 let output = picker
3681 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3682 .await
3683 .unwrap();
3684
3685 assert_eq!(1, output.len());
3686 assert_eq!(5, output[0].inputs.len());
3688 }
3689}