1use std::cmp::Reverse;
62use std::ops::Range;
63
64use bytes::Bytes;
65use common_time::Timestamp;
66
67use crate::sst::file::FileHandle;
68use crate::sst::primary_key::PrimaryKeyRangeMapper;
69
70const MAX_REWRITE_FILES_PER_REDUCED_FILE: u64 = 4;
73
74const SMALL_L1_SIZE_FACTOR: u64 = 4;
76
77const CONFLICT_GRAPH_BUDGET: usize = 1 << 20;
81
82#[derive(Debug, Clone, Copy)]
84pub(crate) struct PickLimits {
85 pub(crate) max_output_file_size: Option<u64>,
87 pub(crate) max_input_files: usize,
89}
90
91impl PickLimits {
92 fn predicted_outputs(&self, stats: &IntervalStats) -> usize {
96 match self.max_output_file_size {
97 Some(max) if max > 0 && stats.single_key.is_none() => {
98 stats.size.div_ceil(max).max(1) as usize
99 }
100 _ => 1,
101 }
102 }
103
104 fn within_reduction_cost(&self, size: u64, reduction: usize) -> bool {
107 match self.max_output_file_size {
108 Some(max) if max > 0 => {
109 size <= (reduction as u64)
110 .saturating_mul(MAX_REWRITE_FILES_PER_REDUCED_FILE)
111 .saturating_mul(max)
112 }
113 _ => true,
114 }
115 }
116
117 fn run_tolerance(&self) -> u64 {
119 self.max_output_file_size.unwrap_or(0)
120 }
121}
122
123#[derive(Debug)]
125pub(crate) struct UnitPick {
126 pub(crate) inputs: Vec<FileHandle>,
127 pub(crate) reduces_files: bool,
130 pub(crate) units: usize,
132 pub(crate) dense_fallback: bool,
134}
135
136pub(crate) fn pick_active_l1(
146 l1_files: &[FileHandle],
147 mapper: &PrimaryKeyRangeMapper,
148 options: PickLimits,
149) -> Option<UnitPick> {
150 pick_active_l1_with_budget(l1_files, mapper, options, CONFLICT_GRAPH_BUDGET)
151}
152
153fn pick_active_l1_with_budget(
156 l1_files: &[FileHandle],
157 mapper: &PrimaryKeyRangeMapper,
158 options: PickLimits,
159 work_budget: usize,
160) -> Option<UnitPick> {
161 let extents = extents_of(l1_files, mapper);
162 let index = ExtentIndex::new(&extents);
163 let mut units = match conflict_units(l1_files, &extents, &index, work_budget) {
164 Some(units) => units,
165 None => single_file_units(l1_files, &extents),
166 };
167 split_oversized_units(&mut units, l1_files, &extents, options.max_input_files);
168 if let Some(pick) = pick_reducing_interval(&mut units, l1_files, options) {
169 return Some(pick);
170 }
171 if units.len() == l1_files.len() {
172 return None;
173 }
174 split_oversized_units(&mut units, l1_files, &extents, 1);
175 pick_reducing_interval(&mut units, l1_files, options)
176}
177
178fn pick_reducing_interval(
181 units: &mut [Unit],
182 files: &[FileHandle],
183 options: PickLimits,
184) -> Option<UnitPick> {
185 sort_units(units);
186 let range = pick_reducing_interval_where(units, options, |_| true)?;
187 Some(UnitPick {
188 inputs: collect_inputs(&units[range], files),
189 reduces_files: true,
190 units: units.len(),
191 dense_fallback: false,
192 })
193}
194
195fn pick_reducing_interval_where(
198 units: &[Unit],
199 options: PickLimits,
200 is_eligible: impl Fn(&IntervalStats) -> bool,
201) -> Option<Range<usize>> {
202 let mut best: Option<(usize, u64, Range<usize>)> = None;
203 for_each_interval(units, &[], options.max_input_files, |range, stats| {
205 let reduction = stats
206 .num_files
207 .saturating_sub(options.predicted_outputs(stats));
208 if stats.num_files < 2
209 || reduction == 0
210 || !options.within_reduction_cost(stats.size, reduction)
211 || stats.largest_file_size > stats.size - stats.largest_file_size
212 || !is_eligible(stats)
213 {
214 return;
215 }
216 let better = best.as_ref().is_none_or(|(best_reduction, best_size, _)| {
217 (reduction, Reverse(stats.size)) > (*best_reduction, Reverse(*best_size))
218 });
219 if better {
220 best = Some((reduction, stats.size, range));
221 }
222 });
223
224 best.map(|(_, _, range)| range)
225}
226
227pub(crate) fn pick_small_l1(
235 files: &[FileHandle],
236 mapper: &PrimaryKeyRangeMapper,
237 limits: PickLimits,
238) -> Option<UnitPick> {
239 let max_output = limits.max_output_file_size.filter(|size| *size > 0)?;
240 let extents = extents_of(files, mapper);
241 let mut units = single_file_units(files, &extents);
242 units.retain(|unit| {
243 let file = &files[unit.files[0]];
244 file.level() == 1 && file.size() <= max_output / SMALL_L1_SIZE_FACTOR
245 });
246 sort_units(&mut units);
247 let range = pick_reducing_interval_where(&units, limits, |stats| stats.size <= max_output)?;
248 let index = ExtentIndex::new(&extents);
249 if !adds_no_conflicts(&units[range.clone()], &extents, &index, 1) {
250 return None;
251 }
252 Some(UnitPick {
253 inputs: collect_inputs(&units[range], files),
254 reduces_files: true,
255 units: units.len(),
256 dense_fallback: false,
257 })
258}
259
260#[derive(Debug, Clone, Copy)]
262pub(crate) struct ClosedMergeOptions {
263 pub(crate) limits: PickLimits,
264 pub(crate) allow_file_reduction: bool,
266 pub(crate) allow_conflicts: bool,
269 pub(crate) allow_unbalanced: bool,
273}
274
275pub(crate) fn pick_closed_merge(
292 files: &[FileHandle],
293 mapper: &PrimaryKeyRangeMapper,
294 options: ClosedMergeOptions,
295) -> Option<UnitPick> {
296 pick_closed_merge_with_budget(files, mapper, options, CONFLICT_GRAPH_BUDGET)
297}
298
299fn pick_closed_merge_with_budget(
302 files: &[FileHandle],
303 mapper: &PrimaryKeyRangeMapper,
304 options: ClosedMergeOptions,
305 work_budget: usize,
306) -> Option<UnitPick> {
307 let [best, _] = pick_closed_merge_candidates(files, mapper, options, work_budget, false);
308 best
309}
310
311pub(crate) fn pick_closed_merge_by_class(
314 files: &[FileHandle],
315 mapper: &PrimaryKeyRangeMapper,
316 options: ClosedMergeOptions,
317) -> [Option<UnitPick>; 2] {
318 pick_closed_merge_candidates(files, mapper, options, CONFLICT_GRAPH_BUDGET, true)
319}
320
321struct ClosedMergeCandidate {
323 benefit: u64,
324 size: u64,
325 range: Range<usize>,
326 reduces_files: bool,
327}
328
329fn pick_closed_merge_candidates(
330 files: &[FileHandle],
331 mapper: &PrimaryKeyRangeMapper,
332 options: ClosedMergeOptions,
333 work_budget: usize,
334 per_class: bool,
335) -> [Option<UnitPick>; 2] {
336 let limits = options.limits;
337 let extents = extents_of(files, mapper);
338 let index = ExtentIndex::new(&extents);
339 let mut dense = false;
340 let mut units = match conflict_units(files, &extents, &index, work_budget) {
341 Some(mut units) => {
342 close_units(&mut units, &extents, &index);
343 split_oversized_units(&mut units, files, &extents, limits.max_input_files);
344 units
345 }
346 None => {
347 dense = true;
348 single_file_units(files, &extents)
349 }
350 };
351 sort_units(&mut units);
352 if dense {
353 return [
354 pick_dense_closed_merge(&units, files, &extents, &index, options),
355 None,
356 ];
357 }
358 let Some(pairs) = split_conflicts(&units, &extents, &index, work_budget) else {
359 return [
360 pick_dense_closed_merge(&units, files, &extents, &index, options),
361 None,
362 ];
363 };
364
365 let mut best: [Option<ClosedMergeCandidate>; 2] = [None, None];
366 for_each_interval(&units, &pairs, limits.max_input_files, |range, stats| {
367 if stats.num_files < 2 {
368 return;
369 }
370 let outputs = limits.predicted_outputs(stats);
371 if outputs > stats.num_files + 1 {
372 return;
373 }
374 let reduction = stats.num_files.saturating_sub(outputs);
375 let reduces_files = reduction > 0
376 && (options.allow_file_reduction || stats.conflicts > 0)
377 && limits.within_reduction_cost(stats.size, reduction);
378 if (stats.conflicts > 0 && !options.allow_conflicts)
379 || (!reduces_files && stats.conflicts == 0)
380 {
381 return;
382 }
383 let best = &mut best[usize::from(per_class && !reduces_files)];
386 let benefit = (reduction + stats.conflicts) as u64;
387 let better = best.as_ref().is_none_or(|candidate| {
388 is_more_efficient(benefit, stats.size, candidate.benefit, candidate.size)
389 });
390 if !better {
391 return;
392 }
393 let units = &units[range.clone()];
394 if !reduces_files
397 && !options.allow_unbalanced
398 && !has_balanced_components(units, files, &extents, limits.run_tolerance())
399 {
400 return;
401 }
402 if units.len() > 1 && !adds_no_conflicts(units, &extents, &index, outputs) {
403 return;
404 }
405 *best = Some(ClosedMergeCandidate {
406 benefit,
407 size: stats.size,
408 range,
409 reduces_files,
410 });
411 });
412
413 best.map(|candidate| {
414 candidate.map(|candidate| UnitPick {
415 inputs: collect_inputs(&units[candidate.range], files),
416 reduces_files: candidate.reduces_files,
417 units: units.len(),
418 dense_fallback: false,
419 })
420 })
421}
422
423fn pick_dense_closed_merge(
434 units: &[Unit],
435 files: &[FileHandle],
436 extents: &[Extent],
437 index: &ExtentIndex,
438 options: ClosedMergeOptions,
439) -> Option<UnitPick> {
440 let limits = options.limits;
441 let range = pick_reducing_interval_where(units, limits, |_| true)?;
442 let mut selected = &units[range];
443 if !is_admissible_dense_interval(selected, extents, index, options) {
444 let range = pick_reducing_interval_where(selected, limits, |stats| {
445 limits.predicted_outputs(stats) == 1
446 })?;
447 selected = &selected[range];
448 if !is_admissible_dense_interval(selected, extents, index, options) {
449 return None;
450 }
451 }
452 Some(UnitPick {
453 inputs: collect_inputs(selected, files),
454 reduces_files: true,
455 units: units.len(),
456 dense_fallback: true,
457 })
458}
459
460fn is_admissible_dense_interval(
463 units: &[Unit],
464 extents: &[Extent],
465 index: &ExtentIndex,
466 options: ClosedMergeOptions,
467) -> bool {
468 let stats = interval_stats_exact(units, extents);
469 (options.allow_file_reduction || stats.conflicts > 0)
470 && (options.allow_conflicts || stats.conflicts == 0)
471 && adds_no_conflicts(
472 units,
473 extents,
474 index,
475 options.limits.predicted_outputs(&stats),
476 )
477}
478
479fn interval_stats_exact<'a>(units: &'a [Unit], extents: &[Extent]) -> IntervalStats<'a> {
488 let mut stats = IntervalStats::default();
489 let mut first = true;
490 for unit in units {
491 stats.num_files += unit.files.len();
492 stats.size += unit.size;
493 stats.largest_file_size = stats.largest_file_size.max(unit.largest_file_size);
494 stats.conflicts += unit.conflicts;
495 stats.single_key = if first {
496 unit.single_key()
497 } else {
498 stats
499 .single_key
500 .filter(|&key| unit.single_key() == Some(key))
501 };
502 first = false;
503 }
504
505 let split_files: Vec<usize> = units
506 .iter()
507 .filter(|unit| unit.split)
508 .map(|unit| unit.files[0])
509 .collect();
510 for (offset, &lhs) in split_files.iter().enumerate() {
511 for &rhs in &split_files[offset + 1..] {
512 if extents[lhs].conflicts(&extents[rhs]) {
513 stats.conflicts += 1;
514 }
515 }
516 }
517 stats
518}
519
520fn sort_units(units: &mut [Unit]) {
525 units.sort_by(|lhs, rhs| {
526 (lhs.first_key(), lhs.extent.start, Reverse(lhs.extent.end))
527 .cmp(&(rhs.first_key(), rhs.extent.start, Reverse(rhs.extent.end)))
528 .then_with(|| lhs.files[0].cmp(&rhs.files[0]))
529 });
530}
531
532fn is_more_efficient(benefit: u64, size: u64, best_benefit: u64, best_size: u64) -> bool {
535 let lhs = u128::from(benefit) * u128::from(best_size.max(1));
536 let rhs = u128::from(best_benefit) * u128::from(size.max(1));
537 lhs > rhs || (lhs == rhs && size < best_size)
538}
539
540#[derive(Debug, Clone)]
542pub(crate) struct Extent {
543 start: Timestamp,
544 end: Timestamp,
545 primary_key: Option<(Bytes, Bytes)>,
547}
548
549impl Extent {
550 pub(crate) fn of_file(file: &FileHandle, mapper: &PrimaryKeyRangeMapper) -> Self {
551 let (start, end) = file.time_range();
552 Self {
553 start,
554 end,
555 primary_key: file.primary_key_range(mapper),
556 }
557 }
558
559 fn union(&self, other: &Self) -> Self {
560 Self {
561 start: self.start.min(other.start),
562 end: self.end.max(other.end),
563 primary_key: match (&self.primary_key, &other.primary_key) {
564 (Some((lhs_min, lhs_max)), Some((rhs_min, rhs_max))) => Some((
565 lhs_min.clone().min(rhs_min.clone()),
566 lhs_max.clone().max(rhs_max.clone()),
567 )),
568 _ => None,
569 },
570 }
571 }
572
573 fn time_overlaps(&self, other: &Self) -> bool {
575 self.start.max(other.start) < self.end.min(other.end)
576 }
577
578 pub(crate) fn conflicts(&self, other: &Self) -> bool {
584 self.time_overlaps(other)
585 && match (&self.primary_key, &other.primary_key) {
586 (Some(lhs), Some(rhs)) => {
587 let (low, high) = ((&lhs.0).max(&rhs.0), (&lhs.1).min(&rhs.1));
588 low <= high
589 }
590 _ => true,
591 }
592 }
593
594 fn intrudes(&self, area: &Self) -> bool {
600 self.conflicts(area)
601 }
602}
603
604fn is_single_key(range: &(Bytes, Bytes)) -> bool {
605 range.0 == range.1
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
610enum UpperKey<'a> {
611 Key(&'a Bytes),
612 Unbounded,
613}
614
615#[derive(Debug, Clone, Copy)]
617struct SubtreeBounds<'a> {
618 max_upper_key: UpperKey<'a>,
619 min_start: Timestamp,
620 max_end: Timestamp,
621}
622
623impl<'a> SubtreeBounds<'a> {
624 fn of(extent: &'a Extent) -> Self {
625 Self {
626 max_upper_key: match &extent.primary_key {
627 Some((_, max)) => UpperKey::Key(max),
628 None => UpperKey::Unbounded,
629 },
630 min_start: extent.start,
631 max_end: extent.end,
632 }
633 }
634
635 fn merge(self, other: Self) -> Self {
636 Self {
637 max_upper_key: self.max_upper_key.max(other.max_upper_key),
638 min_start: self.min_start.min(other.min_start),
639 max_end: self.max_end.max(other.max_end),
640 }
641 }
642}
643
644struct ExtentIndex<'a> {
652 extents: &'a [Extent],
653 by_lower_key: Vec<usize>,
656 bounds: Vec<Option<SubtreeBounds<'a>>>,
658 leaf_base: usize,
660}
661
662impl<'a> ExtentIndex<'a> {
663 fn new(extents: &'a [Extent]) -> Self {
664 let lower_key = |i: usize| extents[i].primary_key.as_ref().map(|(min, _)| min);
665 let mut by_lower_key: Vec<usize> = (0..extents.len()).collect();
666 by_lower_key.sort_by(|&lhs, &rhs| {
667 (lower_key(lhs), extents[lhs].start).cmp(&(lower_key(rhs), extents[rhs].start))
668 });
669 let leaf_base = extents.len().next_power_of_two();
670 let mut bounds = vec![None; 2 * leaf_base];
671 for (offset, &i) in by_lower_key.iter().enumerate() {
672 bounds[leaf_base + offset] = Some(SubtreeBounds::of(&extents[i]));
673 }
674 for node in (1..leaf_base).rev() {
675 bounds[node] = match (bounds[2 * node], bounds[2 * node + 1]) {
676 (Some(lhs), Some(rhs)) => Some(lhs.merge(rhs)),
677 (lhs, rhs) => lhs.or(rhs),
678 };
679 }
680 Self {
681 extents,
682 by_lower_key,
683 bounds,
684 leaf_base,
685 }
686 }
687
688 fn for_each_near(&self, area: &Extent, visit: impl FnMut(usize)) {
692 let mut budget = usize::MAX;
693 let _ = self.for_each_near_with_budget(area, &mut budget, visit);
694 }
695
696 fn for_each_near_with_budget(
703 &self,
704 area: &Extent,
705 budget: &mut usize,
706 mut visit: impl FnMut(usize),
707 ) -> bool {
708 let leaves_end = match &area.primary_key {
709 Some((_, max)) => self.by_lower_key.partition_point(|&i| {
710 self.extents[i]
711 .primary_key
712 .as_ref()
713 .is_none_or(|(min, _)| min <= max)
714 }),
715 None => self.by_lower_key.len(),
716 };
717 self.visit_subtree(1, 0..self.leaf_base, leaves_end, area, budget, &mut visit)
718 }
719
720 fn visit_subtree(
721 &self,
722 node: usize,
723 leaves: Range<usize>,
724 leaves_end: usize,
725 area: &Extent,
726 budget: &mut usize,
727 visit: &mut impl FnMut(usize),
728 ) -> bool {
729 if *budget == 0 {
730 return false;
731 }
732 *budget -= 1;
733 if leaves.start >= leaves_end {
734 return true;
735 }
736 let Some(bounds) = self.bounds[node] else {
737 return true;
738 };
739 if bounds.min_start >= area.end
740 || bounds.max_end <= area.start
741 || area
742 .primary_key
743 .as_ref()
744 .is_some_and(|(min, _)| bounds.max_upper_key < UpperKey::Key(min))
745 {
746 return true;
747 }
748 if leaves.len() == 1 {
749 let i = self.by_lower_key[leaves.start];
750 if self.extents[i].time_overlaps(area) {
751 visit(i);
752 }
753 return true;
754 }
755 let mid = leaves.start + leaves.len() / 2;
756 if !self.visit_subtree(2 * node, leaves.start..mid, leaves_end, area, budget, visit) {
757 return false;
758 }
759 self.visit_subtree(
760 2 * node + 1,
761 mid..leaves.end,
762 leaves_end,
763 area,
764 budget,
765 visit,
766 )
767 }
768}
769
770fn extents_of(files: &[FileHandle], mapper: &PrimaryKeyRangeMapper) -> Vec<Extent> {
771 files
772 .iter()
773 .map(|file| Extent::of_file(file, mapper))
774 .collect()
775}
776
777#[derive(Debug)]
779struct Unit {
780 files: Vec<usize>,
782 extent: Extent,
783 size: u64,
784 largest_file_size: u64,
785 conflicts: usize,
787 split: bool,
790}
791
792impl Unit {
793 fn new(
794 members: Vec<usize>,
795 files: &[FileHandle],
796 extents: &[Extent],
797 conflicts: usize,
798 ) -> Self {
799 let extent = members[1..]
800 .iter()
801 .fold(extents[members[0]].clone(), |extent, &i| {
802 extent.union(&extents[i])
803 });
804 let sizes = members.iter().map(|&i| files[i].size());
805 Self {
806 size: sizes.clone().sum(),
807 largest_file_size: sizes.max().unwrap_or(0),
808 files: members,
809 extent,
810 conflicts,
811 split: false,
812 }
813 }
814
815 fn first_key(&self) -> Option<&Bytes> {
817 self.extent.primary_key.as_ref().map(|(min, _)| min)
818 }
819
820 fn single_key(&self) -> Option<&Bytes> {
822 self.extent
823 .primary_key
824 .as_ref()
825 .filter(|range| is_single_key(range))
826 .map(|(min, _)| min)
827 }
828}
829
830fn conflict_units(
838 files: &[FileHandle],
839 extents: &[Extent],
840 index: &ExtentIndex,
841 work_budget: usize,
842) -> Option<Vec<Unit>> {
843 let mut sets = DisjointSet::new(extents.len());
844 let mut conflicts = vec![0; extents.len()];
845 let mut budget = work_budget;
846 for i in 0..extents.len() {
847 let completed = index.for_each_near_with_budget(&extents[i], &mut budget, |j| {
848 if j > i && extents[i].conflicts(&extents[j]) {
850 sets.union(i, j);
851 conflicts[i] += 1;
852 }
853 });
854 if !completed {
855 return None;
856 }
857 }
858 Some(
859 sets.groups()
860 .into_iter()
861 .map(|members| {
862 let pairs = members.iter().map(|&i| conflicts[i]).sum();
863 Unit::new(members, files, extents, pairs)
864 })
865 .collect(),
866 )
867}
868
869fn single_file_units(files: &[FileHandle], extents: &[Extent]) -> Vec<Unit> {
872 (0..files.len())
873 .map(|i| Unit {
874 split: true,
875 ..Unit::new(vec![i], files, extents, 0)
876 })
877 .collect()
878}
879
880fn split_oversized_units(
883 units: &mut Vec<Unit>,
884 files: &[FileHandle],
885 extents: &[Extent],
886 max_input_files: usize,
887) {
888 let (oversized, mut kept): (Vec<_>, Vec<_>) = std::mem::take(units)
889 .into_iter()
890 .partition(|unit| unit.files.len() > max_input_files);
891 kept.extend(
892 oversized
893 .into_iter()
894 .flat_map(|unit| unit.files)
895 .map(|i| Unit {
896 split: true,
897 ..Unit::new(vec![i], files, extents, 0)
898 }),
899 );
900 *units = kept;
901}
902
903fn close_units(units: &mut Vec<Unit>, extents: &[Extent], index: &ExtentIndex) {
908 let mut owner = vec![0; extents.len()];
909 for (u, unit) in units.iter().enumerate() {
910 for &i in &unit.files {
911 owner[i] = u;
912 }
913 }
914 let mut alive = vec![true; units.len()];
915 let mut pending: Vec<usize> = (0..units.len()).collect();
916 while let Some(u) = pending.pop() {
917 if !alive[u] {
918 continue;
919 }
920 let area = &units[u].extent;
921 let mut intruders = std::collections::BTreeSet::new();
922 index.for_each_near(area, |i| {
923 if owner[i] != u && extents[i].intrudes(area) {
924 intruders.insert(owner[i]);
925 }
926 });
927 if intruders.is_empty() {
928 continue;
929 }
930 for v in intruders {
931 alive[v] = false;
932 let absorbed = std::mem::take(&mut units[v].files);
933 for &i in &absorbed {
934 owner[i] = u;
935 }
936 let (extent, size, largest, conflicts) = {
937 let other = &units[v];
938 (
939 other.extent.clone(),
940 other.size,
941 other.largest_file_size,
942 other.conflicts,
943 )
944 };
945 let unit = &mut units[u];
946 unit.files.extend(absorbed);
947 unit.extent = unit.extent.union(&extent);
948 unit.size += size;
949 unit.largest_file_size = unit.largest_file_size.max(largest);
950 unit.conflicts += conflicts;
951 }
952 pending.push(u);
953 }
954 let mut closed = Vec::with_capacity(units.len());
955 for (u, mut unit) in std::mem::take(units).into_iter().enumerate() {
956 if alive[u] {
957 unit.files.sort_unstable();
958 closed.push(unit);
959 }
960 }
961 *units = closed;
962}
963
964#[derive(Debug, Default)]
966struct IntervalStats<'a> {
967 num_files: usize,
968 size: u64,
969 largest_file_size: u64,
970 conflicts: usize,
971 single_key: Option<&'a Bytes>,
973}
974
975fn split_conflicts(
981 units: &[Unit],
982 extents: &[Extent],
983 index: &ExtentIndex,
984 work_budget: usize,
985) -> Option<Vec<Vec<usize>>> {
986 let mut position = vec![None; extents.len()];
987 for (p, unit) in units.iter().enumerate() {
988 if unit.split {
989 position[unit.files[0]] = Some(p);
990 }
991 }
992 let mut budget = work_budget;
993 let mut result = Vec::with_capacity(units.len());
994 for unit in units {
995 if !unit.split {
996 result.push(Vec::new());
997 continue;
998 }
999 let i = unit.files[0];
1000 let mut neighbors = Vec::new();
1001 let completed = index.for_each_near_with_budget(&extents[i], &mut budget, |j| {
1002 if let Some(p) = position[j]
1003 && j != i
1004 && extents[i].conflicts(&extents[j])
1005 {
1006 neighbors.push(p);
1007 }
1008 });
1009 if !completed {
1010 return None;
1011 }
1012 neighbors.sort_unstable();
1013 result.push(neighbors);
1014 }
1015 Some(result)
1016}
1017
1018fn for_each_interval<'a>(
1024 units: &'a [Unit],
1025 split_conflicts: &[Vec<usize>],
1026 max_input_files: usize,
1027 mut visit: impl FnMut(Range<usize>, &IntervalStats<'a>),
1028) {
1029 for left in 0..units.len() {
1030 let mut stats = IntervalStats::default();
1031 for (right, unit) in units.iter().enumerate().skip(left) {
1032 stats.num_files += unit.files.len();
1033 if stats.num_files > max_input_files {
1034 break;
1035 }
1036 stats.size += unit.size;
1037 stats.largest_file_size = stats.largest_file_size.max(unit.largest_file_size);
1038 stats.conflicts += unit.conflicts;
1039 stats.single_key = if right == left {
1040 unit.single_key()
1041 } else {
1042 stats
1043 .single_key
1044 .filter(|&key| unit.single_key() == Some(key))
1045 };
1046 if let Some(neighbors) = split_conflicts.get(right) {
1047 stats.conflicts += neighbors.partition_point(|&p| p < right)
1049 - neighbors.partition_point(|&p| p < left);
1050 }
1051 visit(left..right + 1, &stats);
1052 }
1053 }
1054}
1055
1056fn adds_no_conflicts(
1061 units: &[Unit],
1062 extents: &[Extent],
1063 index: &ExtentIndex,
1064 outputs: usize,
1065) -> bool {
1066 let area = units[1..]
1067 .iter()
1068 .fold(units[0].extent.clone(), |area, unit| {
1069 area.union(&unit.extent)
1070 });
1071 let selected: std::collections::HashSet<usize> = units
1072 .iter()
1073 .flat_map(|unit| unit.files.iter().copied())
1074 .collect();
1075 let mut intruders = Vec::new();
1076 index.for_each_near(&area, |i| {
1077 if !selected.contains(&i) && extents[i].intrudes(&area) {
1078 intruders.push(i);
1079 }
1080 });
1081 if intruders.is_empty() {
1082 return true;
1083 }
1084 let existing: usize = intruders
1085 .iter()
1086 .map(|&other| {
1087 units
1088 .iter()
1089 .flat_map(|unit| unit.files.iter())
1090 .filter(|&&i| extents[i].conflicts(&extents[other]))
1091 .count()
1092 })
1093 .sum();
1094 outputs.saturating_mul(intruders.len()) <= existing
1095}
1096
1097fn has_balanced_components(
1102 units: &[Unit],
1103 files: &[FileHandle],
1104 extents: &[Extent],
1105 tolerance: u64,
1106) -> bool {
1107 let members: Vec<usize> = units
1108 .iter()
1109 .flat_map(|unit| unit.files.iter().copied())
1110 .collect();
1111 let mut sets = DisjointSet::new(members.len());
1112 for (i, &lhs) in members.iter().enumerate() {
1113 for (j, &rhs) in members.iter().enumerate().skip(i + 1) {
1114 if extents[lhs].conflicts(&extents[rhs]) {
1115 sets.union(i, j);
1116 }
1117 }
1118 }
1119 sets.groups()
1120 .into_iter()
1121 .filter(|group| group.len() > 1)
1122 .all(|group| {
1123 has_balanced_runs(
1124 group.into_iter().map(|i| members[i]),
1125 files,
1126 extents,
1127 tolerance,
1128 )
1129 })
1130}
1131
1132fn has_balanced_runs(
1136 members: impl Iterator<Item = usize>,
1137 files: &[FileHandle],
1138 extents: &[Extent],
1139 tolerance: u64,
1140) -> bool {
1141 let mut members: Vec<usize> = members.collect();
1142 members.sort_by(|&lhs, &rhs| {
1143 (extents[lhs].start, Reverse(extents[lhs].end), lhs).cmp(&(
1144 extents[rhs].start,
1145 Reverse(extents[rhs].end),
1146 rhs,
1147 ))
1148 });
1149 let mut runs: Vec<(Vec<usize>, u64)> = Vec::new();
1150 for i in members {
1151 let size = files[i].size();
1152 match runs.iter_mut().find(|(run, _)| {
1153 run.iter()
1154 .all(|&other| !extents[i].conflicts(&extents[other]))
1155 }) {
1156 Some((run, run_size)) => {
1157 run.push(i);
1158 *run_size += size;
1159 }
1160 None => runs.push((vec![i], size)),
1161 }
1162 }
1163 let total: u64 = runs.iter().map(|(_, size)| size).sum();
1164 let largest = runs.iter().map(|(_, size)| *size).max().unwrap_or(0);
1165 largest <= (total - largest).saturating_add(tolerance)
1166}
1167
1168fn collect_inputs(units: &[Unit], files: &[FileHandle]) -> Vec<FileHandle> {
1169 let mut indices: Vec<usize> = units
1170 .iter()
1171 .flat_map(|unit| unit.files.iter().copied())
1172 .collect();
1173 indices.sort_unstable();
1174 indices.into_iter().map(|i| files[i].clone()).collect()
1175}
1176
1177struct DisjointSet {
1179 parent: Vec<usize>,
1180}
1181
1182impl DisjointSet {
1183 fn new(len: usize) -> Self {
1184 Self {
1185 parent: (0..len).collect(),
1186 }
1187 }
1188
1189 fn find(&mut self, mut i: usize) -> usize {
1190 while self.parent[i] != i {
1191 self.parent[i] = self.parent[self.parent[i]];
1192 i = self.parent[i];
1193 }
1194 i
1195 }
1196
1197 fn union(&mut self, lhs: usize, rhs: usize) {
1198 let (lhs, rhs) = (self.find(lhs), self.find(rhs));
1199 let (root, child) = (lhs.min(rhs), lhs.max(rhs));
1200 self.parent[child] = root;
1201 }
1202
1203 fn groups(&mut self) -> Vec<Vec<usize>> {
1205 let mut groups: Vec<Vec<usize>> = Vec::new();
1206 let mut slot = vec![usize::MAX; self.parent.len()];
1207 for i in 0..self.parent.len() {
1208 let root = self.find(i);
1209 if slot[root] == usize::MAX {
1210 slot[root] = groups.len();
1211 groups.push(Vec::new());
1212 }
1213 groups[slot[root]].push(i);
1214 }
1215 groups
1216 }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use std::collections::HashSet;
1222
1223 use store_api::storage::FileId;
1224
1225 use super::*;
1226 use crate::compaction::test_util::{
1227 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
1228 primary_key_mapper_for_test,
1229 };
1230
1231 const MAX_OUTPUT: u64 = 16;
1232
1233 fn file(start: i64, end: i64, min: &[u8], max: &[u8], size: u64) -> FileHandle {
1234 new_file_handle_with_size_sequence_and_primary_key_range(
1235 FileId::random(),
1236 start,
1237 end,
1238 1,
1239 1,
1240 size,
1241 pk_range(min, max),
1242 )
1243 }
1244
1245 fn dense_files(count: usize) -> Vec<FileHandle> {
1247 (0..count)
1248 .map(|idx| file(0, 100, b"a", b"z", 1 + (idx % 3) as u64))
1249 .collect()
1250 }
1251
1252 fn chain_files(count: usize, size: u64) -> Vec<FileHandle> {
1254 (0..count)
1255 .map(|idx| {
1256 let min = format!("{idx:04}");
1257 let max = format!("{:04}", idx + 2);
1258 file(0, 100, min.as_bytes(), max.as_bytes(), size)
1259 })
1260 .collect()
1261 }
1262
1263 fn limits(max_output_file_size: u64) -> PickLimits {
1264 PickLimits {
1265 max_output_file_size: Some(max_output_file_size),
1266 max_input_files: 16,
1267 }
1268 }
1269
1270 fn closed_merge(files: &[FileHandle], max_output_file_size: u64) -> Option<UnitPick> {
1271 pick_closed_merge(
1272 files,
1273 &primary_key_mapper_for_test(),
1274 closed_merge_options(max_output_file_size),
1275 )
1276 }
1277
1278 fn closed_merge_with_budget(
1279 files: &[FileHandle],
1280 max_output_file_size: u64,
1281 work_budget: usize,
1282 ) -> Option<UnitPick> {
1283 pick_closed_merge_with_budget(
1284 files,
1285 &primary_key_mapper_for_test(),
1286 closed_merge_options(max_output_file_size),
1287 work_budget,
1288 )
1289 }
1290
1291 fn closed_merge_options(max_output_file_size: u64) -> ClosedMergeOptions {
1292 ClosedMergeOptions {
1293 limits: limits(max_output_file_size),
1294 allow_file_reduction: true,
1295 allow_conflicts: true,
1296 allow_unbalanced: false,
1297 }
1298 }
1299
1300 #[test]
1301 fn test_small_l1_reduction_limits() {
1302 let mapper = primary_key_mapper_for_test();
1303 for (sizes, max_output, max_inputs, expected) in [
1306 (vec![], Some(16), 16, 0),
1307 (vec![4], Some(16), 16, 0),
1308 (vec![4, 4], Some(16), 16, 2),
1309 (vec![5, 5], Some(16), 16, 0),
1310 (vec![4, 1], Some(16), 16, 0),
1311 (vec![4, 2, 2], Some(16), 16, 3),
1312 (vec![4; 8], Some(16), 16, 4),
1313 (vec![4; 8], Some(16), 3, 3),
1314 (vec![4, 4], Some(16), 1, 0),
1315 (vec![4, 4], Some(0), 16, 0),
1316 (vec![4, 4], None, 16, 0),
1317 ] {
1318 let files = sizes
1319 .iter()
1320 .map(|&size| file(0, 100, b"a", b"z", size))
1321 .collect::<Vec<_>>();
1322 let pick = pick_small_l1(
1323 &files,
1324 &mapper,
1325 PickLimits {
1326 max_output_file_size: max_output,
1327 max_input_files: max_inputs,
1328 },
1329 );
1330 assert_eq!(
1331 expected,
1332 pick.map_or(0, |pick| pick.inputs.len()),
1333 "sizes={sizes:?}, target={max_output:?}, cap={max_inputs}"
1334 );
1335 }
1336 }
1337
1338 #[test]
1339 fn test_small_l1_checks_unselected_large_and_l0_files() {
1340 let mapper = primary_key_mapper_for_test();
1341 let small = [file(0, 10, b"a", b"b", 4), file(90, 100, b"y", b"z", 4)];
1342 for level in [0, 1] {
1343 for (min, max, expected) in [(b"m", b"n", false), (b"a", b"z", true)] {
1344 let peer = new_file_handle_with_size_sequence_and_primary_key_range(
1345 FileId::random(),
1346 0,
1347 100,
1348 level,
1349 1,
1350 100,
1351 pk_range(min, max),
1352 );
1353 let files = [small.to_vec(), vec![peer]].concat();
1354 let pick = pick_small_l1(&files, &mapper, limits(16));
1355 assert_eq!(
1356 expected,
1357 pick.is_some(),
1358 "level={level}, range={min:?}..{max:?}"
1359 );
1360 if let Some(pick) = pick {
1361 assert_eq!(ids(&small), ids(&pick.inputs));
1362 }
1363 }
1364 }
1365 let l0 = new_file_handle_with_size_sequence_and_primary_key_range(
1367 FileId::random(),
1368 90,
1369 100,
1370 0,
1371 1,
1372 4,
1373 pk_range(b"y", b"z"),
1374 );
1375 assert!(pick_small_l1(&[small[0].clone(), l0], &mapper, limits(16)).is_none());
1376 }
1377
1378 fn ids(files: &[FileHandle]) -> HashSet<FileId> {
1379 files.iter().map(|file| file.file_id().file_id()).collect()
1380 }
1381
1382 fn extent(start: i64, end: i64, min: &[u8], max: &[u8]) -> Extent {
1383 Extent::of_file(
1384 &file(start, end, min, max, 1),
1385 &primary_key_mapper_for_test(),
1386 )
1387 }
1388
1389 #[test]
1390 fn test_conflicts_include_shared_boundary_keys() {
1391 assert!(extent(0, 10, b"a", b"m").conflicts(&extent(0, 10, b"m", b"z")));
1393 assert!(extent(0, 10, b"m", b"m").conflicts(&extent(0, 10, b"m", b"z")));
1394 assert!(!extent(0, 10, b"a", b"l").conflicts(&extent(0, 10, b"m", b"z")));
1396 assert!(extent(0, 10, b"m", b"m").conflicts(&extent(5, 15, b"m", b"m")));
1398 assert!(extent(0, 10, b"m", b"m").conflicts(&extent(0, 10, b"a", b"z")));
1400 assert!(extent(0, 10, b"a", b"z").conflicts(&extent(0, 10, b"m", b"m")));
1401 assert!(extent(0, 10, b"a", b"n").conflicts(&extent(0, 10, b"m", b"z")));
1402 assert!(!extent(0, 10, b"a", b"z").conflicts(&extent(10, 20, b"a", b"z")));
1404 }
1405
1406 #[test]
1407 fn test_intrusion_covers_every_conflict_inside_an_area() {
1408 let area = extent(0, 10, b"m", b"z");
1409 assert!(extent(0, 10, b"m", b"m").intrudes(&area));
1411 assert!(extent(0, 10, b"a", b"m").intrudes(&area));
1412 assert!(!extent(0, 10, b"a", b"l").intrudes(&area));
1413 assert!(extent(0, 10, b"a", b"n").intrudes(&area));
1414 assert!(!extent(10, 20, b"a", b"z").intrudes(&area));
1415 let point = extent(0, 10, b"m", b"m");
1417 assert!(extent(0, 10, b"a", b"z").intrudes(&point));
1418 assert!(extent(0, 10, b"a", b"m").intrudes(&point));
1419 }
1420
1421 #[test]
1422 fn test_intrusion_covers_conflicts_of_random_files() {
1423 let mut rng = Rng(23);
1424 let random_extent = |rng: &mut Rng| {
1425 let start = rng.next(10) as i64;
1426 let end = start + 1 + rng.next(10) as i64;
1427 let min = rng.next(6) as u8;
1428 let max = min + rng.next(3) as u8;
1429 extent(start, end, &[b'0' + min], &[b'0' + max])
1430 };
1431 for _ in 0..2_000 {
1432 let group: Vec<_> = (0..1 + rng.next(3))
1433 .map(|_| random_extent(&mut rng))
1434 .collect();
1435 let area = group[1..]
1436 .iter()
1437 .fold(group[0].clone(), |area, extent| area.union(extent));
1438 let other = random_extent(&mut rng);
1439 if group.iter().any(|extent| other.conflicts(extent)) {
1440 assert!(other.intrudes(&area), "{other:?} conflicts inside {area:?}");
1441 }
1442 }
1443 }
1444
1445 #[test]
1446 fn test_extent_index_budget_counts_nodes() {
1447 let files = vec![file(0, 10, b"a", b"z", 1), file(90, 100, b"a", b"z", 1)];
1450 let extents = extents_of(&files, &primary_key_mapper_for_test());
1451 let index = ExtentIndex::new(&extents);
1452 let query = extent(50, 51, b"a", b"z");
1453 let mut matched = 0;
1454
1455 let mut budget = 2;
1456 assert!(!index.for_each_near_with_budget(&query, &mut budget, |_| matched += 1));
1457 assert_eq!(0, budget);
1458 assert_eq!(0, matched);
1459
1460 let mut budget = 3;
1461 assert!(index.for_each_near_with_budget(&query, &mut budget, |_| matched += 1));
1462 assert_eq!(0, budget);
1463 assert_eq!(0, matched);
1464 }
1465
1466 #[test]
1467 fn test_extent_index_matches_linear_scan() {
1468 let mut rng = Rng(29);
1469 let mapper = primary_key_mapper_for_test();
1470 for count in [0, 1, 2, 7, 64, 200] {
1471 let extents: Vec<_> = (0..count)
1472 .map(|_| {
1473 let start = rng.next(100) as i64;
1474 let end = start + rng.next(30) as i64;
1475 let min = rng.next(50) as u8;
1476 let max = min + rng.next(10) as u8;
1477 let range = if rng.next(10) > 0 {
1478 pk_range(&[b'0' + min], &[b'0' + max])
1479 } else {
1480 None
1481 };
1482 let file = new_file_handle_with_size_sequence_and_primary_key_range(
1483 FileId::random(),
1484 start,
1485 end,
1486 1,
1487 1,
1488 1,
1489 range,
1490 );
1491 Extent::of_file(&file, &mapper)
1492 })
1493 .collect();
1494 let index = ExtentIndex::new(&extents);
1495 for area in &extents {
1496 let mut near = Vec::new();
1497 index.for_each_near(area, |i| near.push(i));
1498 near.sort_unstable();
1499 let expected: Vec<_> = (0..extents.len())
1500 .filter(|&i| {
1501 let other = &extents[i];
1502 let keys_meet = match (&other.primary_key, &area.primary_key) {
1503 (Some(lhs), Some(rhs)) => lhs.0 <= rhs.1 && rhs.0 <= lhs.1,
1504 _ => true,
1505 };
1506 keys_meet && other.time_overlaps(area)
1507 })
1508 .collect();
1509 assert_eq!(expected, near, "{area:?}");
1510 for (i, other) in extents.iter().enumerate() {
1511 if other.conflicts(area) || other.intrudes(area) {
1512 assert!(near.binary_search(&i).is_ok(), "{other:?} near {area:?}");
1513 }
1514 }
1515 }
1516 }
1517 }
1518
1519 #[test]
1520 fn test_active_l1_packs_pk_disjoint_files() {
1521 let files = vec![
1522 file(0, 1000, b"a", b"a", 100),
1523 file(0, 1000, b"z", b"z", 100),
1524 ];
1525 let pick = pick_active_l1(&files, &primary_key_mapper_for_test(), limits(512)).unwrap();
1526 assert_eq!(ids(&files), ids(&pick.inputs));
1527 }
1528
1529 #[test]
1530 fn test_active_l1_bounds_rewrite_cost() {
1531 let overlapping = |sizes: &[u64]| {
1532 sizes
1533 .iter()
1534 .enumerate()
1535 .map(|(idx, &size)| file(idx as i64, 100, b"a", b"z", size))
1536 .collect::<Vec<_>>()
1537 };
1538 let mapper = primary_key_mapper_for_test();
1539 assert!(pick_active_l1(&overlapping(&[300, 300, 300]), &mapper, limits(512)).is_some());
1540 let costly = overlapping(&[500, 500, 500, 500, 500, 500, 500, 500, 500, 100]);
1542 assert!(pick_active_l1(&costly, &mapper, limits(512)).is_none());
1543 }
1544
1545 #[test]
1546 fn test_active_l1_merges_small_files_beside_dominant_file() {
1547 let mut files = vec![file(0, 1000, b"a", b"z", 1024)];
1550 files.extend((1..16).map(|start| file(start, 1000, b"a", b"z", 1)));
1551 let pick = pick_active_l1(&files, &primary_key_mapper_for_test(), limits(512)).unwrap();
1552 assert_eq!(ids(&files[1..]), ids(&pick.inputs));
1553 }
1554
1555 #[test]
1556 fn test_merges_of_a_single_key_write_one_file() {
1557 let files = vec![file(0, 100, b"m", b"m", 32), file(50, 150, b"m", b"m", 32)];
1560 let mapper = primary_key_mapper_for_test();
1561 let pick = closed_merge(&files, MAX_OUTPUT).unwrap();
1562 assert_eq!(ids(&files), ids(&pick.inputs));
1563 assert!(pick.reduces_files);
1564 let pick = pick_active_l1(&files, &mapper, limits(MAX_OUTPUT)).unwrap();
1565 assert_eq!(ids(&files), ids(&pick.inputs));
1566 }
1567
1568 #[test]
1569 fn test_closed_merge_does_not_enclose_other_files() {
1570 let a = file(0, 10, b"a", b"c", 10);
1571 let b = file(0, 10, b"x", b"z", 10);
1572 let c = file(0, 10, b"m", b"m", 1);
1573 let files = vec![a.clone(), b, c.clone()];
1574 let pick = closed_merge(&files, MAX_OUTPUT).unwrap();
1575 assert_eq!(ids(&[a, c]), ids(&pick.inputs));
1577 assert!(pick.reduces_files);
1578 }
1579
1580 #[test]
1581 fn test_shared_boundary_files_are_picked_without_file_reduction() {
1582 let size = 512 * 1024 * 1024;
1585 let files = [
1586 file(0, 100, b"a", b"m", size),
1587 file(0, 100, b"m", b"z", size),
1588 ];
1589 let pick = closed_merge(&files, size).unwrap();
1590 assert_eq!(ids(&files), ids(&pick.inputs));
1591 assert!(!pick.reduces_files);
1592 }
1593
1594 fn run_with_late_files(
1597 sizes: &[u64],
1598 late: &[(i64, i64)],
1599 ) -> (Vec<FileHandle>, Vec<FileHandle>) {
1600 let run = sizes
1601 .iter()
1602 .enumerate()
1603 .map(|(idx, &size)| {
1604 let idx = 2 * idx as u8;
1605 file(0, 1000, &[b'a', idx], &[b'a', idx + 1], size)
1606 })
1607 .collect::<Vec<_>>();
1608 let max_key = 2 * sizes.len() as u8;
1609 let late = late
1610 .iter()
1611 .map(|&(start, end)| file(start, end, &[b'a', 0], &[b'a', max_key], 1))
1612 .collect();
1613 (run, late)
1614 }
1615
1616 #[test]
1617 fn test_closed_merge_splits_components_over_input_limit() {
1618 let (run, late) = run_with_late_files(&[1; 20], &[(100, 900)]);
1621 let files = [run, late].concat();
1622 let pick = closed_merge(&files, MAX_OUTPUT).unwrap();
1623 assert!(pick.reduces_files);
1624 assert!(pick.inputs.len() <= 16);
1625 }
1626
1627 #[test]
1628 fn test_closed_merge_leaves_late_file_over_large_run() {
1629 let (run, late) = run_with_late_files(&[MAX_OUTPUT; 20], &[(100, 900)]);
1632 let files = [run, late].concat();
1633 assert!(closed_merge(&files, MAX_OUTPUT).is_none());
1634 }
1635
1636 #[test]
1637 fn test_closed_merge_compacts_late_files_over_large_run() {
1638 let (run, late) =
1639 run_with_late_files(&[MAX_OUTPUT; 20], &[(100, 200), (300, 400), (500, 600)]);
1640 let files = [run, late.clone()].concat();
1641 let pick = closed_merge(&files, MAX_OUTPUT).unwrap();
1642 assert_eq!(ids(&late), ids(&pick.inputs));
1643 assert!(pick.reduces_files);
1644 }
1645
1646 #[test]
1647 fn test_closed_merge_rejects_dominant_runs() {
1648 let size = 512 * 1024 * 1024;
1650 let mut files = (0..6u8)
1652 .map(|idx| file(idx.into(), 10_000, &[b'b' + idx], &[b'b' + idx], size))
1653 .collect::<Vec<_>>();
1654 files.push(file(6, 10_000, b"a", b"h", size));
1655 files.extend((0..6u8).map(|idx| {
1656 file(
1657 8 + i64::from(idx),
1658 10_000,
1659 &[b'n' + idx],
1660 &[b'n' + idx],
1661 size,
1662 )
1663 }));
1664 files.push(file(7, 10_000, b"m", b"t", size));
1665 let pair = [
1667 file(14, 10_000, b"x", b"y", size),
1668 file(15, 10_000, b"x", b"y", size),
1669 ];
1670 files.extend(pair.clone());
1671
1672 let pick = closed_merge(&files, size).unwrap();
1673 assert_eq!(ids(&pair), ids(&pick.inputs));
1674 assert!(!pick.reduces_files);
1675 }
1676
1677 #[test]
1678 fn test_closed_merge_ignores_touching_time_ranges() {
1679 let first = file(0, 10, b"a", b"z", 1024);
1680 let pair = [file(10, 20, b"a", b"z", 512), file(11, 19, b"a", b"z", 512)];
1681 let mut files = vec![first];
1682 files.extend(pair.clone());
1683 let pick = closed_merge(&files, 512).unwrap();
1684 assert_eq!(ids(&pair), ids(&pick.inputs));
1685 }
1686
1687 #[test]
1688 fn test_conflict_units_respects_work_budget() {
1689 let files = dense_files(20);
1690 let mapper = primary_key_mapper_for_test();
1691 let extents = extents_of(&files, &mapper);
1692 let index = ExtentIndex::new(&extents);
1693
1694 assert!(conflict_units(&files, &extents, &index, 0).is_none());
1695 assert!(conflict_units(&files, &extents, &index, usize::MAX).is_some());
1696 }
1697
1698 #[test]
1699 fn test_split_conflicts_respects_work_budget() {
1700 let files = dense_files(20);
1701 let mapper = primary_key_mapper_for_test();
1702 let extents = extents_of(&files, &mapper);
1703 let index = ExtentIndex::new(&extents);
1704 let mut units = conflict_units(&files, &extents, &index, usize::MAX).unwrap();
1705 close_units(&mut units, &extents, &index);
1706 split_oversized_units(&mut units, &files, &extents, 16);
1707 sort_units(&mut units);
1708
1709 assert_eq!(20, units.iter().filter(|unit| unit.split).count());
1710 assert!(split_conflicts(&units, &extents, &index, 0).is_none());
1711 assert!(split_conflicts(&units, &extents, &index, usize::MAX).is_some());
1712 }
1713
1714 #[test]
1715 fn test_interval_stats_exact_matches_for_each_interval() {
1716 let mapper = primary_key_mapper_for_test();
1717 let mut files = (0..10)
1720 .map(|_| file(0, 100, b"a", b"c", 1))
1721 .collect::<Vec<_>>();
1722 files.extend((0..20).map(|_| file(0, 100, b"x", b"z", 1)));
1723 let extents = extents_of(&files, &mapper);
1724 let index = ExtentIndex::new(&extents);
1725 let mut units = conflict_units(&files, &extents, &index, usize::MAX).unwrap();
1726 close_units(&mut units, &extents, &index);
1727 split_oversized_units(&mut units, &files, &extents, 16);
1728 sort_units(&mut units);
1729
1730 let pairs = split_conflicts(&units, &extents, &index, usize::MAX).unwrap();
1731 for_each_interval(&units, &pairs, 16, |range, stats| {
1732 let exact = interval_stats_exact(&units[range], &extents);
1733 assert_eq!(stats.num_files, exact.num_files);
1734 assert_eq!(stats.size, exact.size);
1735 assert_eq!(stats.largest_file_size, exact.largest_file_size);
1736 assert_eq!(stats.conflicts, exact.conflicts);
1737 assert_eq!(stats.single_key, exact.single_key);
1738 });
1739 }
1740
1741 #[test]
1742 fn test_active_l1_falls_back_on_dense_conflict_graph() {
1743 let files = dense_files(300);
1744 let pick = pick_active_l1_with_budget(
1745 &files,
1746 &primary_key_mapper_for_test(),
1747 limits(MAX_OUTPUT),
1748 1 << 10,
1749 )
1750 .unwrap();
1751 assert!(!pick.dense_fallback);
1752 assert!(pick.reduces_files);
1753 assert!((2..=16).contains(&pick.inputs.len()));
1754 }
1755
1756 #[rstest::rstest]
1757 #[case::both(true, true)]
1758 #[case::only_reduction(true, false)]
1759 #[case::only_overlap(false, true)]
1760 #[case::neither(false, false)]
1761 fn test_closed_merge_discovers_classes_independently(
1762 #[case] allow_reduction: bool,
1763 #[case] allow_conflicts: bool,
1764 ) {
1765 let files = [
1769 file(0, 10, b"a", b"b", 4),
1770 file(20, 30, b"a", b"b", 4),
1771 file(0, 30, b"c", b"d", 16),
1772 file(0, 30, b"c", b"d", 16),
1773 ];
1774 let mapper = primary_key_mapper_for_test();
1775 let options = ClosedMergeOptions {
1776 limits: PickLimits {
1777 max_input_files: 2,
1778 ..limits(MAX_OUTPUT)
1779 },
1780 allow_file_reduction: allow_reduction,
1781 allow_conflicts,
1782 allow_unbalanced: false,
1783 };
1784 let [reducing, overlap] = pick_closed_merge_by_class(&files, &mapper, options);
1785 assert_eq!(allow_reduction, reducing.is_some());
1786 assert_eq!(allow_conflicts, overlap.is_some());
1787 if let Some(pick) = reducing {
1788 assert!(pick.reduces_files);
1789 assert_eq!(ids(&files[..2]), ids(&pick.inputs));
1790 }
1791 if let Some(pick) = overlap {
1792 assert!(!pick.reduces_files);
1793 assert_eq!(ids(&files[2..]), ids(&pick.inputs));
1794 }
1795 let best = pick_closed_merge(&files, &mapper, options);
1796 let expected = if allow_reduction {
1797 Some(ids(&files[..2]))
1798 } else if allow_conflicts {
1799 Some(ids(&files[2..]))
1800 } else {
1801 None
1802 };
1803 assert_eq!(expected, best.map(|pick| ids(&pick.inputs)));
1804 }
1805
1806 #[rstest::rstest]
1807 #[case::empty(0)]
1808 #[case::single(1)]
1809 fn test_closed_merge_classes_without_enough_inputs(#[case] count: usize) {
1810 let files = dense_files(count);
1811 let picks = pick_closed_merge_by_class(
1812 &files,
1813 &primary_key_mapper_for_test(),
1814 closed_merge_options(MAX_OUTPUT),
1815 );
1816 assert!(picks.into_iter().all(|pick| pick.is_none()));
1817 }
1818
1819 #[test]
1820 fn test_closed_merge_classes_share_dense_reduction() {
1821 let files = dense_files(300);
1822 let [reducing, overlap] = pick_closed_merge_candidates(
1823 &files,
1824 &primary_key_mapper_for_test(),
1825 closed_merge_options(MAX_OUTPUT),
1826 1 << 10,
1827 true,
1828 );
1829 let pick = reducing.unwrap();
1830 assert!(pick.dense_fallback);
1831 assert!(pick.reduces_files);
1832 assert!(overlap.is_none());
1833 let legacy = closed_merge_with_budget(&files, MAX_OUTPUT, 1 << 10).unwrap();
1834 assert!(legacy.dense_fallback);
1835 assert!(legacy.reduces_files);
1836 assert!((2..=16).contains(&legacy.inputs.len()));
1837 assert_eq!(ids(&legacy.inputs), ids(&pick.inputs));
1838 }
1839
1840 #[test]
1841 fn test_dense_closed_merge_resolves_conflicts_without_file_reduction_setting() {
1842 let files = dense_files(300);
1843 let mapper = primary_key_mapper_for_test();
1844 let options = ClosedMergeOptions {
1845 allow_file_reduction: false,
1846 ..closed_merge_options(MAX_OUTPUT)
1847 };
1848 let pick = pick_closed_merge_with_budget(&files, &mapper, options, 1 << 10).unwrap();
1849 assert!(pick.dense_fallback);
1850 assert!(pick.reduces_files);
1851 }
1852
1853 #[test]
1854 fn test_dense_closed_merge_defers_when_conflicts_are_not_allowed() {
1855 let files = dense_files(300);
1856 let mapper = primary_key_mapper_for_test();
1857 let options = ClosedMergeOptions {
1858 allow_conflicts: false,
1859 ..closed_merge_options(MAX_OUTPUT)
1860 };
1861 assert!(pick_closed_merge_with_budget(&files, &mapper, options, 1 << 10).is_none());
1862 }
1863
1864 #[test]
1865 fn test_dense_closed_merge_retries_one_output_interval() {
1866 let files = chain_files(300, 2);
1870 let mapper = primary_key_mapper_for_test();
1871 let extents = extents_of(&files, &mapper);
1872 let index = ExtentIndex::new(&extents);
1873 let mut units = conflict_units(&files, &extents, &index, usize::MAX).unwrap();
1874 close_units(&mut units, &extents, &index);
1875 split_oversized_units(&mut units, &files, &extents, 16);
1876 sort_units(&mut units);
1877
1878 assert!(!adds_no_conflicts(&units[..16], &extents, &index, 2));
1879 assert!(adds_no_conflicts(&units[..8], &extents, &index, 1));
1880 for pick in [
1883 closed_merge_with_budget(&files, MAX_OUTPUT, 0).unwrap(),
1884 pick_dense_closed_merge(
1885 &units,
1886 &files,
1887 &extents,
1888 &index,
1889 closed_merge_options(MAX_OUTPUT),
1890 )
1891 .unwrap(),
1892 ] {
1893 assert_eq!(ids(&files[..8]), ids(&pick.inputs));
1894 assert!(pick.dense_fallback);
1895 assert!(pick.reduces_files);
1896 assert_eq!(300, pick.units);
1897 }
1898 }
1899
1900 #[test]
1901 fn test_dense_closed_merge_rejects_unsafe_one_output_retry() {
1902 let mut files: Vec<_> = (0..4)
1905 .map(|i| file(30 * i, 30 * i + 10, b"a", b"z", 6))
1906 .collect();
1907 files.extend((0..3).map(|i| file(30 * i + 15, 30 * i + 25, b"b", b"y", 1_000)));
1908
1909 assert!(closed_merge_with_budget(&files, MAX_OUTPUT, 0).is_none());
1910 }
1911
1912 struct Rng(u64);
1914
1915 impl Rng {
1916 fn next(&mut self, bound: u64) -> u64 {
1917 self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
1918 let mut z = self.0;
1919 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
1920 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
1921 (z ^ (z >> 31)) % bound
1922 }
1923 }
1924
1925 #[derive(Clone)]
1927 struct SimFile {
1928 handle: FileHandle,
1929 min: u64,
1930 max: u64,
1931 }
1932
1933 fn sim_file(start: i64, end: i64, min: u64, max: u64, size: u64) -> SimFile {
1934 let (min_key, max_key) = (format!("{min:04}"), format!("{max:04}"));
1935 SimFile {
1936 handle: file(start, end, min_key.as_bytes(), max_key.as_bytes(), size),
1937 min,
1938 max,
1939 }
1940 }
1941
1942 fn merge(inputs: &[SimFile], rng: &mut Rng) -> Vec<SimFile> {
1946 let min = inputs.iter().map(|file| file.min).min().unwrap();
1947 let max = inputs.iter().map(|file| file.max).max().unwrap();
1948 let start = inputs
1949 .iter()
1950 .map(|file| file.handle.time_range().0.value())
1951 .min()
1952 .unwrap();
1953 let end = inputs
1954 .iter()
1955 .map(|file| file.handle.time_range().1.value())
1956 .max()
1957 .unwrap();
1958 let size: u64 = inputs.iter().map(|file| file.handle.size()).sum();
1959 let predicted = if min == max {
1962 1
1963 } else {
1964 size.div_ceil(MAX_OUTPUT).max(1)
1965 };
1966 let keys = max - min + 1;
1967 let outputs = (predicted - rng.next(2).min(predicted - 1)).min(keys);
1968 (0..outputs)
1969 .map(|i| {
1970 let first_key = min + keys * i / outputs;
1971 let last_key = min + keys * (i + 1) / outputs - 1;
1972 let output_size = size * (i + 1) / outputs - size * i / outputs;
1973 sim_file(start, end, first_key, last_key, output_size)
1974 })
1975 .collect()
1976 }
1977
1978 fn conflict_pairs(files: &[FileHandle]) -> usize {
1979 let extents = extents_of(files, &primary_key_mapper_for_test());
1980 (0..extents.len())
1981 .map(|i| {
1982 (i + 1..extents.len())
1983 .filter(|&j| extents[i].conflicts(&extents[j]))
1984 .count()
1985 })
1986 .sum()
1987 }
1988
1989 fn random_window(rng: &mut Rng) -> Vec<SimFile> {
1990 (0..2 + rng.next(30))
1991 .map(|_| {
1992 let start = rng.next(3_000) as i64;
1993 let end = start + 1 + rng.next(600) as i64;
1994 let min = rng.next(200);
1995 let span = if rng.next(4) == 0 { 200 } else { 20 };
1996 let max = min + rng.next(span);
1997 sim_file(start, end, min, max, 1 + rng.next(2 * MAX_OUTPUT))
1998 })
1999 .collect()
2000 }
2001
2002 fn oversized_window(rng: &mut Rng) -> Vec<SimFile> {
2005 let run_files = 17 + rng.next(40);
2006 let mut files: Vec<_> = (0..run_files)
2007 .map(|idx| {
2008 let start = rng.next(100) as i64;
2009 let end = 2_900 + rng.next(100) as i64;
2010 sim_file(
2011 start,
2012 end,
2013 5 * idx,
2014 5 * (idx + 1) - 1,
2015 1 + rng.next(2 * MAX_OUTPUT),
2016 )
2017 })
2018 .collect();
2019 files.extend((0..1 + rng.next(6)).map(|_| {
2020 let start = rng.next(2_800) as i64;
2021 let end = start + 1 + rng.next(200) as i64;
2022 let min = rng.next(5 * run_files / 2);
2023 let max = min + 5 * run_files / 2 + rng.next(5 * run_files / 2);
2024 sim_file(start, end, min, max, 1 + rng.next(MAX_OUTPUT))
2025 }));
2026 files
2027 }
2028
2029 #[test]
2030 fn test_dense_closed_merge_terminates() {
2031 let mut rng = Rng(23);
2032 let mut files: Vec<_> = (0..300)
2033 .map(|_| sim_file(0, 100, 0, 8, 1 + rng.next(2 * MAX_OUTPUT)))
2034 .collect();
2035 let handles = |files: &[SimFile]| {
2036 files
2037 .iter()
2038 .map(|file| file.handle.clone())
2039 .collect::<Vec<_>>()
2040 };
2041 let mut current = (conflict_pairs(&handles(&files)), files.len());
2042 let mut steps = 0;
2043 let mut dense_steps = 0;
2044 while let Some(pick) = closed_merge_with_budget(&handles(&files), MAX_OUTPUT, 1 << 10) {
2045 assert!(pick.inputs.len() >= 2);
2046 if pick.dense_fallback {
2047 dense_steps += 1;
2048 }
2049 let picked = ids(&pick.inputs);
2050 let (inputs, kept): (Vec<_>, Vec<_>) = files
2051 .into_iter()
2052 .partition(|file| picked.contains(&file.handle.file_id().file_id()));
2053 files = kept;
2054 files.extend(merge(&inputs, &mut rng));
2055 let next = (conflict_pairs(&handles(&files)), files.len());
2056 assert!(next < current, "step {steps}: {current:?} -> {next:?}");
2057 current = next;
2058 steps += 1;
2059 assert!(steps < 1_000);
2060 }
2061 assert!(dense_steps > 0);
2062 assert!(files.len() < 300);
2063 }
2064
2065 fn run_to_fixpoint(
2068 rng: &mut Rng,
2069 pick: impl Fn(&[FileHandle]) -> Option<UnitPick>,
2070 measure: impl Fn(&[FileHandle]) -> (usize, usize),
2071 ) -> usize {
2072 run_windows_to_fixpoint(rng, random_window, pick, measure)
2073 }
2074
2075 fn run_windows_to_fixpoint(
2076 rng: &mut Rng,
2077 new_window: impl Fn(&mut Rng) -> Vec<SimFile>,
2078 pick: impl Fn(&[FileHandle]) -> Option<UnitPick>,
2079 measure: impl Fn(&[FileHandle]) -> (usize, usize),
2080 ) -> usize {
2081 let mut total_steps = 0;
2082 for window in 0..300 {
2083 let mut files = new_window(rng);
2084 let handles =
2085 |files: &[SimFile]| files.iter().map(|f| f.handle.clone()).collect::<Vec<_>>();
2086 let mut current = measure(&handles(&files));
2087 let mut steps = 0;
2088 while let Some(pick) = pick(&handles(&files)) {
2089 assert!(pick.inputs.len() >= 2);
2090 let picked = ids(&pick.inputs);
2091 let (inputs, kept): (Vec<_>, Vec<_>) = files
2092 .into_iter()
2093 .partition(|file| picked.contains(&file.handle.file_id().file_id()));
2094 files = kept;
2095 files.extend(merge(&inputs, rng));
2096 let next = measure(&handles(&files));
2097 assert!(next < current, "window {window}: {current:?} -> {next:?}");
2098 current = next;
2099 steps += 1;
2100 }
2101 total_steps += steps;
2102 }
2103 total_steps
2104 }
2105
2106 #[test]
2107 fn test_closed_merges_terminate() {
2108 let steps = run_to_fixpoint(
2109 &mut Rng(7),
2110 |files| closed_merge(files, MAX_OUTPUT),
2111 |files| (conflict_pairs(files), files.len()),
2112 );
2113 assert!(steps > 300);
2114 }
2115
2116 #[test]
2117 fn test_closed_merges_of_oversized_components_terminate() {
2118 let steps = run_windows_to_fixpoint(
2119 &mut Rng(19),
2120 oversized_window,
2121 |files| closed_merge(files, MAX_OUTPUT),
2122 |files| (conflict_pairs(files), files.len()),
2123 );
2124 assert!(steps > 300);
2125 }
2126
2127 #[test]
2128 fn test_closed_merges_of_quiet_and_settling_windows_terminate() {
2129 let mapper = primary_key_mapper_for_test();
2130 for (seed, options, more_merges) in [
2133 (
2134 13,
2135 ClosedMergeOptions {
2136 allow_unbalanced: true,
2137 ..closed_merge_options(MAX_OUTPUT)
2138 },
2139 true,
2140 ),
2141 (
2142 17,
2143 ClosedMergeOptions {
2144 allow_conflicts: false,
2145 ..closed_merge_options(MAX_OUTPUT)
2146 },
2147 false,
2148 ),
2149 ] {
2150 let steps = run_to_fixpoint(
2151 &mut Rng(seed),
2152 |files| pick_closed_merge(files, &mapper, options),
2153 |files| (conflict_pairs(files), files.len()),
2154 );
2155 let base = run_to_fixpoint(
2156 &mut Rng(seed),
2157 |files| closed_merge(files, MAX_OUTPUT),
2158 |files| (conflict_pairs(files), files.len()),
2159 );
2160 assert_eq!(more_merges, steps > base, "seed {seed}: {steps} vs {base}");
2161 }
2162 }
2163
2164 #[test]
2165 fn test_small_l1_merges_terminate() {
2166 let mapper = primary_key_mapper_for_test();
2167 let steps = run_windows_to_fixpoint(
2168 &mut Rng(31),
2169 |rng| {
2170 let mut files = random_window(rng);
2171 files.push(sim_file(0, 3000, 0, 400, 64));
2174 files.extend((0..8).map(|_| sim_file(0, 100, 0, 8, 1 + rng.next(4))));
2175 files
2176 },
2177 |files| pick_small_l1(files, &mapper, limits(MAX_OUTPUT)),
2178 |files| (conflict_pairs(files), files.len()),
2179 );
2180 assert!(steps >= 300);
2181 }
2182
2183 #[test]
2184 fn test_active_l1_picks_terminate() {
2185 let mapper = primary_key_mapper_for_test();
2186 let steps = run_to_fixpoint(
2187 &mut Rng(11),
2188 |files| pick_active_l1(files, &mapper, limits(MAX_OUTPUT)),
2189 |files| (files.len(), 0),
2190 );
2191 assert!(steps > 200);
2192 }
2193}