1mod units;
16
17use std::collections::hash_map::Entry;
18use std::collections::{BTreeMap, HashMap, HashSet};
19use std::fmt::Debug;
20use std::sync::{Arc, LazyLock};
21
22use common_base::readable_size::ReadableSize;
23use common_telemetry::{debug, info};
24use common_time::Timestamp;
25use common_time::range::TimestampRange;
26use common_time::timestamp::TimeUnit;
27use common_time::timestamp_millis::BucketAligned;
28use prometheus::{HistogramVec, IntCounterVec};
29use snafu::ResultExt;
30use store_api::storage::RegionId;
31
32use crate::compaction::CompactionOutput;
33use crate::compaction::buckets::infer_time_bucket;
34use crate::compaction::compactor::CompactionRegion;
35use crate::compaction::picker::{Picker, PickerOutput, get_expired_ssts};
36use crate::compaction::run::{
37 Ranged, SortedRun, files_overlap, files_overlap_inclusive, find_sorted_runs,
38 find_sorted_runs_by_time_range, merge_primary_key_ranges, primary_key_ranges_overlap,
39};
40use crate::compaction::twcs::units::{ClosedMergeOptions, PickLimits};
41use crate::error::{JoinSnafu, Result};
42use crate::metrics::{TWCS_PICK_ELAPSED, TWCS_SCANNED_WINDOWS};
43use crate::region::options::MergeMode;
44use crate::sst::file::{FileHandle, Level, overlaps};
45use crate::sst::primary_key::PrimaryKeyRangeMapper;
46use crate::sst::version::LevelMeta;
47
48const LEVEL_COMPACTED: Level = 1;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub(crate) enum PickClass {
54 L0Merge,
56 FileReduction,
58 OverlapResolution,
60}
61
62impl PickClass {
63 pub(crate) fn as_str(self) -> &'static str {
64 match self {
65 Self::L0Merge => "l0_merge",
66 Self::FileReduction => "file_reduction",
67 Self::OverlapResolution => "overlap_resolution",
68 }
69 }
70}
71
72#[derive(Debug)]
74pub(crate) struct WindowPick {
75 pub(crate) class: PickClass,
76 branch: &'static str,
78 pub(crate) inputs: Vec<FileHandle>,
79 found_runs: usize,
81}
82
83#[derive(Debug)]
85pub(crate) struct WindowCandidate {
86 pub(crate) window: i64,
87 pub(crate) pick: WindowPick,
88}
89
90pub(crate) struct TwcsCandidates {
93 pub(crate) candidates: Vec<WindowCandidate>,
94 expired_ssts: Vec<FileHandle>,
95 time_window_size: i64,
96 windows: Arc<BTreeMap<i64, Window>>,
97 active_window: Option<i64>,
98 primary_key_mapper: Arc<PrimaryKeyRangeMapper>,
99 region_id: RegionId,
100 append_mode: bool,
101 max_output_file_size: Option<u64>,
102}
103
104impl TwcsCandidates {
105 pub(crate) async fn finish(self) -> Result<PickerOutput> {
108 common_runtime::spawn_blocking_compact(move || {
109 let outputs = self
110 .candidates
111 .into_iter()
112 .rev()
113 .map(|candidate| {
114 let WindowCandidate { window, pick } = candidate;
115 let files = &self.windows[&window];
116 let filter_deleted = !self.append_mode
117 && !window_has_overlap(files, &self.windows)
118 && !selected_overlaps_unselected(
119 &pick.inputs,
120 files,
121 &self.primary_key_mapper,
122 );
123 if pick.inputs.len() > 1 {
124 log_pick_result(
125 self.region_id,
126 window,
127 self.active_window,
128 &pick,
129 files.files.len(),
130 self.max_output_file_size,
131 filter_deleted,
132 );
133 }
134 CompactionOutput {
135 output_level: LEVEL_COMPACTED,
136 inputs: pick.inputs,
137 filter_deleted,
138 output_time_range: None,
139 }
140 })
141 .collect();
142 PickerOutput {
143 outputs,
144 expired_ssts: self.expired_ssts,
145 time_window_size: self.time_window_size,
146 max_file_size: self
148 .max_output_file_size
149 .filter(|size| *size > 0)
150 .map(|size| size as usize),
151 }
152 })
153 .await
154 .context(JoinSnafu)
155 }
156}
157
158struct WindowPickContext<'a> {
160 all_candidates: bool,
161 region_id: RegionId,
162 active_window: Option<i64>,
163 time_window_size: Option<i64>,
164 region_sequence: u64,
165 mapper: &'a PrimaryKeyRangeMapper,
166}
167
168struct WindowOutputContext {
169 all_candidates: bool,
170 expired_ssts: Vec<FileHandle>,
171 active_window: Option<i64>,
172 time_window_size: Option<i64>,
173 max_outputs: Option<usize>,
174 primary_key_mapper: Arc<PrimaryKeyRangeMapper>,
175}
176
177const MAX_L1_L0_ROW_RATIO: usize = 2;
179
180const DEFAULT_MAX_INPUT_FILES: usize = 16;
182
183const MAX_INPUT_FILES_ENV: &str = "GREPTIME_TWCS_MAX_INPUT_FILES";
184
185static MAX_INPUT_FILES: LazyLock<usize> = LazyLock::new(|| {
188 let env_value = std::env::var(MAX_INPUT_FILES_ENV).ok();
189 parse_max_input_files(env_value.as_deref())
190});
191
192const CLOSED_MERGE_INPUT_FACTOR: usize = 4;
196
197fn parse_max_input_files(env_value: Option<&str>) -> usize {
198 env_value
199 .and_then(|env_value| env_value.parse().ok())
200 .filter(|max_input_files| *max_input_files >= 2)
201 .unwrap_or(DEFAULT_MAX_INPUT_FILES)
202}
203
204#[derive(Clone, Debug)]
206pub(crate) struct TwcsPickMetrics {
207 elapsed: HistogramVec,
208 scanned_windows: IntCounterVec,
209}
210
211impl Default for TwcsPickMetrics {
212 fn default() -> Self {
213 Self {
214 elapsed: TWCS_PICK_ELAPSED.clone(),
215 scanned_windows: TWCS_SCANNED_WINDOWS.clone(),
216 }
217 }
218}
219
220#[derive(Clone, Debug)]
223pub struct TwcsPicker {
224 pub(crate) metrics: TwcsPickMetrics,
226 pub trigger_file_num: usize,
228 pub active_window_l1_merge_trigger: usize,
230 pub inactive_window_trigger_file_num: usize,
232 pub inactive_window_l1_merge_trigger: usize,
234 pub time_window_seconds: Option<i64>,
236 pub max_output_file_size: Option<u64>,
239 pub append_mode: bool,
241 pub(crate) merge_mode: MergeMode,
244 pub max_background_tasks: Option<usize>,
246 pub(crate) time_range: Option<TimestampRange>,
248 pub(crate) all_windows_inactive: bool,
252}
253
254impl TwcsPicker {
255 async fn build_candidates(
259 &self,
260 region_id: RegionId,
261 time_windows: BTreeMap<i64, Window>,
262 context: WindowOutputContext,
263 ) -> Result<TwcsCandidates> {
264 let all_candidates = context.all_candidates;
265 let mode = if all_candidates { "fair" } else { "stateless" };
266 let _timer = self
267 .metrics
268 .elapsed
269 .with_label_values(&[mode])
270 .start_timer();
271 let scanned_windows = self.metrics.scanned_windows.with_label_values(&[mode]);
272 let WindowOutputContext {
273 all_candidates: _,
274 expired_ssts,
275 active_window,
276 time_window_size,
277 max_outputs,
278 primary_key_mapper,
279 } = context;
280 let region_sequence = time_windows
281 .values()
282 .flat_map(|window| window.files())
283 .filter_map(|file| file.meta_ref().sequence)
284 .map(|sequence| sequence.get())
285 .max()
286 .unwrap_or(0);
287 let windows = time_windows
288 .values()
289 .rev()
290 .filter(|window| {
291 !window.files.is_empty()
292 && self.time_range.as_ref().is_none_or(|time_range| {
293 time_window_size.is_none_or(|time_window_size| {
294 time_window_intersects_range(
295 window.time_window,
296 time_window_size,
297 time_range,
298 )
299 })
300 })
301 })
302 .map(|window| window.time_window)
303 .collect::<Vec<_>>();
304 let time_windows = Arc::new(time_windows);
305 let max_outputs = max_outputs.unwrap_or(usize::MAX);
306 let chunk_size = self.max_background_tasks.unwrap_or(windows.len()).max(1);
309 let mut picks = Vec::new();
310 for chunk in windows.chunks(chunk_size) {
311 let mut handles = Vec::with_capacity(chunk.len());
312 for &window in chunk {
313 let picker = self.clone();
314 let time_windows = time_windows.clone();
315 let primary_key_mapper = primary_key_mapper.clone();
316 let scanned_windows = scanned_windows.clone();
317 handles.push(common_runtime::spawn_blocking_compact(move || {
318 let mut picks = Vec::new();
319 if let Some(files) = time_windows.get(&window) {
320 scanned_windows.inc();
321 picks.extend(
322 picker
323 .pick_window(
324 files,
325 &WindowPickContext {
326 all_candidates,
327 region_id,
328 active_window,
329 time_window_size,
330 region_sequence,
331 mapper: &primary_key_mapper,
332 },
333 )
334 .into_iter()
335 .map(|pick| WindowCandidate { window, pick }),
336 );
337 }
338 picks
339 }));
340 tokio::task::yield_now().await;
341 }
342 for result in futures::future::join_all(handles).await {
343 picks.extend(result.context(JoinSnafu)?);
344 }
345 if !all_candidates
348 && picks
349 .iter()
350 .filter(|candidate| candidate.pick.class == PickClass::L0Merge)
351 .count()
352 >= max_outputs
353 {
354 break;
355 }
356 }
357
358 if !all_candidates {
359 picks.sort_by_key(|candidate| candidate.pick.class);
361 if picks.len() > max_outputs {
362 debug!(
363 "Region ({:?}) compaction output limit ({}) reached, remaining candidates discarded",
364 region_id, max_outputs
365 );
366 picks.truncate(max_outputs);
367 }
368 }
369 Ok(TwcsCandidates {
370 candidates: picks,
371 expired_ssts,
372 time_window_size: time_window_size.unwrap_or_default(),
375 windows: time_windows,
376 active_window,
377 primary_key_mapper,
378 region_id,
379 append_mode: self.append_mode,
380 max_output_file_size: self.max_output_file_size,
381 })
382 }
383
384 fn pick_window(&self, window: &Window, context: &WindowPickContext<'_>) -> Vec<WindowPick> {
387 let WindowPickContext {
388 all_candidates,
389 region_id,
390 active_window,
391 time_window_size,
392 region_sequence,
393 mapper,
394 } = *context;
395 let mut files: Vec<_> = window.files().cloned().collect();
396 let quiet = is_quiet_window(&files, region_sequence);
397
398 if self.append_mode
400 && let Some(max_size) = self.max_output_file_size
401 {
402 let (kept_files, ignored_files) =
403 files.into_iter().partition(|file| file.size() <= max_size);
404 files = kept_files;
405 if !ignored_files.is_empty() {
406 info!(
407 "Skipped {} large files in append mode for region {}, window {}, max_size: {}",
408 ignored_files.len(),
409 region_id,
410 window.time_window,
411 max_size
412 );
413 }
414 }
415
416 let (l0_files, l1_files): (Vec<_>, Vec<_>) =
417 files.into_iter().partition(|file| file.level() == 0);
418 if !self.all_windows_inactive && active_window == Some(window.time_window) {
419 self.pick_active_window(l0_files, l1_files, mapper, all_candidates)
420 } else if window.files.len() < self.inactive_window_trigger_file_num
421 && l1_files.len() < self.inactive_window_l1_merge_trigger
422 {
423 Vec::new()
424 } else {
425 let settling = !self.all_windows_inactive
426 && is_settling_window(window.time_window, active_window, time_window_size);
427 let num_l1_files = l1_files.len();
428 let mut picks = self.pick_inactive_window(
429 l0_files,
430 l1_files,
431 mapper,
432 settling,
433 quiet,
434 all_candidates,
435 );
436 let needs_reduction = if all_candidates {
437 !picks
438 .iter()
439 .any(|pick| pick.class == PickClass::FileReduction)
440 } else {
441 picks.is_empty()
442 };
443 if needs_reduction
444 && self.merge_mode != MergeMode::LastNonNull
445 && num_l1_files >= self.inactive_window_l1_merge_trigger
446 && let Some(pick) = units::pick_small_l1(&window.files, mapper, self.pick_limits())
449 {
450 picks.push(WindowPick {
451 class: PickClass::FileReduction,
452 branch: "inactive:pick_small_l1",
453 inputs: pick.inputs,
454 found_runs: pick.units,
455 });
456 }
457 picks
458 }
459 }
460
461 fn pick_active_window(
475 &self,
476 l0_files: Vec<FileHandle>,
477 l1_files: Vec<FileHandle>,
478 mapper: &PrimaryKeyRangeMapper,
479 all_candidates: bool,
480 ) -> Vec<WindowPick> {
481 let mut picks = Vec::new();
482 if l0_files.len() >= self.trigger_file_num {
483 let (inputs, found_runs) = pick_candidate_files(
484 l0_files,
485 self.max_output_file_size,
486 pick_count_first,
487 mapper,
488 );
489 if !inputs.is_empty() {
490 picks.push(WindowPick {
491 class: PickClass::L0Merge,
492 branch: "active:pick_count_first",
493 inputs,
494 found_runs,
495 });
496 if !all_candidates {
497 return picks;
498 }
499 }
500 }
501 if l1_files.len() >= self.active_window_l1_merge_trigger
503 && let Some(pick) = units::pick_active_l1(&l1_files, mapper, self.pick_limits())
504 {
505 picks.push(WindowPick {
506 class: PickClass::FileReduction,
507 branch: "active:pick_l1_file_reduction",
508 inputs: pick.inputs,
509 found_runs: pick.units,
510 });
511 }
512 picks
513 }
514
515 fn pick_inactive_window(
542 &self,
543 l0_files: Vec<FileHandle>,
544 l1_files: Vec<FileHandle>,
545 mapper: &PrimaryKeyRangeMapper,
546 settling: bool,
547 quiet: bool,
548 all_candidates: bool,
549 ) -> Vec<WindowPick> {
550 let num_l1_files = l1_files.len();
551 let mut picks = Vec::new();
552 if !l0_files.is_empty()
553 && let Some((inputs, found_runs, branch)) = pick_inactive_l0(
554 l0_files.clone(),
555 (!settling).then_some(l1_files.as_slice()),
556 self.max_output_file_size,
557 mapper,
558 )
559 {
560 picks.push(WindowPick {
561 class: PickClass::L0Merge,
562 branch,
563 inputs,
564 found_runs,
565 });
566 if !all_candidates {
567 return picks;
568 }
569 }
570
571 let options = ClosedMergeOptions {
572 limits: self.closed_merge_limits(),
573 allow_file_reduction: num_l1_files >= self.inactive_window_l1_merge_trigger,
574 allow_conflicts: !settling,
575 allow_unbalanced: (quiet || self.all_windows_inactive) && !settling,
576 };
577 let mut files = l1_files;
580 files.extend(l0_files);
581 let candidates = if all_candidates {
582 units::pick_closed_merge_by_class(&files, mapper, options)
583 } else {
584 [units::pick_closed_merge(&files, mapper, options), None]
585 };
586 for pick in candidates.into_iter().flatten() {
587 let (class, branch) = if pick.dense_fallback {
588 (PickClass::FileReduction, "inactive:dense_file_reduction")
589 } else if pick.reduces_files {
590 (PickClass::FileReduction, "inactive:pick_l1_file_reduction")
591 } else {
592 (PickClass::OverlapResolution, "inactive:pick_l1_overlap")
593 };
594 picks.push(WindowPick {
595 class,
596 branch,
597 inputs: pick.inputs,
598 found_runs: pick.units,
599 });
600 }
601 picks
602 }
603
604 fn pick_limits(&self) -> PickLimits {
605 PickLimits {
606 max_output_file_size: self.max_output_file_size,
607 max_input_files: *MAX_INPUT_FILES,
608 }
609 }
610
611 fn closed_merge_limits(&self) -> PickLimits {
612 PickLimits {
613 max_input_files: MAX_INPUT_FILES.saturating_mul(CLOSED_MERGE_INPUT_FACTOR),
614 ..self.pick_limits()
615 }
616 }
617}
618
619fn pick_inactive_l0(
630 l0_files: Vec<FileHandle>,
631 l1_files: Option<&[FileHandle]>,
632 max_output_file_size: Option<u64>,
633 mapper: &PrimaryKeyRangeMapper,
634) -> Option<(Vec<FileHandle>, usize, &'static str)> {
635 let (inputs, found_runs) = pick_candidate_files(
636 l0_files.clone(),
637 max_output_file_size,
638 pick_count_first,
639 mapper,
640 );
641 if !inputs.is_empty() {
642 return Some((inputs, found_runs, "inactive:pick_count_first"));
643 }
644 if let Some(l1_files) = l1_files {
645 let mut all_files = l0_files.clone();
646 all_files.extend_from_slice(l1_files);
647 let (inputs, found_runs) = pick_candidate_files(
648 all_files,
649 max_output_file_size,
650 pick_mixed_within_budget,
651 mapper,
652 );
653 if !inputs.is_empty() {
654 return Some((inputs, found_runs, "inactive:pick_mixed_within_budget"));
655 }
656 }
657 let (inputs, found_runs) = pick_candidate_files(
658 l0_files,
659 max_output_file_size,
660 pick_unbalanced_count_first,
661 mapper,
662 );
663 (!inputs.is_empty()).then_some((inputs, found_runs, "inactive:pick_unbalanced_count_first"))
664}
665
666type CandidatePicker =
667 fn(Vec<SortedRun<FileHandle>>, Option<u64>, &PrimaryKeyRangeMapper) -> Vec<FileHandle>;
668
669fn pick_candidate_files(
670 mut files: Vec<FileHandle>,
671 max_output_file_size: Option<u64>,
672 picker: CandidatePicker,
673 mapper: &PrimaryKeyRangeMapper,
674) -> (Vec<FileHandle>, usize) {
675 let sorted_runs = if files.len() < 1024 {
676 find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, mapper))
677 } else {
678 find_sorted_runs_by_time_range(&mut files)
679 };
680 let found_runs = sorted_runs.len();
681 (
682 picker(sorted_runs, max_output_file_size, mapper),
683 found_runs,
684 )
685}
686
687#[derive(Debug)]
688struct OrderedFile<'a> {
689 file: &'a FileHandle,
690 run_id: usize,
691 position_in_run: usize,
692 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
694}
695
696impl OrderedFile<'_> {
697 fn overlaps_inclusive(&self, other: &Self) -> bool {
699 self.file.overlap_inclusive(other.file)
700 && match (&self.primary_key_range, &other.primary_key_range) {
701 (Some(lhs), Some(rhs)) => primary_key_ranges_overlap(lhs, rhs),
702 _ => true,
703 }
704 }
705}
706
707#[derive(Debug, Default)]
709struct Candidate {
710 num_files: usize,
712 total_size: usize,
714 largest_file_size: usize,
716 overlap_participants: usize,
719 l0_rows: usize,
721 l1_rows: usize,
723 has_l0: bool,
724 has_l1: bool,
725 has_unknown_rows: bool,
727}
728
729impl Candidate {
730 fn absorb(
734 &mut self,
735 file: &OrderedFile,
736 preceding: &[OrderedFile],
737 participations: &mut Vec<bool>,
738 ) {
739 self.absorb_file(file.file);
740
741 let mut participates = false;
742 for (offset, other) in preceding.iter().enumerate() {
743 if file.run_id != other.run_id && file.overlaps_inclusive(other) {
744 if !participations[offset] {
745 participations[offset] = true;
746 self.overlap_participants += 1;
747 }
748 participates = true;
749 }
750 }
751 participations.push(participates);
752 if participates {
753 self.overlap_participants += 1;
754 }
755 }
756
757 fn absorb_file(&mut self, file: &FileHandle) {
759 self.num_files += 1;
760 let file_size = file.size() as usize;
761 self.total_size += file_size;
762 self.largest_file_size = self.largest_file_size.max(file_size);
763 self.absorb_level_rows(file);
764 }
765
766 fn absorb_level_rows(&mut self, file: &FileHandle) {
767 let num_rows = file.num_rows();
768 self.has_unknown_rows |= num_rows == 0;
769 if file.level() == 0 {
770 self.has_l0 = true;
771 self.l0_rows = self.l0_rows.saturating_add(num_rows);
772 } else {
773 self.has_l1 = true;
774 self.l1_rows = self.l1_rows.saturating_add(num_rows);
775 }
776 }
777
778 fn predicted_output_files(&self, max_output_file_size: Option<u64>) -> usize {
783 match max_output_file_size {
784 Some(max) if max > 0 => self.total_size.div_ceil(max as usize).max(1),
785 _ => 1,
786 }
787 }
788
789 fn file_reduction(&self, max_output_file_size: Option<u64>) -> usize {
793 self.num_files
794 .saturating_sub(self.predicted_output_files(max_output_file_size))
795 }
796
797 fn is_balanced(&self) -> bool {
800 self.largest_file_size <= self.total_size - self.largest_file_size
801 }
802
803 fn has_balanced_level_rows(&self) -> bool {
806 !self.has_l0
807 || !self.has_l1
808 || self.has_unknown_rows
809 || self.l1_rows <= self.l0_rows.saturating_mul(MAX_L1_L0_ROW_RATIO)
810 }
811
812 fn has_mixed_levels(&self) -> bool {
813 self.has_l0 && self.has_l1
814 }
815
816 fn within_rewrite_budget(&self, max_output_file_size: Option<u64>) -> bool {
821 match max_output_file_size {
822 Some(limit) if limit > 0 => self.total_size <= limit as usize,
823 _ => true,
824 }
825 }
826
827 fn makes_progress(&self, max_output_file_size: Option<u64>) -> bool {
832 self.file_reduction(max_output_file_size) > 0 || self.overlap_participants > 0
833 }
834
835 fn score(&self, max_output_file_size: Option<u64>) -> CandidateScore {
836 CandidateScore {
837 file_reduction: self.file_reduction(max_output_file_size),
838 overlap_participants: self.overlap_participants,
839 total_size: self.total_size,
840 }
841 }
842}
843
844#[derive(Debug)]
848struct CandidateScore {
849 file_reduction: usize,
850 overlap_participants: usize,
851 total_size: usize,
852}
853
854impl CandidateScore {
855 fn is_better_than(&self, other: &Self) -> bool {
856 self.file_reduction
857 .cmp(&other.file_reduction)
858 .then_with(|| self.overlap_participants.cmp(&other.overlap_participants))
859 .then_with(|| other.total_size.cmp(&self.total_size))
860 .is_gt()
861 }
862}
863
864fn pick_count_first(
880 sorted_runs: Vec<SortedRun<FileHandle>>,
881 max_output_file_size: Option<u64>,
882 mapper: &PrimaryKeyRangeMapper,
883) -> Vec<FileHandle> {
884 pick_count_first_where(
885 sorted_runs,
886 max_output_file_size,
887 mapper,
888 is_balanced_candidate,
889 )
890}
891
892#[cfg(test)]
893fn pick_mixed_count_first(
894 sorted_runs: Vec<SortedRun<FileHandle>>,
895 max_output_file_size: Option<u64>,
896 mapper: &PrimaryKeyRangeMapper,
897) -> Vec<FileHandle> {
898 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
899 candidate.has_mixed_levels() && is_balanced_candidate(candidate)
900 })
901}
902
903fn pick_mixed_within_budget(
906 sorted_runs: Vec<SortedRun<FileHandle>>,
907 max_output_file_size: Option<u64>,
908 mapper: &PrimaryKeyRangeMapper,
909) -> Vec<FileHandle> {
910 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
911 candidate.has_mixed_levels() && candidate.within_rewrite_budget(max_output_file_size)
912 })
913}
914
915fn pick_unbalanced_count_first(
919 sorted_runs: Vec<SortedRun<FileHandle>>,
920 max_output_file_size: Option<u64>,
921 mapper: &PrimaryKeyRangeMapper,
922) -> Vec<FileHandle> {
923 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |_| true)
924}
925
926fn is_balanced_candidate(candidate: &Candidate) -> bool {
927 candidate.is_balanced() && candidate.has_balanced_level_rows()
928}
929
930fn pick_count_first_where(
931 sorted_runs: Vec<SortedRun<FileHandle>>,
932 max_output_file_size: Option<u64>,
933 mapper: &PrimaryKeyRangeMapper,
934 is_eligible: impl Fn(&Candidate) -> bool,
935) -> Vec<FileHandle> {
936 let files = ordered_files(&sorted_runs, mapper);
937
938 let mut best = None;
939 for left in 0..files.len() {
940 let mut candidate = Candidate::default();
941 let right_bound = left.saturating_add(*MAX_INPUT_FILES).min(files.len());
942 let mut participations: Vec<bool> = Vec::with_capacity(right_bound - left);
943 for right in left..right_bound {
944 candidate.absorb(&files[right], &files[left..right], &mut participations);
945 if candidate.num_files < 2
946 || !is_eligible(&candidate)
947 || !candidate.makes_progress(max_output_file_size)
948 {
949 continue;
950 }
951
952 let score = candidate.score(max_output_file_size);
953 if best
954 .as_ref()
955 .is_none_or(|(best_score, _)| score.is_better_than(best_score))
956 {
957 best = Some((score, &files[left..=right]));
958 }
959 }
960 }
961
962 let Some((_, best)) = best else {
963 return vec![];
964 };
965 best.iter().map(|file| file.file.clone()).collect()
966}
967
968fn ordered_files<'a>(
971 sorted_runs: &'a [SortedRun<FileHandle>],
972 mapper: &PrimaryKeyRangeMapper,
973) -> Vec<OrderedFile<'a>> {
974 let mut files = sorted_runs
975 .iter()
976 .enumerate()
977 .flat_map(|(run_id, run)| {
978 run.items()
979 .iter()
980 .enumerate()
981 .map(move |(position_in_run, file)| OrderedFile {
982 file,
983 run_id,
984 position_in_run,
985 primary_key_range: file.primary_key_range(mapper),
986 })
987 })
988 .collect::<Vec<_>>();
989 files.sort_unstable_by(|lhs, rhs| {
990 let (lhs_start, lhs_end) = lhs.file.range();
991 let (rhs_start, rhs_end) = rhs.file.range();
992 lhs_start
993 .cmp(&rhs_start)
994 .then_with(|| rhs_end.cmp(&lhs_end))
995 .then_with(|| lhs.run_id.cmp(&rhs.run_id))
996 .then_with(|| lhs.position_in_run.cmp(&rhs.position_in_run))
997 });
998 files
999}
1000
1001fn selected_overlaps_unselected(
1002 selected: &[FileHandle],
1003 window: &Window,
1004 mapper: &PrimaryKeyRangeMapper,
1005) -> bool {
1006 let Some((span_start, span_end)) = selected
1009 .iter()
1010 .map(Ranged::range)
1011 .reduce(|(start_a, end_a), (start_b, end_b)| (start_a.min(start_b), end_a.max(end_b)))
1012 else {
1013 return false;
1014 };
1015 let selected_file_ids = selected
1016 .iter()
1017 .map(FileHandle::file_id)
1018 .collect::<HashSet<_>>();
1019 window
1020 .files()
1021 .filter(|file| {
1022 let (start, end) = file.range();
1023 start <= span_end && span_start <= end
1024 })
1025 .filter(|file| !selected_file_ids.contains(&file.file_id()))
1026 .any(|unselected| {
1027 selected
1028 .iter()
1029 .any(|selected| files_overlap_inclusive(selected, unselected, mapper))
1030 })
1031}
1032
1033fn log_pick_result(
1034 region_id: RegionId,
1035 window: i64,
1036 active_window: Option<i64>,
1037 pick: &WindowPick,
1038 file_num: usize,
1039 max_output_file_size: Option<u64>,
1040 filter_deleted: bool,
1041) {
1042 let input_file_str: Vec<String> = pick
1043 .inputs
1044 .iter()
1045 .map(|f| {
1046 let range = f.range();
1047 let start = range.0.to_iso8601_string();
1048 let end = range.1.to_iso8601_string();
1049 let num_rows = f.num_rows();
1050 format!(
1051 "File{{id: {:?}, range: ({}, {}), size: {}, num rows: {} }}",
1052 f.file_id(),
1053 start,
1054 end,
1055 ReadableSize(f.size()),
1056 num_rows
1057 )
1058 })
1059 .collect();
1060 let window_str = Timestamp::new_second(window).to_iso8601_string();
1061 let active_window_str = active_window.map(|s| Timestamp::new_second(s).to_iso8601_string());
1062 let max_output_file_size = max_output_file_size.map(|size| ReadableSize(size).to_string());
1063 info!(
1064 "Region ({:?}) compaction pick result: current window: {}, active window: {:?}, branch: {}, \
1065 found runs: {}, file num: {}, max output file size: {:?}, filter deleted: {}, \
1066 input files: {:?}",
1067 region_id,
1068 window_str,
1069 active_window_str,
1070 pick.branch,
1071 pick.found_runs,
1072 file_num,
1073 max_output_file_size,
1074 filter_deleted,
1075 input_file_str
1076 );
1077}
1078
1079#[async_trait::async_trait]
1080impl Picker for TwcsPicker {
1081 async fn pick(&self, compaction_region: &CompactionRegion) -> Result<Option<PickerOutput>> {
1082 self.pick_with_output_limit(compaction_region, self.max_background_tasks)
1083 .await
1084 }
1085}
1086
1087impl TwcsPicker {
1088 pub(crate) async fn pick_with_output_limit(
1089 &self,
1090 compaction_region: &CompactionRegion,
1091 max_outputs: Option<usize>,
1092 ) -> Result<Option<PickerOutput>> {
1093 let candidates = self
1094 .discover_with_limit(compaction_region, max_outputs, false)
1095 .await?;
1096 let output = candidates.finish().await?;
1097 Ok((!output.outputs.is_empty() || !output.expired_ssts.is_empty()).then_some(output))
1098 }
1099
1100 pub(crate) async fn discover(&self, region: &CompactionRegion) -> Result<TwcsCandidates> {
1102 self.discover_with_limit(region, None, true).await
1103 }
1104
1105 async fn discover_with_limit(
1106 &self,
1107 compaction_region: &CompactionRegion,
1108 max_outputs: Option<usize>,
1109 all_candidates: bool,
1110 ) -> Result<TwcsCandidates> {
1111 let region_id = compaction_region.region_id;
1112 let picker = self.clone();
1113 let compaction_region = compaction_region.clone();
1114 let primary_key_mapper = compaction_region.current_version.ssts.primary_key_mapper();
1115 let window_mapper = primary_key_mapper.clone();
1116 let (expired_ssts, time_window_size, active_window, windows) =
1117 common_runtime::spawn_blocking_compact(move || {
1118 let levels = compaction_region.current_version.ssts.levels();
1119 let expired_ssts = get_expired_ssts(
1120 levels,
1121 compaction_region.ttl,
1122 Timestamp::current_millis(),
1123 );
1124 if !expired_ssts.is_empty() {
1125 info!("Expired SSTs in region {}: {:?}", region_id, expired_ssts);
1126 }
1127 let expired_file_ids = expired_ssts
1128 .iter()
1129 .map(|file| file.file_id())
1130 .collect::<HashSet<_>>();
1131
1132 let compaction_time_window = compaction_region
1133 .current_version
1134 .compaction_time_window
1135 .map(|window| window.as_secs() as i64);
1136 let time_window_size = compaction_time_window
1137 .or(picker.time_window_seconds)
1138 .unwrap_or_else(|| {
1139 let inferred = infer_time_bucket(levels[0].files());
1140 info!(
1141 "Compaction window for region {} is not present, inferring from files: {:?}",
1142 region_id, inferred
1143 );
1144 inferred
1145 });
1146
1147 let windows = assign_to_windows(
1148 levels
1149 .iter()
1150 .flat_map(LevelMeta::files)
1151 .filter(|file| !expired_file_ids.contains(&file.file_id())),
1152 time_window_size,
1153 &window_mapper,
1154 );
1155 let active_window = find_active_window_by_sequence(
1158 windows.values().flat_map(Window::files),
1159 time_window_size,
1160 )
1161 .or_else(|| {
1162 find_latest_window_in_seconds(
1163 windows
1164 .values()
1165 .flat_map(Window::files)
1166 .filter(|file| file.level() == 0),
1167 time_window_size,
1168 )
1169 });
1170
1171 (expired_ssts, time_window_size, active_window, windows)
1172 })
1173 .await
1174 .context(JoinSnafu)?;
1175
1176 self.build_candidates(
1177 region_id,
1178 windows,
1179 WindowOutputContext {
1180 all_candidates,
1181 expired_ssts,
1182 active_window,
1183 time_window_size: Some(time_window_size),
1184 max_outputs,
1185 primary_key_mapper,
1186 },
1187 )
1188 .await
1189 }
1190}
1191
1192#[derive(Clone)]
1193struct Window {
1194 start: Timestamp,
1195 end: Timestamp,
1196 files: Vec<FileHandle>,
1197 time_window: i64,
1199 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
1200}
1201
1202impl Window {
1203 fn new_with_file(file: FileHandle, mapper: &PrimaryKeyRangeMapper) -> Self {
1205 let (start, end) = file.time_range();
1206 let primary_key_range = file.primary_key_range(mapper);
1207 Self {
1208 start,
1209 end,
1210 files: vec![file],
1211 time_window: 0,
1212 primary_key_range,
1213 }
1214 }
1215
1216 fn range(&self) -> (Timestamp, Timestamp) {
1218 (self.start, self.end)
1219 }
1220
1221 fn add_file(&mut self, file: FileHandle, mapper: &PrimaryKeyRangeMapper) {
1223 let (start, end) = file.time_range();
1224 self.start = self.start.min(start);
1225 self.end = self.end.max(end);
1226 self.primary_key_range = merge_primary_key_ranges(
1227 self.primary_key_range.take(),
1228 file.primary_key_range(mapper),
1229 );
1230 self.files.push(file);
1231 }
1232
1233 fn files(&self) -> impl Iterator<Item = &FileHandle> {
1234 self.files.iter()
1235 }
1236}
1237
1238fn is_settling_window(
1241 window: i64,
1242 active_window: Option<i64>,
1243 time_window_size: Option<i64>,
1244) -> bool {
1245 active_window
1246 .zip(time_window_size)
1247 .is_some_and(|(active, size)| {
1248 active
1250 .checked_sub(window)
1251 .is_some_and(|distance| (1..=size).contains(&distance))
1252 })
1253}
1254
1255fn is_quiet_window(files: &[FileHandle], region_sequence: u64) -> bool {
1261 if files.iter().any(|file| file.num_rows() == 0) {
1262 return false;
1263 }
1264 let Some(window_sequence) = files
1265 .iter()
1266 .filter_map(|file| file.meta_ref().sequence)
1267 .map(|sequence| sequence.get())
1268 .max()
1269 else {
1270 return false;
1271 };
1272 let rows: u64 = files.iter().map(|file| file.num_rows() as u64).sum();
1273 region_sequence.saturating_sub(window_sequence) >= rows.max(1)
1274}
1275
1276fn assign_to_windows<'a>(
1278 files: impl Iterator<Item = &'a FileHandle>,
1279 time_window_size: i64,
1280 mapper: &PrimaryKeyRangeMapper,
1281) -> BTreeMap<i64, Window> {
1282 let mut windows: HashMap<i64, Window> = HashMap::new();
1283 for f in files {
1285 if f.compacting() {
1286 continue;
1287 }
1288 let (_, end) = f.time_range();
1289 let time_window = time_window_start(end, time_window_size);
1290
1291 match windows.entry(time_window) {
1292 Entry::Occupied(mut e) => {
1293 e.get_mut().add_file(f.clone(), mapper);
1294 }
1295 Entry::Vacant(e) => {
1296 let mut window = Window::new_with_file(f.clone(), mapper);
1297 window.time_window = time_window;
1298 e.insert(window);
1299 }
1300 }
1301 }
1302 windows.into_iter().collect()
1303}
1304
1305fn time_window_start(timestamp: Timestamp, time_window_size: i64) -> i64 {
1308 timestamp
1310 .convert_to(TimeUnit::Second)
1311 .and_then(|ts| ts.value().align_by_bucket(time_window_size))
1312 .unwrap_or(i64::MIN)
1313}
1314
1315fn time_window_intersects_range(
1316 window_start: i64,
1317 time_window_size: i64,
1318 time_range: &TimestampRange,
1319) -> bool {
1320 if time_range.is_empty() {
1321 return false;
1322 }
1323 let first_window = time_range
1324 .start()
1325 .map_or(i64::MIN, |start| time_window_start(start, time_window_size));
1326 let last_window = match time_range.end() {
1327 None => i64::MAX,
1328 Some(end) => {
1329 let Some(last_second) = end
1331 .convert_to_ceil(TimeUnit::Second)
1332 .and_then(|timestamp| timestamp.value().checked_sub(1))
1333 else {
1334 return false;
1335 };
1336 time_window_start(Timestamp::new_second(last_second), time_window_size)
1337 }
1338 };
1339 (first_window..=last_window).contains(&window_start)
1340}
1341
1342fn window_has_overlap(this: &Window, windows: &BTreeMap<i64, Window>) -> bool {
1343 windows
1344 .values()
1345 .filter(|that| this.time_window != that.time_window)
1346 .any(|that| {
1347 overlaps(&this.range(), &that.range()) && {
1348 match (&this.primary_key_range, &that.primary_key_range) {
1349 (Some(l), Some(r)) => primary_key_ranges_overlap(l, r),
1350 _ => true,
1351 }
1352 }
1353 })
1354}
1355
1356fn find_latest_window_in_seconds<'a>(
1359 files: impl Iterator<Item = &'a FileHandle>,
1360 time_window_size: i64,
1361) -> Option<i64> {
1362 let mut latest_timestamp = None;
1363 for f in files {
1364 let (_, end) = f.time_range();
1365 if let Some(latest) = latest_timestamp {
1366 if end > latest {
1367 latest_timestamp = Some(end);
1368 }
1369 } else {
1370 latest_timestamp = Some(end);
1371 }
1372 }
1373 latest_timestamp.map(|ts| time_window_start(ts, time_window_size))
1374}
1375
1376fn find_active_window_by_sequence<'a>(
1385 files: impl Iterator<Item = &'a FileHandle>,
1386 time_window_size: i64,
1387) -> Option<i64> {
1388 files
1389 .filter(|f| f.meta_ref().sequence.is_some())
1390 .max_by_key(|f| f.meta_ref().sequence)
1391 .map(|f| time_window_start(f.time_range().1, time_window_size))
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396 use std::collections::HashSet;
1397 use std::num::NonZeroU64;
1398 use std::sync::Arc;
1399 use std::time::{Duration, Instant};
1400
1401 use common_base::Plugins;
1402 use common_time::range::TimestampRange;
1403 use store_api::storage::FileId;
1404
1405 use super::*;
1406 use crate::cache::CacheManager;
1407 use crate::compaction::compactor::CompactionVersion;
1408 use crate::compaction::scheduler::history::{PickHistory, PickKey};
1409 use crate::compaction::scheduler::pick_automatic_for_test;
1410 use crate::compaction::test_util::{
1411 compaction_region_with_ssts, new_file_handle, new_file_handle_with_sequence,
1412 new_file_handle_with_size_and_sequence,
1413 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
1414 primary_key_mapper_for_test,
1415 };
1416 use crate::config::MitoConfig;
1417 use crate::region::options::RegionOptions;
1418 use crate::sst::file::{FileMeta, Level};
1419 use crate::sst::version::SstVersion;
1420 use crate::test_util::memtable_util::metadata_for_test;
1421 use crate::test_util::scheduler_util::SchedulerEnv;
1422
1423 fn assign_to_windows<'a>(
1424 files: impl Iterator<Item = &'a FileHandle>,
1425 time_window_size: i64,
1426 ) -> BTreeMap<i64, Window> {
1427 super::assign_to_windows(files, time_window_size, &primary_key_mapper_for_test())
1428 }
1429
1430 fn pick_count_first(
1431 sorted_runs: Vec<SortedRun<FileHandle>>,
1432 max_output_file_size: Option<u64>,
1433 ) -> Vec<FileHandle> {
1434 super::pick_count_first(
1435 sorted_runs,
1436 max_output_file_size,
1437 &primary_key_mapper_for_test(),
1438 )
1439 }
1440
1441 fn pick_mixed_count_first(
1442 sorted_runs: Vec<SortedRun<FileHandle>>,
1443 max_output_file_size: Option<u64>,
1444 ) -> Vec<FileHandle> {
1445 super::pick_mixed_count_first(
1446 sorted_runs,
1447 max_output_file_size,
1448 &primary_key_mapper_for_test(),
1449 )
1450 }
1451
1452 impl TwcsPicker {
1453 async fn build_output_with_time_range(
1454 &self,
1455 region_id: RegionId,
1456 time_windows: BTreeMap<i64, Window>,
1457 active_window: Option<i64>,
1458 time_window_size: Option<i64>,
1459 ) -> Result<Vec<CompactionOutput>> {
1460 self.build_candidates(
1461 region_id,
1462 time_windows,
1463 WindowOutputContext {
1464 all_candidates: false,
1465 expired_ssts: Vec::new(),
1466 active_window,
1467 time_window_size,
1468 max_outputs: self.max_background_tasks,
1469 primary_key_mapper: Arc::new(primary_key_mapper_for_test()),
1470 },
1471 )
1472 .await?
1473 .finish()
1474 .await
1475 .map(|output| output.outputs)
1476 }
1477
1478 async fn build_output_with_history(
1479 &self,
1480 time_windows: BTreeMap<i64, Window>,
1481 active_window: Option<i64>,
1482 history: &mut PickHistory,
1483 ) -> Result<Vec<CompactionOutput>> {
1484 let mut candidates = self
1485 .build_candidates(
1486 RegionId::from_u64(1),
1487 time_windows,
1488 WindowOutputContext {
1489 all_candidates: true,
1490 expired_ssts: Vec::new(),
1491 active_window,
1492 time_window_size: None,
1493 max_outputs: None,
1494 primary_key_mapper: Arc::new(primary_key_mapper_for_test()),
1495 },
1496 )
1497 .await?;
1498 let now = Instant::now();
1499 candidates.candidates = history.rank_candidates(candidates.candidates, now);
1500 candidates
1501 .candidates
1502 .truncate(self.max_background_tasks.unwrap_or(usize::MAX));
1503 history.acknowledge(candidates.candidates.iter().map(PickKey::from), now);
1504 candidates.finish().await.map(|output| output.outputs)
1505 }
1506 }
1507
1508 #[test]
1509 fn test_cross_schema_pk_overlap_before_window_aggregation() {
1510 use crate::test_util::sst_util::{new_primary_key, sst_region_metadata};
1511
1512 let metadata = Arc::new(sst_region_metadata());
1513 let mut old = FileMeta {
1514 region_id: metadata.region_id,
1515 file_id: FileId::random(),
1516 time_range: (
1517 Timestamp::new_millisecond(0),
1518 Timestamp::new_millisecond(1000),
1519 ),
1520 primary_key_min: Some(new_primary_key(&["a"]).into()),
1521 primary_key_max: Some(new_primary_key(&["b"]).into()),
1522 ..Default::default()
1523 };
1524 let mut completed = new_primary_key(&["b"]);
1525 completed.push(0); let new = FileMeta {
1527 file_id: FileId::random(),
1528 time_range: (
1529 Timestamp::new_millisecond(500),
1530 Timestamp::new_millisecond(2000),
1531 ),
1532 primary_key_min: Some(completed.clone().into()),
1533 primary_key_max: Some(completed.clone().into()),
1534 ..old.clone()
1535 };
1536 assert!(old.primary_key_max < new.primary_key_min);
1537 let mut ssts = SstVersion::new(metadata);
1538 let old_id = old.file_id;
1539 let new_id = new.file_id;
1540 ssts.add_files(
1541 crate::test_util::new_noop_file_purger(),
1542 [old.clone(), new].into_iter(),
1543 );
1544 let old_file = &ssts.levels()[0].files[&old_id];
1545 let new_file = &ssts.levels()[0].files[&new_id];
1546 let ranges = ssts.primary_key_mapper();
1547 let windows = super::assign_to_windows([old_file, new_file].into_iter(), 1, &ranges);
1548 assert_eq!(2, windows.len());
1549 assert!(
1550 windows
1551 .values()
1552 .all(|window| window_has_overlap(window, &windows))
1553 );
1554
1555 let mut window = Window::new_with_file(old_file.clone(), &ranges);
1556 window.add_file(new_file.clone(), &ranges);
1557 assert!(selected_overlaps_unselected(
1558 std::slice::from_ref(new_file),
1559 &window,
1560 &ranges,
1561 ));
1562 assert_eq!(
1563 Some(completed.into()),
1564 window
1565 .primary_key_range
1566 .as_ref()
1567 .map(|range| range.1.clone())
1568 );
1569
1570 old.file_id = FileId::random();
1572 old.primary_key_max = old.primary_key_min.clone();
1573 let disjoint_id = old.file_id;
1574 ssts.add_files(crate::test_util::new_noop_file_purger(), [old].into_iter());
1575 let files = &ssts.levels()[0].files;
1576 assert!(!files_overlap_inclusive(
1577 &files[&disjoint_id],
1578 &files[&new_id],
1579 &ranges
1580 ));
1581 }
1582
1583 #[test]
1584 fn test_valid_max_input_files_env_overrides_default() {
1585 assert_eq!(64, parse_max_input_files(Some("64")));
1586 }
1587
1588 #[test]
1589 fn test_invalid_max_input_files_env_falls_back_to_default() {
1590 for env_value in [None, Some(""), Some("invalid"), Some("0"), Some("1")] {
1591 assert_eq!(16, parse_max_input_files(env_value));
1592 }
1593 }
1594
1595 async fn compaction_region_with_expired_sst() -> CompactionRegion {
1596 compaction_region_with_ssts(
1597 (1..=4).map(|sequence| FileMeta {
1598 file_id: FileId::random(),
1599 time_range: (
1600 Timestamp::new_millisecond(0),
1601 Timestamp::new_millisecond(10),
1602 ),
1603 level: 0,
1604 sequence: NonZeroU64::new(sequence),
1605 ..Default::default()
1606 }),
1607 Duration::from_millis(1),
1608 )
1609 .await
1610 }
1611
1612 #[tokio::test]
1613 async fn test_pick_normalizes_zero_output_size_to_unlimited() {
1614 let mut compaction_region = compaction_region_with_ssts(
1615 (1..=4).map(|sequence| new_file(0, 10, sequence, 100).meta_ref().clone()),
1616 Duration::from_secs(3600),
1617 )
1618 .await;
1619 compaction_region.ttl = None;
1620
1621 for (max_output_file_size, expected_max_file_size) in
1622 [(Some(0), None), (None, None), (Some(1024), Some(1024))]
1623 {
1624 let picker = TwcsPicker {
1625 trigger_file_num: 4,
1626 active_window_l1_merge_trigger: 8,
1627 inactive_window_trigger_file_num: 4,
1628 inactive_window_l1_merge_trigger: 8,
1629 time_window_seconds: Some(3),
1630 max_output_file_size,
1631 append_mode: false,
1632 merge_mode: MergeMode::LastRow,
1633 max_background_tasks: None,
1634 time_range: None,
1635 all_windows_inactive: false,
1636 metrics: Default::default(),
1637 };
1638
1639 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1640
1641 assert_eq!(output.outputs.len(), 1);
1642 assert_eq!(output.outputs[0].inputs.len(), 4);
1643 assert!(output.expired_ssts.is_empty());
1644 assert_eq!(output.max_file_size, expected_max_file_size);
1645 }
1646 }
1647
1648 #[tokio::test]
1649 async fn test_pick_expired_ssts_without_marking_compacting() {
1650 let picker = TwcsPicker {
1651 trigger_file_num: 4,
1652 active_window_l1_merge_trigger: 8,
1653 inactive_window_trigger_file_num: 4,
1654 inactive_window_l1_merge_trigger: 8,
1655 time_window_seconds: Some(3),
1656 max_output_file_size: None,
1657 append_mode: false,
1658 merge_mode: MergeMode::LastRow,
1659 max_background_tasks: None,
1660 time_range: None,
1661 all_windows_inactive: false,
1662 metrics: Default::default(),
1663 };
1664 let compaction_region = compaction_region_with_expired_sst().await;
1665
1666 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1667
1668 assert!(output.outputs.is_empty());
1669 assert!(!output.expired_ssts.is_empty());
1670 assert!(output.expired_ssts.iter().all(|file| !file.compacting()));
1671 }
1672
1673 #[tokio::test]
1674 async fn test_expired_sst_does_not_determine_active_window() {
1675 let now = Timestamp::current_millis().value();
1676 let files = [
1677 FileMeta {
1678 file_id: FileId::random(),
1679 time_range: (
1680 Timestamp::new_millisecond(0),
1681 Timestamp::new_millisecond(10),
1682 ),
1683 level: 0,
1684 sequence: NonZeroU64::new(100),
1685 ..Default::default()
1686 },
1687 FileMeta {
1688 file_id: FileId::random(),
1689 time_range: (
1690 Timestamp::new_millisecond(now - 1000),
1691 Timestamp::new_millisecond(now),
1692 ),
1693 level: 0,
1694 sequence: NonZeroU64::new(1),
1695 ..Default::default()
1696 },
1697 FileMeta {
1698 file_id: FileId::random(),
1699 time_range: (
1700 Timestamp::new_millisecond(now - 1000),
1701 Timestamp::new_millisecond(now),
1702 ),
1703 level: 0,
1704 sequence: NonZeroU64::new(2),
1705 ..Default::default()
1706 },
1707 ];
1708 let compaction_region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
1709 let picker = TwcsPicker {
1710 trigger_file_num: 4,
1711 active_window_l1_merge_trigger: 8,
1712 inactive_window_trigger_file_num: 2,
1713 inactive_window_l1_merge_trigger: 8,
1714 time_window_seconds: Some(3),
1715 max_output_file_size: None,
1716 append_mode: false,
1717 merge_mode: MergeMode::LastRow,
1718 max_background_tasks: None,
1719 time_range: None,
1720 all_windows_inactive: false,
1721 metrics: Default::default(),
1722 };
1723
1724 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1725
1726 assert_eq!(1, output.expired_ssts.len());
1727 assert!(output.outputs.is_empty());
1728 }
1729
1730 #[tokio::test]
1731 async fn test_active_window_survives_l0_consumption() {
1732 let env = SchedulerEnv::new().await;
1737 let metadata = metadata_for_test();
1738 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
1739 let mut ssts = SstVersion::new(metadata.clone());
1740 ssts.add_files(
1741 Arc::new(crate::sst::file_purger::NoopFilePurger),
1742 [100, 101].into_iter().map(|sequence| FileMeta {
1743 region_id: metadata.region_id,
1744 file_id: FileId::random(),
1745 time_range: (
1746 Timestamp::new_millisecond(0),
1747 Timestamp::new_millisecond(10),
1748 ),
1749 level: 1,
1750 sequence: NonZeroU64::new(sequence),
1751 ..Default::default()
1752 }),
1753 );
1754 let compaction_region = CompactionRegion {
1755 region_id: metadata.region_id,
1756 region_options: RegionOptions::default(),
1757 engine_config: Arc::new(MitoConfig::default()),
1758 region_metadata: metadata.clone(),
1759 cache_manager: Arc::new(CacheManager::default()),
1760 access_layer: env.access_layer,
1761 manifest_ctx,
1762 current_version: CompactionVersion {
1763 metadata,
1764 options: RegionOptions::default(),
1765 ssts: Arc::new(ssts),
1766 memtable_min_sequence: None,
1767 compaction_time_window: None,
1768 },
1769 file_purger: None,
1770 ttl: None,
1771 max_parallelism: 1,
1772 plugins: Plugins::new(),
1773 };
1774 let picker = TwcsPicker {
1775 trigger_file_num: 4,
1776 active_window_l1_merge_trigger: 8,
1777 inactive_window_trigger_file_num: 2,
1778 inactive_window_l1_merge_trigger: 8,
1779 time_window_seconds: Some(3600),
1780 max_output_file_size: None,
1781 append_mode: false,
1782 merge_mode: MergeMode::LastRow,
1783 max_background_tasks: None,
1784 time_range: None,
1785 all_windows_inactive: false,
1786 metrics: Default::default(),
1787 };
1788
1789 assert!(picker.pick(&compaction_region).await.unwrap().is_none());
1790 }
1791
1792 #[test]
1793 fn test_find_active_window_by_sequence() {
1794 let files = [
1799 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 1),
1800 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 1, 10),
1801 ];
1802 assert_eq!(Some(2), find_active_window_by_sequence(files.iter(), 1));
1803
1804 let files = [
1807 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 10),
1808 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 1),
1809 ];
1810 assert_eq!(Some(0), find_active_window_by_sequence(files.iter(), 1));
1811
1812 let files = [new_file_handle_with_sequence(
1815 FileId::random(),
1816 0,
1817 999,
1818 1,
1819 0,
1820 )];
1821 assert_eq!(None, find_active_window_by_sequence(files.iter(), 1));
1822 assert!(find_active_window_by_sequence(Vec::<FileHandle>::new().iter(), 1).is_none());
1823 }
1824
1825 #[test]
1826 fn test_active_window_falls_back_to_l0_rule_without_sequence() {
1827 let active_window_of = |files: &[FileHandle]| {
1828 find_active_window_by_sequence(files.iter(), 1).or_else(|| {
1829 find_latest_window_in_seconds(files.iter().filter(|f| f.level() == 0), 1)
1830 })
1831 };
1832
1833 let files = [
1835 new_file_handle_with_sequence(FileId::random(), 0, 999, 1, 0),
1836 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 0),
1837 ];
1838 assert_eq!(Some(2), active_window_of(&files));
1839
1840 let files = [new_file_handle_with_sequence(
1842 FileId::random(),
1843 0,
1844 999,
1845 1,
1846 0,
1847 )];
1848 assert_eq!(None, active_window_of(&files));
1849 }
1850
1851 #[test]
1852 fn test_get_latest_window_in_seconds() {
1853 assert_eq!(
1854 Some(0),
1855 find_latest_window_in_seconds([new_file_handle(FileId::random(), 0, 999, 0)].iter(), 1)
1856 );
1857 assert_eq!(
1858 Some(1),
1859 find_latest_window_in_seconds(
1860 [new_file_handle(FileId::random(), 0, 1000, 0)].iter(),
1861 1
1862 )
1863 );
1864
1865 assert_eq!(
1866 Some(-9223372036857600),
1867 find_latest_window_in_seconds(
1868 [new_file_handle(FileId::random(), i64::MIN, i64::MIN + 1, 0)].iter(),
1869 3600,
1870 )
1871 );
1872
1873 assert_eq!(
1874 (i64::MAX / 10000000) * 10000,
1875 find_latest_window_in_seconds(
1876 [new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0)].iter(),
1877 10000,
1878 )
1879 .unwrap()
1880 );
1881
1882 assert_eq!(
1883 Some((i64::MAX / 3600000) * 3600),
1884 find_latest_window_in_seconds(
1885 [
1886 new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0),
1887 new_file_handle(FileId::random(), 0, 1000, 0)
1888 ]
1889 .iter(),
1890 3600
1891 )
1892 );
1893 }
1894
1895 #[test]
1896 fn test_window_assignment_and_activity_at_boundaries() {
1897 for (unit, ticks_per_second) in [
1900 (TimeUnit::Second, 1),
1901 (TimeUnit::Millisecond, 1_000),
1902 (TimeUnit::Microsecond, 1_000_000),
1903 (TimeUnit::Nanosecond, 1_000_000_000),
1904 ] {
1905 for window_size in [1, 3, 86_400] {
1906 for boundary in [-window_size, 0, window_size] {
1907 for (offset, expected_window) in [
1908 (-1, boundary - window_size),
1909 (0, boundary),
1910 (ticks_per_second - 1, boundary),
1911 (ticks_per_second, boundary + i64::from(window_size == 1)),
1912 ] {
1913 let timestamp = Timestamp::new(boundary * ticks_per_second + offset, unit);
1914 let file = FileHandle::new(
1915 FileMeta {
1916 time_range: (timestamp, timestamp),
1917 sequence: NonZeroU64::new(1),
1918 ..Default::default()
1919 },
1920 crate::test_util::new_noop_file_purger(),
1921 );
1922 let files = [file];
1923 let windows = assign_to_windows(files.iter(), window_size);
1924 assert_eq!(
1925 vec![expected_window],
1926 windows.keys().copied().collect::<Vec<_>>(),
1927 "{timestamp:?}, window size {window_size}"
1928 );
1929 assert_eq!(
1930 Some(expected_window),
1931 find_active_window_by_sequence(files.iter(), window_size)
1932 );
1933 assert_eq!(
1934 Some(expected_window),
1935 find_latest_window_in_seconds(files.iter(), window_size)
1936 );
1937 }
1938 }
1939 }
1940 }
1941 }
1942
1943 #[test]
1944 fn test_settling_window_at_timestamp_limits() {
1945 assert!(is_settling_window(0, Some(60), Some(60)));
1946 assert!(!is_settling_window(60, Some(60), Some(60)));
1947 assert!(!is_settling_window(-60, Some(60), Some(60)));
1948 assert!(!is_settling_window(0, None, Some(60)));
1949 assert!(!is_settling_window(i64::MAX, Some(i64::MIN), Some(1)));
1951 assert!(!is_settling_window(i64::MIN, Some(i64::MAX), Some(1)));
1952 }
1953
1954 #[test]
1955 fn test_window_assignment_at_timestamp_limits() {
1956 for (seconds, expected_window) in [(i64::MIN, i64::MIN), (i64::MAX, i64::MAX - 1)] {
1957 let timestamp = Timestamp::new_second(seconds);
1958 let files = [FileHandle::new(
1959 FileMeta {
1960 time_range: (timestamp, timestamp),
1961 sequence: NonZeroU64::new(1),
1962 ..Default::default()
1963 },
1964 crate::test_util::new_noop_file_purger(),
1965 )];
1966 let windows = assign_to_windows(files.iter(), 3);
1967 assert_eq!(
1968 vec![expected_window],
1969 windows.keys().copied().collect::<Vec<_>>()
1970 );
1971 assert_eq!(
1972 Some(expected_window),
1973 find_active_window_by_sequence(files.iter(), 3)
1974 );
1975 assert_eq!(
1976 Some(expected_window),
1977 find_latest_window_in_seconds(files.iter(), 3)
1978 );
1979 assert!(time_window_intersects_range(
1980 expected_window,
1981 3,
1982 &TimestampRange::single(timestamp)
1983 ));
1984 }
1985 }
1986
1987 #[tokio::test]
1988 async fn test_midnight_file_is_not_compacted_with_previous_day() {
1989 let midnight = 86_400_000;
1991 CompactionPickerTestCase {
1992 window_size: 86_400,
1993 input_files: [
1994 (midnight - 240_000, midnight - 180_000),
1995 (midnight - 180_000, midnight - 90_000),
1996 (midnight - 90_000, midnight - 30_000),
1997 (midnight, midnight),
1998 ]
1999 .into_iter()
2000 .enumerate()
2001 .map(|(index, (start, end))| {
2002 new_file_handle_with_sequence(FileId::random(), start, end, 0, index as u64 + 1)
2003 })
2004 .collect(),
2005 expected_outputs: vec![ExpectedOutput {
2006 input_files: vec![0, 1, 2],
2007 output_level: 1,
2008 }],
2009 }
2010 .check()
2011 .await;
2012 }
2013
2014 #[test]
2015 fn test_assign_to_windows() {
2016 let windows = assign_to_windows(
2017 [
2018 new_file_handle(FileId::random(), 0, 999, 0),
2019 new_file_handle(FileId::random(), 0, 999, 0),
2020 new_file_handle(FileId::random(), 0, 999, 0),
2021 new_file_handle(FileId::random(), 0, 999, 0),
2022 new_file_handle(FileId::random(), 0, 999, 0),
2023 ]
2024 .iter(),
2025 3,
2026 );
2027 let fgs = &windows.get(&0).unwrap().files;
2028 assert_eq!(5, fgs.len());
2029
2030 let files = [FileId::random(); 3];
2031 let windows = assign_to_windows(
2032 [
2033 new_file_handle(files[0], -2000, -3, 0),
2034 new_file_handle(files[1], 0, 2999, 0),
2035 new_file_handle(files[2], 50, 10001, 0),
2036 ]
2037 .iter(),
2038 3,
2039 );
2040 assert_eq!(
2041 files[0],
2042 windows
2043 .get(&-3)
2044 .unwrap()
2045 .files()
2046 .next()
2047 .unwrap()
2048 .file_id()
2049 .file_id()
2050 );
2051 assert_eq!(
2052 files[1],
2053 windows
2054 .get(&0)
2055 .unwrap()
2056 .files()
2057 .next()
2058 .unwrap()
2059 .file_id()
2060 .file_id()
2061 );
2062 assert_eq!(
2063 files[2],
2064 windows
2065 .get(&9)
2066 .unwrap()
2067 .files()
2068 .next()
2069 .unwrap()
2070 .file_id()
2071 .file_id()
2072 );
2073 }
2074
2075 #[test]
2076 fn test_assign_files_to_windows() {
2077 let files = [
2078 FileId::random(),
2079 FileId::random(),
2080 FileId::random(),
2081 FileId::random(),
2082 ];
2083 let windows = assign_to_windows(
2084 [
2085 new_file_handle_with_sequence(files[0], 0, 999, 0, 1),
2086 new_file_handle_with_sequence(files[1], 0, 999, 0, 1),
2087 new_file_handle_with_sequence(files[2], 0, 999, 0, 2),
2088 new_file_handle_with_sequence(files[3], 0, 999, 0, 2),
2089 ]
2090 .iter(),
2091 3,
2092 );
2093 assert_eq!(windows.len(), 1);
2094 let window_files = &windows.get(&0).unwrap().files;
2095 assert_eq!(4, window_files.len());
2096 assert_eq!(
2097 window_files
2098 .iter()
2099 .map(|f| f.file_id().file_id())
2100 .collect::<HashSet<_>>(),
2101 files.into_iter().collect()
2102 );
2103 }
2104
2105 #[test]
2106 fn test_assign_compacting_to_windows() {
2107 let files = [
2108 new_file_handle(FileId::random(), 0, 999, 0),
2109 new_file_handle(FileId::random(), 0, 999, 0),
2110 new_file_handle(FileId::random(), 0, 999, 0),
2111 new_file_handle(FileId::random(), 0, 999, 0),
2112 new_file_handle(FileId::random(), 0, 999, 0),
2113 ];
2114 files[0].set_compacting(true);
2115 files[2].set_compacting(true);
2116 let mut windows = assign_to_windows(files.iter(), 3);
2117 let window0 = windows.remove(&0).unwrap();
2118 assert_eq!(3, window0.files.len());
2119 let candidates = window0
2120 .files
2121 .iter()
2122 .map(|f| f.file_id().file_id())
2123 .collect::<HashSet<_>>();
2124 assert_eq!(candidates.len(), 3);
2125 assert_eq!(
2126 candidates,
2127 [
2128 files[1].file_id().file_id(),
2129 files[3].file_id().file_id(),
2130 files[4].file_id().file_id()
2131 ]
2132 .into_iter()
2133 .collect::<HashSet<_>>()
2134 );
2135 }
2136
2137 type ExpectedWindowSpec = (i64, bool, Vec<(i64, i64)>);
2139
2140 fn check_assign_to_windows_with_overlapping(
2141 file_time_ranges: &[(i64, i64)],
2142 time_window: i64,
2143 expected_files: &[ExpectedWindowSpec],
2144 ) {
2145 let files: Vec<_> = (0..file_time_ranges.len())
2146 .map(|_| FileId::random())
2147 .collect();
2148
2149 let file_handles = files
2150 .iter()
2151 .zip(file_time_ranges.iter())
2152 .map(|(file_id, range)| new_file_handle(*file_id, range.0, range.1, 0))
2153 .collect::<Vec<_>>();
2154
2155 let windows = assign_to_windows(file_handles.iter(), time_window);
2156 assert_eq!(expected_files.len(), windows.len());
2157
2158 for (expected_window, overlapping, window_files) in expected_files {
2159 let actual_window = windows.get(expected_window).unwrap();
2160 let actual_overlapping = window_has_overlap(actual_window, &windows);
2161 assert_eq!(*overlapping, actual_overlapping);
2162 let mut file_ranges = actual_window
2163 .files
2164 .iter()
2165 .map(|f| {
2166 let (s, e) = f.time_range();
2167 (s.value(), e.value())
2168 })
2169 .collect::<Vec<_>>();
2170 file_ranges.sort_unstable_by(|l, r| l.0.cmp(&r.0).then(l.1.cmp(&r.1)));
2171 assert_eq!(window_files, &file_ranges);
2172 }
2173 }
2174
2175 #[test]
2176 fn test_assign_to_windows_with_overlapping() {
2177 check_assign_to_windows_with_overlapping(
2178 &[(0, 999), (1000, 1999), (2000, 2999)],
2179 2,
2180 &[
2181 (0, false, vec![(0, 999), (1000, 1999)]),
2182 (2, false, vec![(2000, 2999)]),
2183 ],
2184 );
2185
2186 check_assign_to_windows_with_overlapping(
2187 &[(0, 1), (0, 999), (100, 2999)],
2188 2,
2189 &[
2190 (0, true, vec![(0, 1), (0, 999)]),
2191 (2, true, vec![(100, 2999)]),
2192 ],
2193 );
2194
2195 check_assign_to_windows_with_overlapping(
2196 &[(0, 999), (1000, 1999), (2000, 2999), (3000, 3999)],
2197 2,
2198 &[
2199 (0, false, vec![(0, 999), (1000, 1999)]),
2200 (2, false, vec![(2000, 2999), (3000, 3999)]),
2201 ],
2202 );
2203
2204 check_assign_to_windows_with_overlapping(
2205 &[
2206 (0, 999),
2207 (1000, 1999),
2208 (2000, 2999),
2209 (3000, 3999),
2210 (0, 3999),
2211 ],
2212 2,
2213 &[
2214 (0, true, vec![(0, 999), (1000, 1999)]),
2215 (2, true, vec![(0, 3999), (2000, 2999), (3000, 3999)]),
2216 ],
2217 );
2218
2219 check_assign_to_windows_with_overlapping(
2220 &[
2221 (0, 999),
2222 (1000, 1999),
2223 (2000, 2999),
2224 (3000, 3999),
2225 (1999, 3999),
2226 ],
2227 2,
2228 &[
2229 (0, true, vec![(0, 999), (1000, 1999)]),
2230 (2, true, vec![(1999, 3999), (2000, 2999), (3000, 3999)]),
2231 ],
2232 );
2233
2234 check_assign_to_windows_with_overlapping(
2235 &[
2236 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (2999, 3999), ],
2242 2,
2243 &[
2244 (0, false, vec![(0, 999), (1000, 1999)]),
2246 (2, false, vec![(2000, 2999), (2999, 3999), (3000, 3999)]),
2247 ],
2248 );
2249
2250 check_assign_to_windows_with_overlapping(
2251 &[
2252 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (0, 2000), ],
2258 2,
2259 &[
2260 (0, true, vec![(0, 999), (1000, 1999)]),
2261 (2, true, vec![(0, 2000), (2000, 2999), (3000, 3999)]),
2262 ],
2263 );
2264 }
2265
2266 #[test]
2267 fn test_assign_to_windows_not_overlapping_when_pk_disjoint() {
2268 let files = [
2269 new_file_handle_with_size_sequence_and_primary_key_range(
2270 FileId::random(),
2271 0,
2272 1000,
2273 0,
2274 1,
2275 10,
2276 pk_range(b"a", b"f"),
2277 ),
2278 new_file_handle_with_size_sequence_and_primary_key_range(
2279 FileId::random(),
2280 500,
2281 2999,
2282 0,
2283 2,
2284 10,
2285 pk_range(b"x", b"z"),
2286 ),
2287 ];
2288
2289 let windows = assign_to_windows(files.iter(), 2);
2290
2291 assert_eq!(2, windows.len());
2292 let overlapping = window_has_overlap(windows.get(&0).unwrap(), &windows);
2293 assert!(!overlapping);
2294 }
2295
2296 #[test]
2297 fn test_assign_to_windows_pk_unknown_in_earlier_window_does_not_poison_later_windows() {
2298 let files = [
2299 new_file_handle(FileId::random(), 0, 1999, 0),
2300 new_file_handle_with_size_sequence_and_primary_key_range(
2301 FileId::random(),
2302 2000,
2303 3999,
2304 0,
2305 1,
2306 10,
2307 pk_range(b"a", b"f"),
2308 ),
2309 new_file_handle_with_size_sequence_and_primary_key_range(
2310 FileId::random(),
2311 3000,
2312 4999,
2313 0,
2314 2,
2315 10,
2316 pk_range(b"x", b"z"),
2317 ),
2318 ];
2319
2320 let windows = assign_to_windows(files.iter(), 2);
2321
2322 let overlapping = window_has_overlap(windows.get(&4).unwrap(), &windows);
2323 assert!(!overlapping);
2324 }
2325
2326 struct CompactionPickerTestCase {
2327 window_size: i64,
2328 input_files: Vec<FileHandle>,
2329 expected_outputs: Vec<ExpectedOutput>,
2330 }
2331
2332 impl CompactionPickerTestCase {
2333 async fn check(&self) {
2334 let file_id_to_idx = self
2335 .input_files
2336 .iter()
2337 .enumerate()
2338 .map(|(idx, file)| (file.file_id(), idx))
2339 .collect::<HashMap<_, _>>();
2340 let windows = assign_to_windows(self.input_files.iter(), self.window_size);
2341 let active_window =
2342 find_active_window_by_sequence(self.input_files.iter(), self.window_size).or_else(
2343 || find_latest_window_in_seconds(self.input_files.iter(), self.window_size),
2344 );
2345 let output = TwcsPicker {
2346 trigger_file_num: 2,
2347 active_window_l1_merge_trigger: 8,
2348 inactive_window_trigger_file_num: 2,
2349 inactive_window_l1_merge_trigger: 2,
2350 time_window_seconds: None,
2351 max_output_file_size: None,
2352 append_mode: false,
2353 merge_mode: MergeMode::LastRow,
2354 max_background_tasks: None,
2355 time_range: None,
2356 all_windows_inactive: false,
2357 metrics: Default::default(),
2358 }
2359 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2360 .await
2361 .unwrap();
2362
2363 let output = output
2364 .iter()
2365 .map(|o| {
2366 let input_file_ids = o
2367 .inputs
2368 .iter()
2369 .map(|f| file_id_to_idx.get(&f.file_id()).copied().unwrap())
2370 .collect::<HashSet<_>>();
2371 (input_file_ids, o.output_level)
2372 })
2373 .collect::<Vec<_>>();
2374
2375 let expected = self
2376 .expected_outputs
2377 .iter()
2378 .map(|o| {
2379 let input_file_ids = o.input_files.iter().copied().collect::<HashSet<_>>();
2380 (input_file_ids, o.output_level)
2381 })
2382 .collect::<Vec<_>>();
2383 assert_eq!(expected, output);
2384 }
2385 }
2386
2387 struct ExpectedOutput {
2388 input_files: Vec<usize>,
2389 output_level: Level,
2390 }
2391
2392 #[tokio::test]
2393 async fn test_newer_windows_are_placed_last_for_execution() {
2394 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2395
2396 CompactionPickerTestCase {
2398 window_size: 3,
2399 input_files: [
2400 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2401 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2402 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3), new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4), ]
2405 .to_vec(),
2406 expected_outputs: vec![
2407 ExpectedOutput {
2408 input_files: vec![0, 1],
2409 output_level: 1,
2410 },
2411 ExpectedOutput {
2412 input_files: vec![2, 3],
2413 output_level: 1,
2414 },
2415 ],
2416 }
2417 .check()
2418 .await;
2419
2420 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2427 CompactionPickerTestCase {
2428 window_size: 3,
2429 input_files: [
2430 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2431 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2432 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3),
2433 new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4),
2434 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 5),
2435 ]
2436 .to_vec(),
2437 expected_outputs: vec![
2438 ExpectedOutput {
2439 input_files: vec![0, 1],
2440 output_level: 1,
2441 },
2442 ExpectedOutput {
2443 input_files: vec![2, 3, 4],
2444 output_level: 1,
2445 },
2446 ],
2447 }
2448 .check()
2449 .await;
2450
2451 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2456 CompactionPickerTestCase {
2457 window_size: 3,
2458 input_files: [
2459 new_file_handle_with_sequence(file_ids[0], 0, 2999, 1, 1),
2460 new_file_handle_with_sequence(file_ids[1], 0, 2998, 1, 1),
2461 new_file_handle_with_sequence(file_ids[2], 3000, 5999, 1, 2),
2462 new_file_handle_with_sequence(file_ids[3], 3000, 5000, 1, 2),
2463 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 3),
2464 ]
2465 .to_vec(),
2466 expected_outputs: vec![ExpectedOutput {
2467 input_files: vec![2, 3],
2468 output_level: 1,
2469 }],
2470 }
2471 .check()
2472 .await;
2473 }
2474
2475 #[tokio::test]
2476 async fn test_build_output_skips_pk_disjoint_files() {
2477 let files = [
2478 new_file_handle_with_size_sequence_and_primary_key_range(
2479 FileId::random(),
2480 0,
2481 2999,
2482 0,
2483 1,
2484 10,
2485 pk_range(b"a", b"f"),
2486 ),
2487 new_file_handle_with_size_sequence_and_primary_key_range(
2488 FileId::random(),
2489 50,
2490 2998,
2491 0,
2492 2,
2493 10,
2494 pk_range(b"x", b"z"),
2495 ),
2496 ];
2497 let windows = assign_to_windows(files.iter(), 3);
2498 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2499 let output = 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: None,
2505 max_output_file_size: None,
2506 append_mode: false,
2507 merge_mode: MergeMode::LastRow,
2508 max_background_tasks: None,
2509 time_range: None,
2510 all_windows_inactive: false,
2511 metrics: Default::default(),
2512 }
2513 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2514 .await
2515 .unwrap();
2516
2517 assert!(output.is_empty());
2518 }
2519
2520 #[test]
2521 fn test_append_mode_filter_large_files() {
2522 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2523 let max_output_file_size = 1000u64;
2524
2525 let small_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 500);
2527 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 1500);
2528 let small_file_2 = new_file_handle_with_size_and_sequence(file_ids[2], 0, 999, 0, 3, 800);
2529 let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[3], 0, 999, 0, 4, 2000);
2530
2531 let mut files_to_merge = vec![small_file_1, large_file_1, small_file_2, large_file_2];
2532
2533 let original_count = files_to_merge.len();
2535
2536 files_to_merge.retain(|file| file.size() <= max_output_file_size);
2538
2539 assert_eq!(files_to_merge.len(), 2);
2541 assert_eq!(original_count, 4);
2542
2543 for file in &files_to_merge {
2545 assert!(
2546 file.size() <= max_output_file_size,
2547 "File size {} should be <= {}",
2548 file.size(),
2549 max_output_file_size
2550 );
2551 }
2552 }
2553
2554 #[tokio::test]
2555 async fn test_build_output_multiple_windows_with_zero_runs() {
2556 let file_ids = (0..7).map(|_| FileId::random()).collect::<Vec<_>>();
2557
2558 let files = [
2559 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2561 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2562 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2563 new_file_handle_with_sequence(file_ids[3], 3000, 3999, 0, 4),
2565 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2566 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2567 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2568 ];
2569
2570 let windows = assign_to_windows(files.iter(), 3);
2571
2572 let picker = TwcsPicker {
2574 trigger_file_num: 4, active_window_l1_merge_trigger: 8,
2576 inactive_window_trigger_file_num: 4,
2577 inactive_window_l1_merge_trigger: 8,
2578 time_window_seconds: Some(3),
2579 max_output_file_size: None,
2580 append_mode: false,
2581 merge_mode: MergeMode::LastRow,
2582 max_background_tasks: None,
2583 time_range: None,
2584 all_windows_inactive: false,
2585 metrics: Default::default(),
2586 };
2587
2588 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2589 let output = picker
2590 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2591 .await
2592 .unwrap();
2593
2594 assert!(
2595 !output.is_empty(),
2596 "Should have output from windows with runs, even when one window has 0 runs"
2597 );
2598
2599 let all_output_files: Vec<_> = output
2600 .iter()
2601 .flat_map(|o| o.inputs.iter())
2602 .map(|f| f.file_id().file_id())
2603 .collect();
2604
2605 assert!(
2606 all_output_files.contains(&file_ids[3])
2607 || all_output_files.contains(&file_ids[4])
2608 || all_output_files.contains(&file_ids[5]),
2609 "Output should contain files from the window with runs"
2610 );
2611 }
2612
2613 #[tokio::test]
2614 async fn test_build_output_single_window_zero_runs() {
2615 let file_ids = (0..2).map(|_| FileId::random()).collect::<Vec<_>>();
2616
2617 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];
2621
2622 let windows = assign_to_windows(files.iter(), 3);
2623
2624 let picker = TwcsPicker {
2625 trigger_file_num: 2,
2626 active_window_l1_merge_trigger: 8,
2627 inactive_window_trigger_file_num: 2,
2628 inactive_window_l1_merge_trigger: 8,
2629 time_window_seconds: Some(3),
2630 max_output_file_size: Some(1000),
2631 append_mode: true,
2632 merge_mode: MergeMode::LastRow,
2633 max_background_tasks: None,
2634 time_range: None,
2635 all_windows_inactive: false,
2636 metrics: Default::default(),
2637 };
2638
2639 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2640 let output = picker
2641 .build_output_with_time_range(RegionId::from_u64(456), windows, active_window, None)
2642 .await
2643 .unwrap();
2644
2645 assert!(
2647 output.is_empty(),
2648 "Should return empty output when no runs are found after filtering"
2649 );
2650 }
2651
2652 #[tokio::test]
2653 async fn test_append_mode_can_pick_remaining_single_level_files() {
2654 let files = [
2655 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100),
2656 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 100),
2657 new_file_handle_with_size_and_sequence(FileId::random(), 40, 49, 0, 3, 100),
2658 new_file_handle_with_size_and_sequence(FileId::random(), 60, 69, 0, 4, 2_000),
2659 ];
2660 let windows = assign_to_windows(files.iter(), 1);
2661 let picker = TwcsPicker {
2662 trigger_file_num: 4,
2663 active_window_l1_merge_trigger: 8,
2664 inactive_window_trigger_file_num: 4,
2665 inactive_window_l1_merge_trigger: 8,
2666 time_window_seconds: Some(1),
2667 max_output_file_size: Some(1_000),
2668 append_mode: true,
2669 merge_mode: MergeMode::LastRow,
2670 max_background_tasks: None,
2671 time_range: None,
2672 all_windows_inactive: false,
2673 metrics: Default::default(),
2674 };
2675
2676 let output = picker
2677 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
2678 .await
2679 .unwrap();
2680
2681 assert_eq!(1, output.len());
2682 assert_eq!(3, output[0].inputs.len());
2683 }
2684
2685 #[tokio::test]
2686 async fn test_max_background_tasks_truncation() {
2687 let file_ids = (0..10).map(|_| FileId::random()).collect::<Vec<_>>();
2688 let max_background_tasks = 3;
2689
2690 let files = [
2692 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2694 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2695 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2696 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2697 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2699 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2700 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2701 new_file_handle_with_sequence(file_ids[7], 3000, 3999, 0, 8),
2702 new_file_handle_with_sequence(file_ids[8], 6000, 6999, 0, 9),
2704 new_file_handle_with_sequence(file_ids[9], 6000, 6999, 0, 10),
2705 ];
2706
2707 let windows = assign_to_windows(files.iter(), 3);
2708
2709 let picker = TwcsPicker {
2710 trigger_file_num: 4,
2711 active_window_l1_merge_trigger: 8,
2712 inactive_window_trigger_file_num: 4,
2713 inactive_window_l1_merge_trigger: 8,
2714 time_window_seconds: Some(3),
2715 max_output_file_size: None,
2716 append_mode: false,
2717 merge_mode: MergeMode::LastRow,
2718 max_background_tasks: Some(max_background_tasks),
2719 time_range: None,
2720 all_windows_inactive: false,
2721 metrics: Default::default(),
2722 };
2723
2724 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2725 let output = picker
2726 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2727 .await
2728 .unwrap();
2729
2730 assert!(
2732 output.len() <= max_background_tasks,
2733 "Output should be truncated to max_background_tasks: expected <= {}, got {}",
2734 max_background_tasks,
2735 output.len()
2736 );
2737
2738 let picker_no_limit = TwcsPicker {
2740 trigger_file_num: 4,
2741 active_window_l1_merge_trigger: 8,
2742 inactive_window_trigger_file_num: 4,
2743 inactive_window_l1_merge_trigger: 8,
2744 time_window_seconds: Some(3),
2745 max_output_file_size: None,
2746 append_mode: false,
2747 merge_mode: MergeMode::LastRow,
2748 max_background_tasks: None,
2749 time_range: None,
2750 all_windows_inactive: false,
2751 metrics: Default::default(),
2752 };
2753
2754 let windows_no_limit = assign_to_windows(files.iter(), 3);
2755 let output_no_limit = picker_no_limit
2756 .build_output_with_time_range(
2757 RegionId::from_u64(123),
2758 windows_no_limit,
2759 active_window,
2760 None,
2761 )
2762 .await
2763 .unwrap();
2764
2765 if output_no_limit.len() > max_background_tasks {
2767 assert!(
2768 output_no_limit.len() > output.len(),
2769 "Without limit should have more outputs than with limit"
2770 );
2771 }
2772 }
2773
2774 #[tokio::test]
2775 async fn test_max_background_tasks_no_truncation_when_under_limit() {
2776 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2777 let max_background_tasks = 10; let files = [
2781 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2782 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2783 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2784 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2785 ];
2786
2787 let windows = assign_to_windows(files.iter(), 3);
2788
2789 let picker = TwcsPicker {
2790 trigger_file_num: 4,
2791 active_window_l1_merge_trigger: 8,
2792 inactive_window_trigger_file_num: 4,
2793 inactive_window_l1_merge_trigger: 8,
2794 time_window_seconds: Some(3),
2795 max_output_file_size: None,
2796 append_mode: false,
2797 merge_mode: MergeMode::LastRow,
2798 max_background_tasks: Some(max_background_tasks),
2799 time_range: None,
2800 all_windows_inactive: false,
2801 metrics: Default::default(),
2802 };
2803
2804 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2805 let output = picker
2806 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2807 .await
2808 .unwrap();
2809
2810 assert!(
2812 output.len() <= max_background_tasks,
2813 "Output should be within limit"
2814 );
2815 assert!(!output.is_empty(), "Should have at least one output");
2817 }
2818
2819 #[tokio::test]
2820 async fn test_pick_multiple_runs() {
2821 common_telemetry::init_default_ut_logging();
2822
2823 let num_files = 8;
2824 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2825
2826 let files: Vec<_> = file_ids
2828 .iter()
2829 .enumerate()
2830 .map(|(idx, file_id)| {
2831 new_file_handle_with_size_and_sequence(
2832 *file_id,
2833 0,
2834 999,
2835 0,
2836 (idx + 1) as u64,
2837 1024 * 1024,
2838 )
2839 })
2840 .collect();
2841
2842 let windows = assign_to_windows(files.iter(), 3);
2843
2844 let picker = TwcsPicker {
2845 trigger_file_num: 4,
2846 active_window_l1_merge_trigger: 8,
2847 inactive_window_trigger_file_num: 4,
2848 inactive_window_l1_merge_trigger: 8,
2849 time_window_seconds: Some(3),
2850 max_output_file_size: None,
2851 append_mode: false,
2852 merge_mode: MergeMode::LastRow,
2853 max_background_tasks: None,
2854 time_range: None,
2855 all_windows_inactive: false,
2856 metrics: Default::default(),
2857 };
2858
2859 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2860 let output = picker
2861 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2862 .await
2863 .unwrap();
2864
2865 assert_eq!(1, output.len());
2866 assert_eq!(output[0].inputs.len(), num_files);
2867 }
2868
2869 #[tokio::test]
2870 async fn test_window_trigger_can_exceed_input_limit() {
2871 common_telemetry::init_default_ut_logging();
2872
2873 let num_files = 50;
2874 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2875
2876 let files: Vec<_> = file_ids
2878 .iter()
2879 .enumerate()
2880 .map(|(idx, file_id)| {
2881 new_file_handle_with_size_and_sequence(
2882 *file_id,
2883 (idx / 2 * 10) as i64,
2884 (idx / 2 * 10 + 5) as i64,
2885 0,
2886 (idx + 1) as u64,
2887 1024 * 1024,
2888 )
2889 })
2890 .collect();
2891
2892 let windows = assign_to_windows(files.iter(), 3);
2893
2894 let picker = TwcsPicker {
2895 trigger_file_num: num_files,
2896 active_window_l1_merge_trigger: 8,
2897 inactive_window_trigger_file_num: num_files,
2898 inactive_window_l1_merge_trigger: 8,
2899 time_window_seconds: Some(3),
2900 max_output_file_size: None,
2901 append_mode: false,
2902 merge_mode: MergeMode::LastRow,
2903 max_background_tasks: None,
2904 time_range: None,
2905 all_windows_inactive: false,
2906 metrics: Default::default(),
2907 };
2908
2909 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2910 let output = picker
2911 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2912 .await
2913 .unwrap();
2914
2915 assert_eq!(1, output.len());
2916 assert_eq!(output[0].inputs.len(), num_files.min(*MAX_INPUT_FILES));
2917 }
2918
2919 #[tokio::test]
2920 async fn test_limit_max_input_files_keeps_deletion_markers() {
2921 common_telemetry::init_default_ut_logging();
2922
2923 let mut files = vec![new_file_handle_with_size_and_sequence(
2926 FileId::random(),
2927 0,
2928 3_000_000,
2929 0,
2930 1,
2931 1024 * 1024 * 1024,
2932 )];
2933 files.extend((0..DEFAULT_MAX_INPUT_FILES as i64).map(|idx| {
2934 new_file_handle_with_size_and_sequence(
2935 FileId::random(),
2936 (idx + 1) * 10_000,
2937 (idx + 1) * 10_000 + 1_000,
2938 0,
2939 (idx + 2) as u64,
2940 1024,
2941 )
2942 }));
2943
2944 let windows = assign_to_windows(files.iter(), 3600);
2945
2946 let picker = TwcsPicker {
2947 trigger_file_num: 4,
2948 active_window_l1_merge_trigger: 8,
2949 inactive_window_trigger_file_num: 4,
2950 inactive_window_l1_merge_trigger: 8,
2951 time_window_seconds: Some(3600),
2952 max_output_file_size: None,
2953 append_mode: false,
2954 merge_mode: MergeMode::LastRow,
2955 max_background_tasks: None,
2956 time_range: None,
2957 all_windows_inactive: false,
2958 metrics: Default::default(),
2959 };
2960
2961 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2962 let output = picker
2963 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2964 .await
2965 .unwrap();
2966
2967 assert_eq!(1, output.len());
2968 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2971 assert!(
2972 !output[0].filter_deleted,
2973 "deletion markers must be kept once the file num limit drops files they may mask"
2974 );
2975 }
2976
2977 #[tokio::test]
2978 async fn test_limit_max_input_files_still_filters_without_overlap() {
2979 common_telemetry::init_default_ut_logging();
2980
2981 let files: Vec<_> = (0..DEFAULT_MAX_INPUT_FILES as i64 + 8)
2984 .map(|idx| {
2985 new_file_handle_with_size_and_sequence(
2986 FileId::random(),
2987 (idx + 1) * 10_000,
2988 (idx + 1) * 10_000 + 1_000,
2989 0,
2990 (idx + 1) as u64,
2991 1024,
2992 )
2993 })
2994 .collect();
2995
2996 let windows = assign_to_windows(files.iter(), 3600);
2997
2998 let picker = TwcsPicker {
2999 trigger_file_num: 4,
3000 active_window_l1_merge_trigger: 8,
3001 inactive_window_trigger_file_num: 4,
3002 inactive_window_l1_merge_trigger: 8,
3003 time_window_seconds: Some(3600),
3004 max_output_file_size: Some(1024 * 1024 * 1024),
3005 append_mode: false,
3006 merge_mode: MergeMode::LastRow,
3007 max_background_tasks: None,
3008 time_range: None,
3009 all_windows_inactive: false,
3010 metrics: Default::default(),
3011 };
3012
3013 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
3014 let output = picker
3015 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
3016 .await
3017 .unwrap();
3018
3019 assert_eq!(1, output.len());
3020 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
3021 assert!(output[0].filter_deleted);
3022 }
3023
3024 #[tokio::test]
3025 async fn test_newer_windows_have_priority() {
3026 let older_file_ids = [FileId::random(), FileId::random()];
3027 let newer_file_ids = [FileId::random(), FileId::random()];
3028 let files = [
3029 new_file_handle_with_sequence(older_file_ids[0], 1_000, 1_999, 0, 1),
3030 new_file_handle_with_sequence(older_file_ids[1], 1_000, 1_999, 0, 2),
3031 new_file_handle_with_sequence(newer_file_ids[0], 7_000, 7_999, 0, 3),
3032 new_file_handle_with_sequence(newer_file_ids[1], 7_000, 7_999, 0, 4),
3033 ];
3034 let windows = assign_to_windows(files.iter(), 3);
3035 let picker = TwcsPicker {
3036 trigger_file_num: 2,
3037 active_window_l1_merge_trigger: 8,
3038 inactive_window_trigger_file_num: 2,
3039 inactive_window_l1_merge_trigger: 8,
3040 time_window_seconds: Some(3),
3041 max_output_file_size: None,
3042 append_mode: false,
3043 merge_mode: MergeMode::LastRow,
3044 max_background_tasks: Some(1),
3045 time_range: None,
3046 all_windows_inactive: false,
3047 metrics: Default::default(),
3048 };
3049
3050 let output = picker
3051 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), None)
3052 .await
3053 .unwrap();
3054
3055 assert_eq!(1, output.len());
3056 assert_eq!(
3057 newer_file_ids.into_iter().collect::<HashSet<_>>(),
3058 output[0]
3059 .inputs
3060 .iter()
3061 .map(|file| file.file_id().file_id())
3062 .collect::<HashSet<_>>()
3063 );
3064 }
3065
3066 #[test]
3067 fn test_filter_time_windows_by_time_range() {
3068 for (start, end, expected_windows) in [
3071 (1_200, 1_800, vec![0]),
3072 (0, 3_000, vec![0]),
3073 (3_000, 6_000, vec![3]),
3074 (2_999, 3_000, vec![0]),
3075 (2_999, 3_001, vec![0, 3]),
3076 (3_000, 3_001, vec![3]),
3077 (3_001, 3_002, vec![3]),
3078 (-3_000, 0, vec![-3]),
3079 (-3_001, -2_999, vec![-6, -3]),
3080 (0, 0, vec![]),
3081 (1_500, 1_500, vec![]),
3082 ] {
3083 let time_range = TimestampRange::with_unit(start, end, TimeUnit::Millisecond).unwrap();
3084 let selected = [-6, -3, 0, 3, 6]
3085 .into_iter()
3086 .filter(|window| time_window_intersects_range(*window, 3, &time_range))
3087 .collect::<Vec<_>>();
3088 assert_eq!(expected_windows, selected, "{time_range}");
3089 }
3090 }
3091
3092 #[test]
3093 fn test_filter_time_windows_with_unbounded_and_extreme_ranges() {
3094 for (range, expected_windows) in [
3095 (TimestampRange::min_to_max(), vec![-3, 0, 3]),
3096 (
3097 TimestampRange::from_start(Timestamp::new_second(0)),
3098 vec![0, 3],
3099 ),
3100 (
3101 TimestampRange::until_end(Timestamp::new_second(0), false),
3102 vec![-3],
3103 ),
3104 ] {
3105 let selected = [-3, 0, 3]
3106 .into_iter()
3107 .filter(|window| time_window_intersects_range(*window, 3, &range))
3108 .collect::<Vec<_>>();
3109 assert_eq!(expected_windows, selected, "{range}");
3110 }
3111
3112 let upper_range = TimestampRange::new(
3113 Timestamp::new_second(i64::MAX - 1),
3114 Timestamp::new_second(i64::MAX),
3115 )
3116 .unwrap();
3117 assert!(!time_window_intersects_range(0, 4, &upper_range));
3118 assert!(time_window_intersects_range(i64::MAX - 3, 4, &upper_range));
3119
3120 let lower_range = TimestampRange::until_end(Timestamp::new_second(i64::MIN), false);
3121 assert!(!time_window_intersects_range(i64::MIN, 3, &lower_range));
3122 }
3123
3124 #[tokio::test]
3125 async fn test_time_range_filter_precedes_background_task_limit() {
3126 let early_file_ids = [FileId::random(), FileId::random()];
3127 let selected_file_ids = [FileId::random(), FileId::random()];
3128 let files = [
3129 new_file_handle_with_sequence(early_file_ids[0], 1_000, 1_999, 0, 1),
3130 new_file_handle_with_sequence(early_file_ids[1], 1_000, 1_999, 0, 2),
3131 new_file_handle_with_sequence(selected_file_ids[0], 7_000, 7_999, 0, 3),
3132 new_file_handle_with_sequence(selected_file_ids[1], 7_000, 7_999, 0, 4),
3133 ];
3134 let windows = assign_to_windows(files.iter(), 3);
3135 let picker = TwcsPicker {
3136 trigger_file_num: 2,
3137 active_window_l1_merge_trigger: 8,
3138 inactive_window_trigger_file_num: 2,
3139 inactive_window_l1_merge_trigger: 8,
3140 time_window_seconds: Some(3),
3141 max_output_file_size: None,
3142 append_mode: false,
3143 merge_mode: MergeMode::LastRow,
3144 max_background_tasks: Some(1),
3145 time_range: TimestampRange::new(
3146 Timestamp::new_millisecond(7_200),
3147 Timestamp::new_millisecond(7_800),
3148 ),
3149 all_windows_inactive: false,
3150 metrics: Default::default(),
3151 };
3152
3153 let output = picker
3154 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), Some(3))
3155 .await
3156 .unwrap();
3157
3158 assert_eq!(1, output.len());
3159 assert_eq!(
3160 selected_file_ids.into_iter().collect::<HashSet<_>>(),
3161 output[0]
3162 .inputs
3163 .iter()
3164 .map(|file| file.file_id().file_id())
3165 .collect::<HashSet<_>>()
3166 );
3167 }
3168
3169 #[tokio::test]
3170 async fn test_inactive_window_uses_its_trigger_file_num() {
3171 let files = [
3172 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3173 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
3174 ];
3175 let windows = assign_to_windows(files.iter(), 100);
3176 let picker = TwcsPicker {
3177 trigger_file_num: 4,
3178 active_window_l1_merge_trigger: 8,
3179 inactive_window_trigger_file_num: 2,
3180 inactive_window_l1_merge_trigger: 8,
3181 time_window_seconds: Some(100),
3182 max_output_file_size: None,
3183 append_mode: false,
3184 merge_mode: MergeMode::LastRow,
3185 max_background_tasks: None,
3186 time_range: None,
3187 all_windows_inactive: false,
3188 metrics: Default::default(),
3189 };
3190
3191 let output = picker
3192 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3193 .await
3194 .unwrap();
3195
3196 assert_eq!(1, output.len());
3197 assert_eq!(2, output[0].inputs.len());
3198 }
3199
3200 #[tokio::test]
3201 async fn test_inactive_window_requires_a_level_trigger_before_mixed_fallback() {
3202 let files = [
3203 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3204 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 1, 2, 10),
3205 ];
3206 let windows = assign_to_windows(files.iter(), 100);
3207 let picker = TwcsPicker {
3208 trigger_file_num: 4,
3209 active_window_l1_merge_trigger: 8,
3210 inactive_window_trigger_file_num: 8,
3211 inactive_window_l1_merge_trigger: 2,
3212 time_window_seconds: Some(100),
3213 max_output_file_size: Some(1_000),
3214 append_mode: false,
3215 merge_mode: MergeMode::LastRow,
3216 max_background_tasks: None,
3217 time_range: None,
3218 all_windows_inactive: false,
3219 metrics: Default::default(),
3220 };
3221
3222 let output = picker
3223 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3224 .await
3225 .unwrap();
3226
3227 assert!(output.is_empty());
3228 }
3229
3230 #[tokio::test]
3231 async fn test_count_first_prefers_more_files_over_smaller_overlap() {
3232 let files = [
3233 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3234 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
3235 new_file_handle_with_size_and_sequence(FileId::random(), 40, 50, 0, 3, 10),
3236 new_file_handle_with_size_and_sequence(FileId::random(), 5, 15, 0, 4, 10),
3237 ];
3238 let windows = assign_to_windows(files.iter(), 100);
3239 let picker = TwcsPicker {
3240 trigger_file_num: 2,
3241 active_window_l1_merge_trigger: 8,
3242 inactive_window_trigger_file_num: 2,
3243 inactive_window_l1_merge_trigger: 8,
3244 time_window_seconds: Some(100),
3245 max_output_file_size: None,
3246 append_mode: false,
3247 merge_mode: MergeMode::LastRow,
3248 max_background_tasks: None,
3249 time_range: None,
3250 all_windows_inactive: false,
3251 metrics: Default::default(),
3252 };
3253
3254 let output = picker
3255 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3256 .await
3257 .unwrap();
3258
3259 assert_eq!(1, output.len());
3260 assert_eq!(4, output[0].inputs.len());
3261 }
3262
3263 #[tokio::test]
3264 async fn test_count_first_trigger_counts_physical_ssts() {
3265 let files = [
3266 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3267 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3268 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
3269 ];
3270 let windows = assign_to_windows(files.iter(), 100);
3271 let picker = TwcsPicker {
3272 trigger_file_num: 3,
3273 active_window_l1_merge_trigger: 8,
3274 inactive_window_trigger_file_num: 3,
3275 inactive_window_l1_merge_trigger: 8,
3276 time_window_seconds: Some(100),
3277 max_output_file_size: None,
3278 append_mode: false,
3279 merge_mode: MergeMode::LastRow,
3280 max_background_tasks: None,
3281 time_range: None,
3282 all_windows_inactive: false,
3283 metrics: Default::default(),
3284 };
3285
3286 let output = picker
3287 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3288 .await
3289 .unwrap();
3290
3291 assert_eq!(1, output.len());
3292 assert_eq!(3, output[0].inputs.len());
3293 }
3294
3295 #[tokio::test]
3296 async fn test_count_first_does_not_compact_overlap_below_trigger() {
3297 let files = [
3298 new_file_handle_with_size_and_sequence(FileId::random(), 0, 20, 0, 1, 10),
3299 new_file_handle_with_size_and_sequence(FileId::random(), 10, 30, 0, 2, 10),
3300 ];
3301 let windows = assign_to_windows(files.iter(), 100);
3302 let picker = TwcsPicker {
3303 trigger_file_num: 3,
3304 active_window_l1_merge_trigger: 8,
3305 inactive_window_trigger_file_num: 3,
3306 inactive_window_l1_merge_trigger: 8,
3307 time_window_seconds: Some(100),
3308 max_output_file_size: None,
3309 append_mode: false,
3310 merge_mode: MergeMode::LastRow,
3311 max_background_tasks: None,
3312 time_range: None,
3313 all_windows_inactive: false,
3314 metrics: Default::default(),
3315 };
3316
3317 let output = picker
3318 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3319 .await
3320 .unwrap();
3321
3322 assert!(output.is_empty());
3323 }
3324
3325 #[tokio::test]
3326 async fn test_filter_deleted_is_false_when_selected_files_overlap_unselected_file() {
3327 let mut files = (0..32)
3328 .map(|idx| {
3329 new_file_handle_with_size_and_sequence(
3330 FileId::random(),
3331 idx * 10,
3332 idx * 10 + 9,
3333 0,
3334 idx as u64 + 1,
3335 10,
3336 )
3337 })
3338 .collect::<Vec<_>>();
3339 files.push(new_file_handle_with_size_and_sequence(
3340 FileId::random(),
3341 0,
3342 320,
3343 0,
3344 33,
3345 10,
3346 ));
3347 let windows = assign_to_windows(files.iter(), 1000);
3348 let picker = TwcsPicker {
3349 trigger_file_num: 2,
3350 active_window_l1_merge_trigger: 8,
3351 inactive_window_trigger_file_num: 2,
3352 inactive_window_l1_merge_trigger: 8,
3353 time_window_seconds: Some(1000),
3354 max_output_file_size: None,
3355 append_mode: false,
3356 merge_mode: MergeMode::LastRow,
3357 max_background_tasks: None,
3358 time_range: None,
3359 all_windows_inactive: false,
3360 metrics: Default::default(),
3361 };
3362
3363 let output = picker
3364 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3365 .await
3366 .unwrap();
3367
3368 assert_eq!(1, output.len());
3369 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
3370 assert!(!output[0].filter_deleted);
3371 }
3372
3373 #[rstest::rstest]
3374 #[case::historical_l1(0, PickClass::FileReduction, 1)]
3375 #[case::same_window_l1(10, PickClass::FileReduction, 1)]
3376 #[case::older_l0(0, PickClass::L0Merge, 0)]
3377 #[case::historical_overlap(0, PickClass::OverlapResolution, 1)]
3378 #[tokio::test]
3379 async fn test_fair_picker_promotes_waiting_work_with_one_slot(
3380 #[case] waiting_window: i64,
3381 #[case] class: PickClass,
3382 #[case] level: Level,
3383 ) {
3384 let overlap = class == PickClass::OverlapResolution;
3385 let files = priority_test_files(10, 0, 40, false)
3386 .into_iter()
3387 .chain(priority_test_files(
3388 waiting_window,
3389 level,
3390 if overlap { 100 } else { 40 },
3391 overlap,
3392 ))
3393 .collect::<Vec<_>>();
3394 let mut history = PickHistory::default();
3395 let picker = priority_test_picker(1);
3396 let first = picker
3397 .build_output_with_history(assign_to_windows(files.iter(), 1), Some(10), &mut history)
3398 .await
3399 .unwrap();
3400 assert_eq!(1, first.len());
3401 assert!(first[0].inputs.iter().all(|f| f.level() == 0));
3402 assert_eq!(10, output_window(&first[0]));
3403
3404 let key = PickKey {
3407 window: waiting_window,
3408 class,
3409 };
3410 history = PickHistory::default();
3411 history.observe_for_test([key].into_iter(), Instant::now() - Duration::from_secs(120));
3412 let output = picker
3413 .build_output_with_history(assign_to_windows(files.iter(), 1), Some(10), &mut history)
3414 .await
3415 .unwrap();
3416 assert_eq!(1, output.len());
3417 assert_eq!(waiting_window, output_window(&output[0]));
3418 assert!(output[0].inputs.iter().all(|f| f.level() == level));
3419 let expected: HashSet<_> = files
3420 .iter()
3421 .filter(|f| f.level() == level && f.time_range().1.value() / 1000 == waiting_window)
3422 .map(FileHandle::file_id)
3423 .collect();
3424 assert_eq!(
3425 expected,
3426 output[0].inputs.iter().map(FileHandle::file_id).collect()
3427 );
3428 }
3429
3430 #[tokio::test]
3431 async fn test_overdue_candidates_alternate_without_completion() {
3432 let files = priority_test_files(0, 0, 40, false)
3436 .into_iter()
3437 .chain(priority_test_files(10, 0, 40, false))
3438 .collect::<Vec<_>>();
3439 let mut region = compaction_region_with_ssts(
3440 files.iter().map(|file| file.meta_ref().clone()),
3441 Duration::from_secs(60),
3442 )
3443 .await;
3444 region.ttl = None;
3445 let crate::region::options::CompactionOptions::Twcs(options) =
3446 &mut region.region_options.compaction;
3447 options.time_window = Some(Duration::from_secs(1));
3448 options.active_window_trigger_file_num = 2;
3449 let mut history = PickHistory::default();
3450 let start = Instant::now();
3451 history.seed_for_test(
3452 PickKey {
3453 window: 0,
3454 class: PickClass::L0Merge,
3455 },
3456 start - Duration::from_secs(120),
3457 );
3458 let younger = PickKey {
3459 window: 10,
3460 class: PickClass::L0Merge,
3461 };
3462 let waiting_since = start - Duration::from_secs(90);
3463 history.seed_for_test(younger, waiting_since);
3464
3465 let first = pick_automatic_for_test(®ion, &mut history, Some(1))
3468 .await
3469 .unwrap()
3470 .unwrap();
3471 assert_eq!(1, first.outputs.len());
3472 assert_eq!(0, output_window(&first.outputs[0]));
3473 assert_eq!(
3474 Some(waiting_since),
3475 history.promoted_since_for_test(younger, start)
3476 );
3477
3478 let second = pick_automatic_for_test(®ion, &mut history, Some(1))
3481 .await
3482 .unwrap()
3483 .unwrap();
3484 assert_eq!(1, second.outputs.len());
3485 assert_eq!(10, output_window(&second.outputs[0]));
3486 }
3487
3488 #[rstest::rstest]
3489 #[case::stateless_early_exit(false, false, true)]
3490 #[case::fair_full_scan(true, false, true)]
3491 #[case::promoted_selection(true, true, true)]
3492 #[case::empty_plan(true, false, false)]
3493 #[tokio::test]
3494 async fn test_pick_metrics_cover_scanned_windows_and_selected_seeds(
3495 #[case] fair: bool,
3496 #[case] promoted: bool,
3497 #[case] eligible: bool,
3498 ) {
3499 let mut files = priority_test_files(10, 0, 40, false).to_vec();
3500 files.extend(priority_test_files(10, 1, 40, false));
3501 files.extend(priority_test_files(0, 1, 40, false));
3502 if !eligible {
3503 files = vec![files[0].clone(), files[4].clone()];
3504 }
3505 let selected_metrics = IntCounterVec::new(
3507 prometheus::Opts::new("test_twcs_selected", "selected seeds"),
3508 &["class", "priority"],
3509 )
3510 .unwrap();
3511 let picker = TwcsPicker {
3512 metrics: TwcsPickMetrics {
3513 elapsed: HistogramVec::new(
3514 prometheus::HistogramOpts::new("test_twcs_pick_elapsed", "pick elapsed"),
3515 &["mode"],
3516 )
3517 .unwrap(),
3518 scanned_windows: IntCounterVec::new(
3519 prometheus::Opts::new("test_twcs_scanned_windows", "scanned windows"),
3520 &["mode"],
3521 )
3522 .unwrap(),
3523 },
3524 ..priority_test_picker(1)
3525 };
3526 let mut history = PickHistory::with_selected_metrics_for_test(selected_metrics.clone());
3527 if fair && promoted {
3528 history.observe_for_test(
3529 [PickKey {
3530 window: 0,
3531 class: PickClass::FileReduction,
3532 }]
3533 .into_iter(),
3534 Instant::now() - Duration::from_secs(120),
3535 );
3536 }
3537 let mode = if fair { "fair" } else { "stateless" };
3538 let elapsed = picker.metrics.elapsed.with_label_values(&[mode]);
3539 let windows = picker.metrics.scanned_windows.with_label_values(&[mode]);
3540 let elapsed_before = elapsed.get_sample_count();
3541 let windows_before = windows.get();
3542 let selected = [
3543 PickClass::L0Merge,
3544 PickClass::FileReduction,
3545 PickClass::OverlapResolution,
3546 ]
3547 .into_iter()
3548 .flat_map(|class| {
3549 let selected_metrics = &selected_metrics;
3550 ["normal", "promoted"].into_iter().map(move |priority| {
3551 let counter = selected_metrics.with_label_values(&[class.as_str(), priority]);
3552 let before = counter.get();
3553 (class, priority, counter, before)
3554 })
3555 })
3556 .collect::<Vec<_>>();
3557
3558 let output = if fair {
3559 picker
3560 .build_output_with_history(
3561 assign_to_windows(files.iter(), 1),
3562 Some(10),
3563 &mut history,
3564 )
3565 .await
3566 .unwrap()
3567 } else {
3568 picker
3569 .build_output_with_time_range(
3570 RegionId::from_u64(1),
3571 assign_to_windows(files.iter(), 1),
3572 Some(10),
3573 None,
3574 )
3575 .await
3576 .unwrap()
3577 };
3578 assert_eq!(usize::from(eligible), output.len());
3579 if eligible {
3580 assert_eq!(if promoted { 0 } else { 10 }, output_window(&output[0]));
3581 }
3582 assert_eq!(elapsed_before + 1, elapsed.get_sample_count());
3583 assert_eq!(windows_before + if fair { 2 } else { 1 }, windows.get());
3584 for (class, priority, counter, before) in selected {
3588 let chosen = if promoted {
3589 (PickClass::FileReduction, "promoted")
3590 } else {
3591 (PickClass::L0Merge, "normal")
3592 };
3593 let expected = u64::from(fair && eligible && (class, priority) == chosen);
3594 assert_eq!(before + expected, counter.get(), "{class:?}/{priority}");
3595 }
3596 }
3597
3598 #[tokio::test]
3599 async fn test_fair_inactive_closed_merge_absorbs_stranded_l0() {
3600 let files = [
3603 new_file_handle_with_size_and_sequence(FileId::random(), 0, 499, 0, 1, 100),
3604 new_file_handle_with_size_and_sequence(FileId::random(), 250, 749, 1, 1, 100),
3605 ];
3606 let picker = priority_test_picker(1);
3607 let mut history = PickHistory::default();
3608 let output = picker
3609 .build_output_with_history(assign_to_windows(files.iter(), 1), Some(10), &mut history)
3610 .await
3611 .unwrap();
3612 assert_eq!(1, output.len());
3613 assert_eq!(
3614 files
3615 .iter()
3616 .map(FileHandle::file_id)
3617 .collect::<HashSet<_>>(),
3618 output[0].inputs.iter().map(FileHandle::file_id).collect()
3619 );
3620 }
3621
3622 #[rstest::rstest]
3623 #[case::stranded_l0(1)]
3624 #[case::eligible_l0_merge(2)]
3625 #[tokio::test]
3626 async fn test_fair_closed_merge_does_not_add_conflicts_with_l0(#[case] l0_count: usize) {
3627 let mut files = vec![
3631 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 1, 40),
3632 new_file_handle_with_size_and_sequence(FileId::random(), 200, 299, 1, 1, 40),
3633 ];
3634 files.extend((0..l0_count).map(|_| {
3635 new_file_handle_with_size_and_sequence(FileId::random(), 100, 199, 0, 1, 1000)
3636 }));
3637 let mut history = PickHistory::default();
3638 history.observe_for_test(
3639 [PickKey {
3640 window: 0,
3641 class: PickClass::FileReduction,
3642 }]
3643 .into_iter(),
3644 Instant::now() - Duration::from_secs(120),
3645 );
3646 let picker = priority_test_picker(1);
3647 let output = picker
3648 .build_output_with_history(assign_to_windows(files.iter(), 1), Some(10), &mut history)
3649 .await
3650 .unwrap();
3651 if l0_count == 1 {
3652 assert!(output.is_empty());
3653 } else {
3654 assert_eq!(1, output.len());
3655 assert_eq!(
3656 files[2..]
3657 .iter()
3658 .map(FileHandle::file_id)
3659 .collect::<HashSet<_>>(),
3660 output[0].inputs.iter().map(FileHandle::file_id).collect()
3661 );
3662 }
3663 }
3664
3665 #[rstest::rstest]
3666 #[case::active(0, MergeMode::LastRow, "active:pick_l1_file_reduction")]
3667 #[case::inactive(10, MergeMode::LastRow, "inactive:pick_l1_file_reduction")]
3668 #[case::settling_fallback(1, MergeMode::LastRow, "inactive:pick_small_l1")]
3669 #[case::settling_last_non_null(1, MergeMode::LastNonNull, "")]
3670 fn test_window_discovery_keeps_l1_alongside_l0(
3671 #[case] active_window: i64,
3672 #[case] merge_mode: MergeMode,
3673 #[case] l1_branch: &str,
3674 ) {
3675 let files = priority_test_files(0, 0, 40, false)
3676 .into_iter()
3677 .chain(priority_test_files(0, 1, 20, true))
3678 .collect::<Vec<_>>();
3679 let windows = assign_to_windows(files.iter(), 1);
3680 let mapper = primary_key_mapper_for_test();
3681 let mut context = WindowPickContext {
3682 all_candidates: false,
3683 region_id: RegionId::from_u64(1),
3684 active_window: Some(active_window),
3685 time_window_size: Some(1),
3686 region_sequence: 100,
3687 mapper: &mapper,
3688 };
3689 let mut picker = priority_test_picker(4);
3690 picker.merge_mode = merge_mode;
3691 let legacy = picker.pick_window(&windows[&0], &context);
3692 assert_eq!(1, legacy.len());
3693 assert_eq!(PickClass::L0Merge, legacy[0].class);
3694
3695 context.all_candidates = true;
3696 let picks = picker.pick_window(&windows[&0], &context);
3697 assert_eq!(if l1_branch.is_empty() { 1 } else { 2 }, picks.len());
3698 assert_eq!(PickClass::L0Merge, picks[0].class);
3699 if !l1_branch.is_empty() {
3700 assert_eq!(PickClass::FileReduction, picks[1].class);
3701 assert_eq!(l1_branch, picks[1].branch);
3702 assert_eq!(
3703 files
3704 .iter()
3705 .filter(|file| active_window == 10 || file.level() == 1)
3708 .map(FileHandle::file_id)
3709 .collect::<HashSet<_>>(),
3710 picks[1]
3711 .inputs
3712 .iter()
3713 .map(FileHandle::file_id)
3714 .collect::<HashSet<_>>()
3715 );
3716 }
3717 }
3718
3719 #[tokio::test]
3720 async fn test_fair_picker_does_not_schedule_same_window_alternatives_together() {
3721 let files = priority_test_files(0, 0, 40, false)
3722 .into_iter()
3723 .chain(priority_test_files(0, 1, 40, false))
3724 .collect::<Vec<_>>();
3725 let picker = priority_test_picker(4);
3726 let mut history = PickHistory::default();
3727 let output = picker
3728 .build_output_with_history(assign_to_windows(files.iter(), 1), Some(0), &mut history)
3729 .await
3730 .unwrap();
3731 assert_eq!(1, output.len());
3732 assert!(output[0].inputs.iter().all(|f| f.level() == 0));
3733 assert!(
3734 !output[0].filter_deleted,
3735 "unselected overlapping L1 must retain tombstones"
3736 );
3737 }
3738
3739 fn new_file(start: i64, end: i64, sequence: u64, file_size: u64) -> FileHandle {
3740 new_file_handle_with_size_and_sequence(FileId::random(), start, end, 0, sequence, file_size)
3741 }
3742
3743 fn new_file_with_level_and_rows(
3744 start: i64,
3745 end: i64,
3746 level: Level,
3747 sequence: u64,
3748 file_size: u64,
3749 num_rows: u64,
3750 ) -> FileHandle {
3751 let file = new_file_handle_with_size_and_sequence(
3752 FileId::random(),
3753 start,
3754 end,
3755 level,
3756 sequence,
3757 file_size,
3758 );
3759 let mut meta = file.meta_ref().clone();
3760 meta.num_rows = num_rows;
3761 FileHandle::new(meta, crate::test_util::new_noop_file_purger())
3762 }
3763
3764 fn picked_ranges(files: &[FileHandle]) -> Vec<(i64, i64)> {
3765 files
3766 .iter()
3767 .map(|file| {
3768 let (start, end) = file.range();
3769 (start.value(), end.value())
3770 })
3771 .collect()
3772 }
3773
3774 #[test]
3775 fn test_count_first_rejects_dominant_historical_file() {
3776 let picked = pick_count_first(
3777 vec![SortedRun::from(vec![
3778 new_file(0, 9, 1, 400),
3779 new_file(20, 29, 2, 100),
3780 ])],
3781 None,
3782 );
3783
3784 assert!(picked.is_empty());
3785 }
3786
3787 #[test]
3788 fn test_count_first_accepts_balanced_historical_file() {
3789 let picked = pick_count_first(
3790 vec![SortedRun::from(vec![
3791 new_file(0, 9, 1, 400),
3792 new_file(20, 29, 2, 100),
3793 new_file(40, 49, 3, 100),
3794 new_file(60, 69, 4, 100),
3795 new_file(80, 89, 5, 100),
3796 ])],
3797 None,
3798 );
3799
3800 assert_eq!(
3801 vec![(0, 9), (20, 29), (40, 49), (60, 69), (80, 89)],
3802 picked_ranges(&picked)
3803 );
3804 }
3805
3806 #[test]
3807 fn test_count_first_finds_smaller_balanced_interval_when_larger_one_is_unbalanced() {
3808 let picked = pick_count_first(
3809 vec![SortedRun::from(vec![
3810 new_file(0, 9, 1, 1000),
3811 new_file(20, 29, 2, 100),
3812 new_file(40, 49, 3, 100),
3813 new_file(60, 69, 4, 100),
3814 new_file(80, 89, 5, 100),
3815 ])],
3816 None,
3817 );
3818
3819 assert_eq!(
3820 vec![(20, 29), (40, 49), (60, 69), (80, 89)],
3821 picked_ranges(&picked)
3822 );
3823 }
3824
3825 #[test]
3826 fn test_count_first_prefers_overlap_participants_when_file_counts_match() {
3827 let first_run = (0..DEFAULT_MAX_INPUT_FILES)
3828 .map(|idx| {
3829 let start = idx as i64 * 20;
3830 let end = if idx + 1 == DEFAULT_MAX_INPUT_FILES {
3831 700
3832 } else {
3833 start + 9
3834 };
3835 new_file(start, end, idx as u64 + 1, 10)
3836 })
3837 .collect::<Vec<_>>();
3838 let overlapping = new_file(690, 710, 100, 10);
3839
3840 let picked = pick_count_first(
3841 vec![
3842 SortedRun::from(first_run),
3843 SortedRun::from(vec![overlapping]),
3844 ],
3845 None,
3846 );
3847 let ranges = picked_ranges(&picked);
3848
3849 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3850 assert!(!ranges.contains(&(0, 9)));
3851 assert!(ranges.contains(&(690, 710)));
3852 }
3853
3854 #[tokio::test]
3855 async fn test_picker_avoids_chained_l1_rewrites_by_compacting_levels_separately() {
3856 let mut enough_l0 = (0..DEFAULT_MAX_INPUT_FILES)
3857 .map(|idx| {
3858 let start = idx as i64 * 20;
3859 new_file_handle_with_size_and_sequence(
3860 FileId::random(),
3861 start,
3862 start + 9,
3863 0,
3864 idx as u64 + 1,
3865 10,
3866 )
3867 })
3868 .collect::<Vec<_>>();
3869 enough_l0.push(new_file_handle_with_size_and_sequence(
3870 FileId::random(),
3871 0,
3872 700,
3873 1,
3874 100,
3875 100,
3876 ));
3877 let mut enough_l1 = (0..4)
3878 .map(|idx| {
3879 new_file_handle_with_size_and_sequence(
3880 FileId::random(),
3881 idx * 100,
3882 idx * 100 + 99,
3883 1,
3884 idx as u64 + 1,
3885 100,
3886 )
3887 })
3888 .collect::<Vec<_>>();
3889 enough_l1.extend((0..3).map(|idx| {
3890 new_file_handle_with_size_and_sequence(
3891 FileId::random(),
3892 idx * 20,
3893 idx * 20 + 9,
3894 0,
3895 idx as u64 + 10,
3896 10,
3897 )
3898 }));
3899 let picker = TwcsPicker {
3900 trigger_file_num: 4,
3901 active_window_l1_merge_trigger: 8,
3902 inactive_window_trigger_file_num: 4,
3903 inactive_window_l1_merge_trigger: 4,
3904 time_window_seconds: Some(1),
3905 max_output_file_size: None,
3906 append_mode: false,
3907 merge_mode: MergeMode::LastRow,
3908 max_background_tasks: None,
3909 time_range: None,
3910 all_windows_inactive: false,
3911 metrics: Default::default(),
3912 };
3913
3914 for (case, files, expected_level, expected_len) in [
3915 ("L0 reaches trigger", enough_l0, 0, DEFAULT_MAX_INPUT_FILES),
3916 ("L0 fallback precedes triggered L1", enough_l1, 0, 3),
3917 ] {
3918 let windows = assign_to_windows(files.iter(), 1);
3919 let output = picker
3920 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3921 .await
3922 .unwrap();
3923
3924 assert_eq!(1, output.len(), "{case}");
3925 assert_eq!(expected_len, output[0].inputs.len(), "{case}");
3926 assert!(
3927 output[0]
3928 .inputs
3929 .iter()
3930 .all(|file| file.level() == expected_level),
3931 "{case}"
3932 );
3933 }
3934 }
3935
3936 #[tokio::test]
3937 async fn test_active_window_defers_l1_below_safety_trigger() {
3938 let files = (0..7)
3939 .map(|idx| {
3940 let start = idx * 100;
3941 new_file_handle_with_size_and_sequence(
3942 FileId::random(),
3943 start,
3944 start + 99,
3945 1,
3946 idx as u64 + 1,
3947 100,
3948 )
3949 })
3950 .collect::<Vec<_>>();
3951 let windows = assign_to_windows(files.iter(), 1);
3952 let active_window = windows.keys().next().copied();
3953 let picker = TwcsPicker {
3954 trigger_file_num: 4,
3955 active_window_l1_merge_trigger: 8,
3956 inactive_window_trigger_file_num: 2,
3957 inactive_window_l1_merge_trigger: 8,
3958 time_window_seconds: Some(1),
3959 max_output_file_size: None,
3960 append_mode: false,
3961 merge_mode: MergeMode::LastRow,
3962 max_background_tasks: None,
3963 time_range: None,
3964 all_windows_inactive: false,
3965 metrics: Default::default(),
3966 };
3967
3968 let output = picker
3969 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3970 .await
3971 .unwrap();
3972
3973 assert!(output.is_empty());
3974 }
3975
3976 #[tokio::test]
3977 async fn test_active_window_l1_safety_trigger_is_independent_of_l0_trigger() {
3978 let files = (0..8)
3979 .map(|idx| {
3980 let start = idx * 100;
3981 new_file_handle_with_size_and_sequence(
3982 FileId::random(),
3983 start,
3984 start + 99,
3985 1,
3986 idx as u64 + 1,
3987 100,
3988 )
3989 })
3990 .collect::<Vec<_>>();
3991 let windows = assign_to_windows(files.iter(), 1);
3992 let active_window = windows.keys().next().copied();
3993 let picker = TwcsPicker {
3994 trigger_file_num: 16,
3995 active_window_l1_merge_trigger: 8,
3996 inactive_window_trigger_file_num: 2,
3997 inactive_window_l1_merge_trigger: 8,
3998 time_window_seconds: Some(1),
3999 max_output_file_size: None,
4000 append_mode: false,
4001 merge_mode: MergeMode::LastRow,
4002 max_background_tasks: None,
4003 time_range: None,
4004 all_windows_inactive: false,
4005 metrics: Default::default(),
4006 };
4007
4008 let output = picker
4009 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
4010 .await
4011 .unwrap();
4012
4013 assert_eq!(1, output.len());
4014 assert_eq!(8, output[0].inputs.len());
4015 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
4016 }
4017
4018 #[tokio::test]
4019 async fn test_picker_falls_back_to_l1_when_triggered_l0_cannot_make_progress() {
4020 let mut files = (0..4)
4021 .map(|idx| {
4022 let start = idx * 20;
4023 new_file_handle_with_size_and_sequence(
4024 FileId::random(),
4025 start,
4026 start + 9,
4027 0,
4028 idx as u64 + 1,
4029 600,
4030 )
4031 })
4032 .collect::<Vec<_>>();
4033 files.extend((0..8).map(|idx| {
4034 let start = idx * 20 + 100;
4035 new_file_handle_with_size_and_sequence(
4036 FileId::random(),
4037 start,
4038 start + 9,
4039 1,
4040 idx as u64 + 10,
4041 50,
4042 )
4043 }));
4044 let windows = assign_to_windows(files.iter(), 1);
4045 let active_window = windows.keys().next().copied();
4046 let picker = TwcsPicker {
4047 trigger_file_num: 4,
4048 active_window_l1_merge_trigger: 8,
4049 inactive_window_trigger_file_num: 4,
4050 inactive_window_l1_merge_trigger: 8,
4051 time_window_seconds: Some(1),
4052 max_output_file_size: Some(512),
4053 append_mode: false,
4054 merge_mode: MergeMode::LastRow,
4055 max_background_tasks: None,
4056 time_range: None,
4057 all_windows_inactive: false,
4058 metrics: Default::default(),
4059 };
4060
4061 let output = picker
4062 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
4063 .await
4064 .unwrap();
4065
4066 assert_eq!(1, output.len());
4067 assert_eq!(8, output[0].inputs.len());
4068 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
4069 }
4070
4071 fn priority_test_files(
4072 window: i64,
4073 level: Level,
4074 file_size: u64,
4075 overlap: bool,
4076 ) -> [FileHandle; 2] {
4077 let start = window * 1_000;
4078 let ranges = if overlap {
4079 [(start, start + 499), (start + 250, start + 749)]
4080 } else {
4081 [(start, start + 99), (start + 200, start + 299)]
4082 };
4083 ranges.map(|(start, end)| {
4084 new_file_handle_with_size_and_sequence(
4085 FileId::random(),
4086 start,
4087 end,
4088 level,
4089 window as u64 + 1,
4090 file_size,
4091 )
4092 })
4093 }
4094
4095 fn priority_test_picker(max_background_tasks: usize) -> TwcsPicker {
4096 TwcsPicker {
4097 trigger_file_num: 2,
4098 active_window_l1_merge_trigger: 2,
4099 inactive_window_trigger_file_num: 2,
4100 inactive_window_l1_merge_trigger: 2,
4101 time_window_seconds: Some(1),
4102 max_output_file_size: Some(100),
4103 append_mode: false,
4104 merge_mode: MergeMode::LastRow,
4105 max_background_tasks: Some(max_background_tasks),
4106 time_range: None,
4107 all_windows_inactive: false,
4108 metrics: Default::default(),
4109 }
4110 }
4111
4112 fn output_window(output: &CompactionOutput) -> i64 {
4113 output.inputs[0]
4114 .time_range()
4115 .1
4116 .convert_to(TimeUnit::Second)
4117 .unwrap()
4118 .value()
4119 }
4120
4121 #[tokio::test]
4122 async fn test_manual_compaction_resolves_active_window_overlaps() {
4123 let files = priority_test_files(5, 1, 100, true);
4124 for (all_windows_inactive, expected) in [(false, 0), (true, 1)] {
4125 let picker = TwcsPicker {
4126 all_windows_inactive,
4127 ..priority_test_picker(1)
4128 };
4129 let output = picker
4130 .build_output_with_time_range(
4131 RegionId::from_u64(1),
4132 assign_to_windows(files.iter(), 1),
4133 Some(5),
4134 None,
4135 )
4136 .await
4137 .unwrap();
4138 assert_eq!(expected, output.len(), "manual: {all_windows_inactive}");
4139 }
4140 }
4141
4142 #[tokio::test]
4143 async fn test_shared_boundary_l1_files_resolve_in_inactive_and_manual_windows() {
4144 let files = [(b"a", b"m"), (b"m", b"z")].map(|(min, max)| {
4145 new_file_handle_with_size_sequence_and_primary_key_range(
4146 FileId::random(),
4147 0,
4148 100,
4149 1,
4150 1,
4151 100,
4152 pk_range(min, max),
4153 )
4154 });
4155 for (active_window, all_windows_inactive, expected) in
4156 [(0, false, 0), (0, true, 1), (3, false, 1)]
4157 {
4158 let picker = TwcsPicker {
4159 all_windows_inactive,
4160 ..priority_test_picker(1)
4161 };
4162 let output = picker
4163 .build_output_with_time_range(
4164 RegionId::from_u64(1),
4165 assign_to_windows(files.iter(), 1),
4166 Some(active_window),
4167 Some(1),
4168 )
4169 .await
4170 .unwrap();
4171 assert_eq!(
4172 expected,
4173 output.len(),
4174 "active: {active_window}, manual: {all_windows_inactive}"
4175 );
4176 if let Some(output) = output.first() {
4177 assert_eq!(2, output.inputs.len());
4178 }
4179 }
4180 }
4181
4182 #[tokio::test]
4183 async fn test_settling_window_defers_overlap_resolution() {
4184 let files = priority_test_files(3, 1, 100, true);
4185 for (active_window, all_windows_inactive, expected) in
4186 [(4, false, 0), (5, false, 1), (2, false, 1), (4, true, 1)]
4187 {
4188 let picker = TwcsPicker {
4189 all_windows_inactive,
4190 ..priority_test_picker(1)
4191 };
4192 let output = picker
4193 .build_output_with_time_range(
4194 RegionId::from_u64(1),
4195 assign_to_windows(files.iter(), 1),
4196 Some(active_window),
4197 Some(1),
4198 )
4199 .await
4200 .unwrap();
4201 assert_eq!(
4202 expected,
4203 output.len(),
4204 "active: {active_window}, manual: {all_windows_inactive}"
4205 );
4206 }
4207 }
4208
4209 #[tokio::test]
4210 async fn test_inactive_windows_merge_small_l1_without_large_peer() {
4211 let large =
4212 new_file_handle_with_size_and_sequence(FileId::random(), 3_000, 3_999, 1, 1, 1_000);
4213 let small = priority_test_files(3, 1, 20, true);
4214 let files = [vec![large.clone()], small.to_vec()].concat();
4215 for (active_window, merge_mode, trigger, expected) in [
4217 (4, MergeMode::LastRow, 2, 1),
4218 (5, MergeMode::LastRow, 2, 1),
4219 (4, MergeMode::LastNonNull, 2, 0),
4220 (5, MergeMode::LastNonNull, 2, 0),
4221 (4, MergeMode::LastRow, 4, 0),
4222 ] {
4223 let picker = TwcsPicker {
4224 merge_mode,
4225 inactive_window_l1_merge_trigger: trigger,
4226 ..priority_test_picker(1)
4227 };
4228 let output = picker
4229 .build_output_with_time_range(
4230 RegionId::from_u64(1),
4231 assign_to_windows(files.iter(), 1),
4232 Some(active_window),
4233 Some(1),
4234 )
4235 .await
4236 .unwrap();
4237 assert_eq!(
4238 expected,
4239 output.len(),
4240 "active: {active_window}, mode: {merge_mode:?}, trigger: {trigger}"
4241 );
4242 if expected == 0 {
4243 continue;
4244 }
4245 let inputs: HashSet<_> = output[0].inputs.iter().map(FileHandle::file_id).collect();
4246 assert_eq!(
4247 small
4248 .iter()
4249 .map(FileHandle::file_id)
4250 .collect::<HashSet<_>>(),
4251 inputs
4252 );
4253 assert!(
4254 !output[0].filter_deleted,
4255 "the unselected large file still overlaps"
4256 );
4257 }
4258 }
4259
4260 #[tokio::test]
4261 async fn test_small_l1_fallback_checks_large_append_mode_peer() {
4262 let files = [
4265 (0, 20, b"a", b"m", 20),
4266 (10, 30, b"l", b"z", 20),
4267 (0, 5, b"y", b"z", 1_000),
4268 ]
4269 .map(|(start, end, min, max, size)| {
4270 new_file_handle_with_size_sequence_and_primary_key_range(
4271 FileId::random(),
4272 start,
4273 end,
4274 1,
4275 1,
4276 size,
4277 pk_range(min, max),
4278 )
4279 });
4280 for append_mode in [false, true] {
4281 let picker = TwcsPicker {
4282 append_mode,
4283 ..priority_test_picker(1)
4284 };
4285 let output = picker
4286 .build_output_with_time_range(
4287 RegionId::from_u64(1),
4288 assign_to_windows(files.iter(), 1),
4289 Some(1),
4290 Some(1),
4291 )
4292 .await
4293 .unwrap();
4294 assert!(output.is_empty(), "append mode: {append_mode}");
4295 }
4296 }
4297
4298 #[tokio::test]
4299 async fn test_settling_window_does_not_rewrite_l1_for_late_data() {
4300 let mut files = priority_test_files(3, 1, 40, false).to_vec();
4302 files.push(new_file_handle_with_size_and_sequence(
4303 FileId::random(),
4304 3_000,
4305 3_299,
4306 0,
4307 10,
4308 10,
4309 ));
4310 for (active_window, expected) in [(4, 0), (5, 1)] {
4311 let output = priority_test_picker(1)
4312 .build_output_with_time_range(
4313 RegionId::from_u64(1),
4314 assign_to_windows(files.iter(), 1),
4315 Some(active_window),
4316 Some(1),
4317 )
4318 .await
4319 .unwrap();
4320 assert_eq!(expected, output.len(), "active: {active_window}");
4321 }
4322 }
4323
4324 #[tokio::test]
4325 async fn test_older_l0_candidate_has_priority_over_newer_pure_l1_candidate() {
4326 let files = priority_test_files(0, 0, 40, false)
4327 .into_iter()
4328 .chain(priority_test_files(1, 1, 40, false))
4329 .collect::<Vec<_>>();
4330 let windows = assign_to_windows(files.iter(), 1);
4331
4332 let output = priority_test_picker(1)
4333 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
4334 .await
4335 .unwrap();
4336
4337 assert_eq!(1, output.len());
4338 assert_eq!(0, output_window(&output[0]));
4339 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
4340 }
4341
4342 #[tokio::test]
4343 async fn test_older_l1_file_reduction_has_priority_over_newer_overlap_resolution() {
4344 let files = priority_test_files(0, 1, 40, false)
4345 .into_iter()
4346 .chain(priority_test_files(1, 1, 100, true))
4347 .collect::<Vec<_>>();
4348 let windows = assign_to_windows(files.iter(), 1);
4349
4350 let output = priority_test_picker(1)
4351 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
4352 .await
4353 .unwrap();
4354
4355 assert_eq!(1, output.len());
4356 assert_eq!(0, output_window(&output[0]));
4357 }
4358
4359 #[tokio::test]
4360 async fn test_multiple_slots_follow_class_priority_and_newer_first_within_class() {
4361 let files = [
4362 priority_test_files(0, 0, 40, false),
4363 priority_test_files(1, 0, 40, false),
4364 priority_test_files(2, 1, 40, false),
4365 priority_test_files(3, 1, 40, false),
4366 priority_test_files(4, 1, 100, true),
4367 priority_test_files(5, 1, 100, true),
4368 ]
4369 .into_iter()
4370 .flatten()
4371 .collect::<Vec<_>>();
4372 let windows = assign_to_windows(files.iter(), 1);
4373
4374 let output = priority_test_picker(5)
4375 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(5), None)
4376 .await
4377 .unwrap();
4378 let pop_priority = output
4379 .iter()
4380 .rev()
4381 .map(|output| (output.inputs[0].level(), output_window(output)))
4382 .collect::<Vec<_>>();
4383
4384 assert_eq!(vec![(0, 1), (0, 0), (1, 3), (1, 2), (1, 4)], pop_priority);
4387 let units = crate::compaction::unit::CompactionUnit::from_picker(PickerOutput {
4388 outputs: output,
4389 time_window_size: 1,
4390 ..Default::default()
4391 })
4392 .unwrap();
4393 let unit_priority = units
4394 .iter()
4395 .map(|unit| {
4396 (
4397 unit.outputs[0].inputs[0].level(),
4398 output_window(&unit.outputs[0]),
4399 )
4400 })
4401 .collect::<Vec<_>>();
4402 assert_eq!(
4403 pop_priority, unit_priority,
4404 "local FIFO must follow the picker's execution priority"
4405 );
4406 }
4407
4408 #[tokio::test]
4409 async fn test_inactive_window_mixed_fallback_merges_within_rewrite_budget() {
4410 let files = [
4411 new_file_with_level_and_rows(0, 99, 1, 1, 1_000, 1_000_000),
4412 new_file_with_level_and_rows(0, 9, 0, 2, 10, 1),
4413 ];
4414 let windows = assign_to_windows(files.iter(), 100);
4415 let picker = TwcsPicker {
4416 trigger_file_num: 4,
4417 active_window_l1_merge_trigger: 8,
4418 inactive_window_trigger_file_num: 2,
4419 inactive_window_l1_merge_trigger: 8,
4420 time_window_seconds: Some(100),
4421 max_output_file_size: Some(2_000),
4422 append_mode: false,
4423 merge_mode: MergeMode::LastRow,
4424 max_background_tasks: None,
4425 time_range: None,
4426 all_windows_inactive: false,
4427 metrics: Default::default(),
4428 };
4429
4430 let output = picker
4431 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
4432 .await
4433 .unwrap();
4434
4435 assert_eq!(1, output.len());
4436 assert_eq!(2, output[0].inputs.len());
4437 }
4438
4439 #[tokio::test]
4440 async fn test_inactive_window_over_budget_merges_l0_only() {
4441 let files = [
4444 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10_000),
4445 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
4446 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 1_000_000),
4447 ];
4448 let windows = assign_to_windows(files.iter(), 100);
4449 let picker = TwcsPicker {
4450 trigger_file_num: 4,
4451 active_window_l1_merge_trigger: 8,
4452 inactive_window_trigger_file_num: 2,
4453 inactive_window_l1_merge_trigger: 8,
4454 time_window_seconds: Some(100),
4455 max_output_file_size: Some(100_000),
4456 append_mode: false,
4457 merge_mode: MergeMode::LastRow,
4458 max_background_tasks: None,
4459 time_range: None,
4460 all_windows_inactive: false,
4461 metrics: Default::default(),
4462 };
4463
4464 let output = picker
4465 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
4466 .await
4467 .unwrap();
4468
4469 assert_eq!(1, output.len());
4470 assert_eq!(2, output[0].inputs.len());
4471 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
4472 }
4473
4474 #[tokio::test]
4475 async fn test_inactive_window_over_budget_without_l0_pair_does_not_merge() {
4476 let files = [
4479 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
4480 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 2, 1_000_000),
4481 ];
4482 let windows = assign_to_windows(files.iter(), 100);
4483 let picker = TwcsPicker {
4484 trigger_file_num: 4,
4485 active_window_l1_merge_trigger: 8,
4486 inactive_window_trigger_file_num: 2,
4487 inactive_window_l1_merge_trigger: 8,
4488 time_window_seconds: Some(100),
4489 max_output_file_size: Some(100_000),
4490 append_mode: false,
4491 merge_mode: MergeMode::LastRow,
4492 max_background_tasks: None,
4493 time_range: None,
4494 all_windows_inactive: false,
4495 metrics: Default::default(),
4496 };
4497
4498 let output = picker
4499 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
4500 .await
4501 .unwrap();
4502
4503 assert!(output.is_empty());
4504 }
4505
4506 #[tokio::test]
4507 async fn test_inactive_window_falls_through_when_triggered_l0_pick_fails() {
4508 let files = [
4512 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100_000),
4513 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
4514 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 50_000),
4515 ];
4516 let windows = assign_to_windows(files.iter(), 100);
4517 let picker = TwcsPicker {
4518 trigger_file_num: 4,
4519 active_window_l1_merge_trigger: 8,
4520 inactive_window_trigger_file_num: 2,
4521 inactive_window_l1_merge_trigger: 8,
4522 time_window_seconds: Some(100),
4523 max_output_file_size: Some(1_000_000),
4524 append_mode: false,
4525 merge_mode: MergeMode::LastRow,
4526 max_background_tasks: None,
4527 time_range: None,
4528 all_windows_inactive: false,
4529 metrics: Default::default(),
4530 };
4531
4532 let output = picker
4533 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
4534 .await
4535 .unwrap();
4536
4537 assert_eq!(1, output.len());
4538 assert_eq!(3, output[0].inputs.len());
4539 }
4540
4541 #[test]
4542 fn test_mixed_candidate_row_balance() {
4543 for (case, l1_rows, l0_rows, expected_len) in [
4544 ("L1 rows dominate", 1_000_000, 10_000, 0),
4545 ("L1 rows meet ratio", 60_000, 10_000, 4),
4546 ("L0 rows are unknown", 1_000_000, 0, 4),
4547 ] {
4548 let files = vec![
4549 new_file_with_level_and_rows(0, 99, 1, 1, 100, l1_rows),
4550 new_file_with_level_and_rows(0, 9, 0, 2, 100, l0_rows),
4551 new_file_with_level_and_rows(20, 29, 0, 3, 100, l0_rows),
4552 new_file_with_level_and_rows(40, 49, 0, 4, 100, l0_rows),
4553 ];
4554
4555 let picked = pick_mixed_count_first(vec![SortedRun::from(files)], None);
4556
4557 assert_eq!(expected_len, picked.len(), "{case}");
4558 }
4559 }
4560
4561 #[test]
4562 fn test_count_first_prefers_smaller_bytes_when_file_counts_match() {
4563 let files = (0..=DEFAULT_MAX_INPUT_FILES)
4564 .map(|idx| {
4565 let start = idx as i64 * 20;
4566 let size = if idx == 0 {
4567 100
4568 } else if idx == DEFAULT_MAX_INPUT_FILES {
4569 1
4570 } else {
4571 10
4572 };
4573 new_file(start, start + 9, idx as u64 + 1, size)
4574 })
4575 .collect::<Vec<_>>();
4576
4577 let picked = pick_count_first(vec![SortedRun::from(files)], None);
4578 let ranges = picked_ranges(&picked);
4579
4580 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
4581 assert_eq!(Some(&(20, 29)), ranges.first());
4582 let last_start = DEFAULT_MAX_INPUT_FILES as i64 * 20;
4583 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
4584 }
4585
4586 #[test]
4587 fn test_count_first_skips_pure_rewrite_without_progress() {
4588 let picked = pick_count_first(
4592 vec![SortedRun::from(vec![
4593 new_file(0, 9, 1, 600),
4594 new_file(20, 29, 2, 600),
4595 new_file(40, 49, 3, 600),
4596 new_file(60, 69, 4, 600),
4597 ])],
4598 Some(512),
4599 );
4600
4601 assert!(picked.is_empty());
4602 }
4603
4604 #[test]
4605 fn test_count_first_allows_overlap_resolution_without_file_reduction() {
4606 let picked = pick_count_first(
4610 vec![
4611 SortedRun::from(vec![new_file(0, 19, 1, 600)]),
4612 SortedRun::from(vec![new_file(10, 29, 2, 600)]),
4613 ],
4614 Some(512),
4615 );
4616
4617 assert_eq!(vec![(0, 19), (10, 29)], picked_ranges(&picked));
4618 }
4619
4620 #[tokio::test]
4621 async fn test_l1_partial_pk_overlap_does_not_rewrite_unrelated_files() {
4622 let a_files = [
4627 (4, 5249), (7, 5218), (1, 5255), (5, 5263), (6, 5131), (3, 5143), (2, 5197), (8, 4716), ]
4636 .into_iter()
4637 .enumerate()
4638 .map(|(idx, (start, size))| {
4639 let key = b'a' + idx as u8;
4640 new_file_handle_with_size_sequence_and_primary_key_range(
4641 FileId::random(),
4642 start,
4643 10_000,
4644 1,
4645 100,
4646 size * 1024 * 1024 / 10,
4647 pk_range(&[key, b'0'], &[key, b'9']),
4648 )
4649 })
4650 .collect::<Vec<_>>();
4651 let mapper = primary_key_mapper_for_test();
4652 for (idx, file) in a_files.iter().enumerate() {
4653 for other in &a_files[idx + 1..] {
4654 assert!(file.overlap_inclusive(other)); assert!(!files_overlap_inclusive(file, other, &mapper));
4656 }
4657 }
4658
4659 let picker = TwcsPicker {
4660 trigger_file_num: 4,
4661 active_window_l1_merge_trigger: 4,
4662 inactive_window_trigger_file_num: 2,
4663 inactive_window_l1_merge_trigger: 8,
4664 time_window_seconds: Some(60),
4665 max_output_file_size: Some(512 * 1024 * 1024),
4666 append_mode: false,
4667 merge_mode: MergeMode::LastRow,
4668 max_background_tasks: Some(1),
4669 time_range: None,
4670 all_windows_inactive: false,
4671 metrics: Default::default(),
4672 };
4673
4674 for (case, key_range, extra_size, overlapping_a, expected_inputs) in [
4675 (
4676 "A3 and E skip the unrelated A files",
4677 Some((b'c', b'c')),
4678 5155,
4679 &[2][..],
4680 &[2, 8][..],
4681 ),
4682 (
4683 "A8 and E must not pull in A2",
4684 Some((b'h', b'h')),
4685 5155,
4686 &[7][..],
4687 &[7, 8][..],
4688 ),
4689 (
4690 "E overlaps no A: no progress",
4691 Some((b'z', b'z')),
4692 5155,
4693 &[][..],
4694 &[] as &[usize],
4695 ),
4696 (
4697 "A3 and E may split into one extra tail file",
4698 Some((b'c', b'c')),
4699 5255,
4700 &[2][..],
4701 &[2, 8][..],
4702 ),
4703 (
4704 "two A files exceed E's run budget",
4705 Some((b'b', b'c')),
4706 5155,
4707 &[1, 2][..],
4708 &[][..],
4709 ),
4710 (
4711 "sufficient E bytes allow two A files separated in time order",
4712 Some((b'b', b'c')),
4713 6000,
4714 &[1, 2][..],
4715 &[1, 2, 8][..],
4716 ),
4717 (
4718 "unknown PK bounds include the whole dominant run in the budget",
4719 None,
4720 5155,
4721 &[0, 1, 2, 3, 4, 5, 6, 7][..],
4722 &[][..],
4723 ),
4724 ] {
4725 let extra = new_file_handle_with_size_sequence_and_primary_key_range(
4726 FileId::random(),
4727 1_000,
4728 9_000,
4729 1,
4730 100,
4731 extra_size * 1024 * 1024 / 10,
4732 key_range.and_then(|(min, max)| pk_range(&[min, b'0'], &[max, b'9'])),
4733 );
4734 for (idx, file) in a_files.iter().enumerate() {
4735 assert!(file.overlap_inclusive(&extra));
4736 assert_eq!(
4737 overlapping_a.contains(&idx),
4738 files_overlap_inclusive(file, &extra, &mapper),
4739 "{case}: A{}",
4740 idx + 1,
4741 );
4742 }
4743
4744 let mut files = a_files.clone();
4745 files.push(extra);
4746 for active_window in [Some(0), None] {
4747 let expected_inputs = if active_window.is_some() {
4750 &[][..]
4751 } else {
4752 expected_inputs
4753 };
4754 let expected = expected_inputs
4755 .iter()
4756 .map(|&idx| files[idx].file_id())
4757 .collect::<HashSet<_>>();
4758 let windows = assign_to_windows(files.iter(), 60);
4759 let outputs = picker
4760 .build_output_with_time_range(
4761 RegionId::from_u64(0),
4762 windows,
4763 active_window,
4764 Some(60),
4765 )
4766 .await
4767 .unwrap();
4768
4769 assert_eq!(
4770 usize::from(!expected_inputs.is_empty()),
4771 outputs.len(),
4772 "{case}, active window: {active_window:?}"
4773 );
4774 let actual = outputs
4775 .iter()
4776 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
4777 .collect::<HashSet<_>>();
4778 assert_eq!(expected, actual, "{case}, active window: {active_window:?}");
4779 }
4780 }
4781 }
4782
4783 #[rstest::rstest]
4784 #[case::active_filtered(Some(0), false, 1000, &[0, 2])]
4785 #[case::inactive_filtered(None, false, 1000, &[0, 2])]
4786 #[case::older_reduction_before_newer_overlap(Some(3), true, 1000, &[0, 2])]
4787 #[case::contiguous_has_more_reduction(Some(0), false, 90, &[0, 1, 2, 3])]
4788 #[case::filtered_has_equal_reduction_with_less_io(Some(0), false, 600, &[0, 2])]
4789 #[tokio::test]
4790 async fn test_l1_file_reduction_considers_filtered_candidates(
4791 #[case] active_window: Option<i64>,
4792 #[case] newer_overlap_window: bool,
4793 #[case] unrelated_size: u64,
4794 #[case] expected_indices: &[usize],
4795 ) {
4796 let mut files = [
4799 (b'a', 100),
4800 (b'b', unrelated_size),
4801 (b'a', 100),
4802 (b'c', unrelated_size),
4803 ]
4804 .into_iter()
4805 .enumerate()
4806 .map(|(idx, (key, size))| {
4807 new_file_handle_with_size_sequence_and_primary_key_range(
4808 FileId::random(),
4809 idx as i64 * 100,
4810 749,
4811 1,
4812 1,
4813 size,
4814 pk_range(&[key], &[key]),
4815 )
4816 })
4817 .collect::<Vec<_>>();
4818 let expected = expected_indices
4819 .iter()
4820 .map(|&idx| files[idx].file_id())
4821 .collect::<Vec<_>>();
4822 if newer_overlap_window {
4823 files.extend(priority_test_files(1, 1, 512, true));
4826 }
4827 let picker = TwcsPicker {
4828 max_output_file_size: Some(512),
4829 ..priority_test_picker(1)
4830 };
4831 let outputs = picker
4832 .build_output_with_time_range(
4833 RegionId::from_u64(0),
4834 assign_to_windows(files.iter(), 1),
4835 active_window,
4836 Some(1),
4837 )
4838 .await
4839 .unwrap();
4840
4841 assert_eq!(1, outputs.len());
4842 assert_eq!(
4843 expected,
4844 outputs[0]
4845 .inputs
4846 .iter()
4847 .map(FileHandle::file_id)
4848 .collect::<Vec<_>>()
4849 );
4850 }
4851
4852 #[tokio::test]
4853 async fn test_overlap_resolution_limits_dominant_run_rewrites() {
4854 let file_size = 512 * 1024 * 1024;
4855 for (case, peer_sizes, max_output_size, should_pick) in [
4857 (
4858 "small peer cannot rewrite any slice of the large run",
4859 vec![file_size],
4860 file_size,
4861 false,
4862 ),
4863 (
4864 "one byte below the run budget",
4865 [vec![file_size; 6], vec![file_size - 1]].concat(),
4866 file_size,
4867 false,
4868 ),
4869 (
4870 "exact run budget boundary",
4871 vec![file_size; 7],
4872 file_size,
4873 true,
4874 ),
4875 ("equal sized runs", vec![file_size; 8], file_size, true),
4876 (
4877 "file reduction keeps its own admission",
4878 vec![file_size],
4879 2 * file_size,
4880 true,
4881 ),
4882 ] {
4883 let mut files = (0..8)
4884 .map(|idx| {
4885 let key = b'a' + idx as u8;
4886 new_file_handle_with_size_sequence_and_primary_key_range(
4887 FileId::random(),
4888 idx,
4889 10_000,
4890 1,
4891 100,
4892 file_size,
4893 pk_range(&[key, b'0'], &[key, b'9']),
4894 )
4895 })
4896 .collect::<Vec<_>>();
4897 files.extend(peer_sizes.into_iter().enumerate().map(|(idx, size)| {
4898 let start = 1_000 * (idx as i64 + 1);
4899 new_file_handle_with_size_sequence_and_primary_key_range(
4900 FileId::random(),
4901 start,
4902 start + 999,
4903 1,
4904 100,
4905 size,
4906 pk_range(b"a0", b"h9"),
4907 )
4908 }));
4909 let reduces_files = max_output_size > file_size;
4912 let picker = TwcsPicker {
4913 trigger_file_num: 4,
4914 active_window_l1_merge_trigger: 4,
4915 inactive_window_trigger_file_num: 2,
4916 inactive_window_l1_merge_trigger: 8,
4917 time_window_seconds: Some(60),
4918 max_output_file_size: Some(max_output_size),
4919 append_mode: false,
4920 merge_mode: MergeMode::LastRow,
4921 max_background_tasks: Some(1),
4922 time_range: None,
4923 all_windows_inactive: false,
4924 metrics: Default::default(),
4925 };
4926
4927 for active_window in [Some(0), None] {
4928 let expected = if should_pick && (reduces_files || active_window.is_none()) {
4929 files
4930 .iter()
4931 .map(FileHandle::file_id)
4932 .collect::<HashSet<_>>()
4933 } else {
4934 HashSet::new()
4935 };
4936 let outputs = picker
4937 .build_output_with_time_range(
4938 RegionId::from_u64(0),
4939 assign_to_windows(files.iter(), 60),
4940 active_window,
4941 Some(60),
4942 )
4943 .await
4944 .unwrap();
4945 let actual = outputs
4946 .iter()
4947 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
4948 .collect::<HashSet<_>>();
4949 assert_eq!(expected, actual, "{case}, active window: {active_window:?}");
4950 }
4951 }
4952 }
4953
4954 #[tokio::test]
4955 async fn test_quiet_or_manual_window_resolves_dominant_run_overlap() {
4956 let file_size = 512 * 1024 * 1024;
4957 let l1_file =
4958 |start, end, sequence, size, range: Option<(bytes::Bytes, bytes::Bytes)>, num_rows| {
4959 let file = new_file_handle_with_size_sequence_and_primary_key_range(
4960 FileId::random(),
4961 start,
4962 end,
4963 1,
4964 sequence,
4965 size,
4966 range.clone(),
4967 );
4968 FileHandle::new_with_primary_key_range(
4969 FileMeta {
4970 num_rows,
4971 ..file.meta_ref().clone()
4972 },
4973 crate::test_util::new_noop_file_purger(),
4974 range,
4975 )
4976 };
4977
4978 for (newer_sequence, num_rows, manual, resolves) in [
4982 (108, 1, false, false),
4983 (109, 1, false, true),
4984 (109, 0, false, false),
4985 (100, 1, true, true),
4986 ] {
4987 let mut window_files = (0..8)
4990 .map(|idx| {
4991 let key = b'a' + idx as u8;
4992 let range = pk_range(&[key, b'0'], &[key, b'9']);
4993 l1_file(idx, 10_000, 100, file_size, range, num_rows)
4994 })
4995 .collect::<Vec<_>>();
4996 window_files.push(l1_file(
4997 1_000,
4998 1_999,
4999 100,
5000 file_size / 8,
5001 pk_range(b"a0", b"h9"),
5002 num_rows,
5003 ));
5004 let expected = window_files
5005 .iter()
5006 .map(FileHandle::file_id)
5007 .collect::<HashSet<_>>();
5008 let picker = TwcsPicker {
5009 trigger_file_num: 4,
5010 active_window_l1_merge_trigger: 4,
5011 inactive_window_trigger_file_num: 2,
5012 inactive_window_l1_merge_trigger: 16,
5013 time_window_seconds: Some(60),
5014 max_output_file_size: Some(file_size),
5015 append_mode: false,
5016 merge_mode: MergeMode::LastRow,
5017 max_background_tasks: Some(1),
5018 time_range: None,
5019 all_windows_inactive: manual,
5020 metrics: Default::default(),
5021 };
5022 let mut files = window_files;
5023 files.push(l1_file(
5024 120_000,
5025 120_999,
5026 newer_sequence,
5027 file_size,
5028 pk_range(b"a0", b"h9"),
5029 1,
5030 ));
5031 let outputs = picker
5032 .build_output_with_time_range(
5033 RegionId::from_u64(0),
5034 assign_to_windows(files.iter(), 60),
5035 manual.then_some(0),
5036 Some(60),
5037 )
5038 .await
5039 .unwrap();
5040 let actual = outputs
5041 .iter()
5042 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
5043 .collect::<HashSet<_>>();
5044 let case = format!("sequence {newer_sequence}, rows {num_rows}, manual {manual}");
5045 if resolves {
5046 assert_eq!(expected, actual, "{case}");
5047 } else {
5048 assert!(actual.is_empty(), "{case}: {actual:?}");
5049 }
5050 }
5051 }
5052
5053 #[test]
5054 fn test_count_first_prefers_guaranteed_reduction_over_pure_rewrite() {
5055 let picked = pick_count_first(
5059 vec![SortedRun::from(vec![
5060 new_file(0, 9, 1, 600),
5061 new_file(10, 19, 2, 600),
5062 new_file(20, 29, 3, 600),
5063 new_file(30, 39, 4, 10),
5064 new_file(40, 49, 5, 10),
5065 new_file(50, 59, 6, 10),
5066 ])],
5067 Some(512),
5068 );
5069
5070 assert_eq!(vec![(30, 39), (40, 49), (50, 59)], picked_ranges(&picked));
5071 }
5072
5073 #[test]
5074 fn test_count_first_prefers_earlier_time_on_exact_tie() {
5075 let files = (0..=DEFAULT_MAX_INPUT_FILES)
5076 .map(|idx| {
5077 let start = idx as i64 * 20;
5078 new_file(start, start + 9, idx as u64 + 1, 10)
5079 })
5080 .collect::<Vec<_>>();
5081
5082 let picked = pick_count_first(vec![SortedRun::from(files)], None);
5083 let ranges = picked_ranges(&picked);
5084
5085 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
5086 assert_eq!(Some(&(0, 9)), ranges.first());
5087 let last_start = (DEFAULT_MAX_INPUT_FILES as i64 - 1) * 20;
5088 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
5089 }
5090
5091 #[test]
5092 fn test_count_first_keeps_each_interleaved_run_contiguous() {
5093 let picked = pick_count_first(
5094 vec![
5095 SortedRun::from(vec![
5096 new_file(0, 9, 1, 1),
5097 new_file(20, 29, 2, 1),
5098 new_file(40, 49, 3, 1),
5099 ]),
5100 SortedRun::from(vec![new_file(10, 19, 4, 1), new_file(30, 39, 5, 1)]),
5101 ],
5102 None,
5103 );
5104
5105 assert_eq!(
5106 vec![(0, 9), (10, 19), (20, 29), (30, 39), (40, 49)],
5107 picked_ranges(&picked)
5108 );
5109 }
5110
5111 #[test]
5118 fn test_count_first_candidate_spans_same_sequence_files() {
5119 let picked = pick_count_first(
5120 vec![SortedRun::from(vec![
5121 new_file(0, 9, 1, 10),
5122 new_file(10, 19, 2, 10),
5124 new_file(10, 19, 2, 10),
5125 new_file(10, 19, 2, 10),
5126 new_file(20, 29, 3, 10),
5127 ])],
5128 None,
5129 );
5130
5131 assert_eq!(
5132 vec![(0, 9), (10, 19), (10, 19), (10, 19), (20, 29)],
5133 picked_ranges(&picked)
5134 );
5135 }
5136
5137 #[tokio::test]
5143 async fn test_count_first_merges_window_with_interleaved_flush_groups() {
5144 let files = [
5145 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
5147 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
5149 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
5150 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
5151 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 3, 10),
5153 ];
5154 let windows = assign_to_windows(files.iter(), 100);
5155 let picker = TwcsPicker {
5156 trigger_file_num: 3,
5157 active_window_l1_merge_trigger: 8,
5158 inactive_window_trigger_file_num: 3,
5159 inactive_window_l1_merge_trigger: 8,
5160 time_window_seconds: Some(100),
5161 max_output_file_size: None,
5162 append_mode: false,
5163 merge_mode: MergeMode::LastRow,
5164 max_background_tasks: None,
5165 time_range: None,
5166 all_windows_inactive: false,
5167 metrics: Default::default(),
5168 };
5169
5170 let output = picker
5171 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
5172 .await
5173 .unwrap();
5174
5175 assert_eq!(1, output.len());
5176 assert_eq!(5, output[0].inputs.len());
5178 }
5179}