Skip to main content

risingwave_meta/hummock/compaction/picker/
non_overlap_sub_level_picker.rs

1// Copyright 2026 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Write;
16use std::sync::Arc;
17
18use itertools::Itertools;
19use risingwave_hummock_sdk::append_sstable_info_to_string;
20use risingwave_hummock_sdk::key_range::KeyRange;
21use risingwave_hummock_sdk::level::Level;
22use risingwave_hummock_sdk::sstable_info::SstableInfo;
23use risingwave_pb::hummock::LevelType;
24
25use crate::hummock::compaction::overlap_strategy::{OverlapInfo, OverlapStrategy};
26use crate::hummock::level_handler::LevelHandler;
27
28#[derive(Default, Debug)]
29pub struct SubLevelSstables {
30    pub total_file_size: u64,
31    pub total_file_count: usize,
32    pub sstable_infos: Vec<(u64, Vec<SstableInfo>)>,
33    pub expected: bool,
34}
35
36pub struct NonOverlapSubLevelPicker {
37    min_compaction_bytes: u64,
38    max_compaction_bytes: u64,
39    min_expected_level_count: usize,
40    max_file_count: u64,
41    overlap_strategy: Arc<dyn OverlapStrategy>,
42    enable_check_task_level_overlap: bool,
43    max_expected_level_count: usize,
44    enable_optimize_l0_interval_selection: bool,
45}
46
47impl NonOverlapSubLevelPicker {
48    /// Pick the sub-level used to iterate candidate intervals:
49    /// - ignore overlapping sub-levels
50    /// - without optimization: choose the first non-overlapping sub-level
51    /// - with optimization: choose the non-overlapping sub-level with the most non-pending sstables
52    ///   (then by total file count, then by index)
53    ///   Returns `None` when no non-overlapping sub-level exists.
54    fn select_intervals<'a>(
55        &self,
56        l0: &'a [Level],
57        level_handler: &LevelHandler,
58    ) -> Option<&'a [SstableInfo]> {
59        if !self.enable_optimize_l0_interval_selection {
60            return l0.first().map(|level| {
61                debug_assert_eq!(level.level_type, LevelType::Nonoverlapping);
62                level.table_infos.as_slice()
63            });
64        }
65
66        if let [level] = l0 {
67            return (level.level_type == LevelType::Nonoverlapping)
68                .then_some(level.table_infos.as_slice());
69        }
70
71        let mut candidates = l0
72            .iter()
73            .enumerate()
74            .filter(|(_, level)| level.level_type == LevelType::Nonoverlapping)
75            .peekable();
76        let first = candidates.next()?;
77        // No pending lookups are needed when there is only one candidate.
78        if candidates.peek().is_none() {
79            return Some(&first.1.table_infos);
80        }
81        let level_idx = std::iter::once(first)
82            .chain(candidates)
83            .map(|(idx, level)| {
84                let non_pending = level
85                    .table_infos
86                    .iter()
87                    .filter(|sst| !level_handler.is_pending_compact(&sst.sst_id))
88                    .count();
89                (idx, non_pending, level.table_infos.len())
90            })
91            .max_by_key(|&(idx, non_pending, file_count)| {
92                (non_pending, file_count, std::cmp::Reverse(idx))
93            })
94            .map(|(idx, _, _)| idx)?;
95
96        Some(&l0[level_idx].table_infos)
97    }
98
99    pub fn new(
100        min_compaction_bytes: u64,
101        max_compaction_bytes: u64,
102        min_expected_level_count: usize,
103        max_file_count: u64,
104        overlap_strategy: Arc<dyn OverlapStrategy>,
105        enable_check_task_level_overlap: bool,
106        max_expected_level_count: usize,
107        enable_optimize_l0_interval_selection: bool,
108    ) -> Self {
109        Self {
110            min_compaction_bytes,
111            max_compaction_bytes,
112            min_expected_level_count,
113            max_file_count,
114            overlap_strategy,
115            enable_check_task_level_overlap,
116            max_expected_level_count,
117            enable_optimize_l0_interval_selection,
118        }
119    }
120
121    #[cfg(test)]
122    pub fn for_test(
123        min_compaction_bytes: u64,
124        max_compaction_bytes: u64,
125        min_expected_level_count: usize,
126        max_file_count: u64,
127        overlap_strategy: Arc<dyn OverlapStrategy>,
128        enable_check_task_level_overlap: bool,
129        max_expected_level_count: usize,
130        enable_optimize_l0_interval_selection: bool,
131    ) -> Self {
132        Self {
133            min_compaction_bytes,
134            max_compaction_bytes,
135            min_expected_level_count,
136            max_file_count,
137            overlap_strategy,
138            enable_check_task_level_overlap,
139            max_expected_level_count,
140            enable_optimize_l0_interval_selection,
141        }
142    }
143
144    /// Selects overlapping SSTs across multiple L0 sub-levels for a given key range.
145    ///
146    /// For each possible target sub-level, expands backwards (toward older sub-levels)
147    /// to collect overlapping SSTs. Returns the expansion covering the most sub-levels
148    /// within configured size and count limits.
149    ///
150    /// # Arguments
151    ///
152    /// * `levels` - L0 sub-levels ordered from oldest to newest
153    /// * `level_handler` - Tracks which SSTs are currently being compacted
154    /// * `key_range` - Initial key range to search for overlaps
155    ///
156    /// # Returns
157    ///
158    /// `Some` with selected SSTs grouped by sub-level, or `None` if no valid selection exists.
159    fn pick_sub_level(
160        &self,
161        levels: &[Level],
162        level_handler: &LevelHandler,
163        key_range: &KeyRange,
164    ) -> Option<SubLevelSstables> {
165        let mut ret = SubLevelSstables::default();
166        for sub_level in levels {
167            ret.sstable_infos.push((sub_level.sub_level_id, vec![]));
168        }
169
170        let mut pick_levels_range = Vec::default();
171        let mut max_select_level_count = 0;
172
173        // Pay attention to the order here: Make sure to select the lowest sub_level to meet the requirements of base compaction. If you break the assumption of this order, you need to redesign it.
174        // TODO: Use binary selection to replace the step algorithm to optimize algorithm complexity
175        'expand_new_level: for (target_index, target_level) in levels.iter().enumerate() {
176            if target_level.level_type != LevelType::Nonoverlapping {
177                break;
178            }
179
180            if ret
181                .sstable_infos
182                .iter()
183                .filter(|(_sub_level_id, ssts)| !ssts.is_empty())
184                .count()
185                > self.max_expected_level_count
186            {
187                break;
188            }
189
190            // reset the `basic_overlap_info` with basic sst
191            let mut basic_overlap_info = self.overlap_strategy.create_overlap_info();
192            basic_overlap_info.update(key_range);
193
194            let mut overlap_files_range =
195                basic_overlap_info.check_multiple_include(&target_level.table_infos);
196            if overlap_files_range.is_empty() {
197                overlap_files_range =
198                    basic_overlap_info.check_multiple_overlap(&target_level.table_infos);
199            }
200
201            if overlap_files_range.is_empty() {
202                continue;
203            }
204
205            let mut overlap_levels = vec![];
206
207            let mut add_files_size: u64 = 0;
208            let mut add_files_count: usize = 0;
209
210            let mut select_level_count = 0;
211            for reverse_index in (0..=target_index).rev() {
212                let target_tables = &levels[reverse_index].table_infos;
213
214                overlap_files_range = if target_index == reverse_index {
215                    overlap_files_range
216                } else {
217                    basic_overlap_info.check_multiple_overlap(target_tables)
218                };
219
220                // We allow a layer in the middle without overlap, so we need to continue to
221                // the next layer to search for overlap
222                if overlap_files_range.is_empty() {
223                    // empty level
224                    continue;
225                }
226
227                for other in &target_tables[overlap_files_range.clone()] {
228                    if level_handler.is_pending_compact(&other.sst_id) {
229                        break 'expand_new_level;
230                    }
231                    basic_overlap_info.update(&other.key_range);
232
233                    add_files_size += other.sst_size;
234                    add_files_count += 1;
235                }
236
237                overlap_levels.push((reverse_index, overlap_files_range.clone()));
238                select_level_count += 1;
239            }
240
241            if select_level_count > max_select_level_count {
242                max_select_level_count = select_level_count;
243                pick_levels_range = overlap_levels;
244            }
245
246            // When size / file count has exceeded the limit, we need to abandon this plan, it cannot be expanded to the last sub_level
247            if max_select_level_count >= self.min_expected_level_count
248                && (add_files_size >= self.max_compaction_bytes
249                    || add_files_count >= self.max_file_count as usize)
250            {
251                break 'expand_new_level;
252            }
253        }
254
255        if !pick_levels_range.is_empty() {
256            for (reverse_index, sst_range) in pick_levels_range {
257                let level_ssts = &levels[reverse_index].table_infos;
258                ret.sstable_infos[reverse_index].1 = level_ssts[sst_range].to_vec();
259                ret.total_file_count += ret.sstable_infos[reverse_index].1.len();
260                ret.total_file_size += ret.sstable_infos[reverse_index]
261                    .1
262                    .iter()
263                    .map(|sst| sst.sst_size)
264                    .sum::<u64>();
265            }
266
267            // sort sst per level due to reverse expand
268            ret.sstable_infos
269                .iter_mut()
270                .for_each(|(_sub_level_id, level_ssts)| {
271                    level_ssts.sort_by(|sst1, sst2| sst1.key_range.cmp(&sst2.key_range));
272                });
273        } else {
274            ret.total_file_count = 0;
275            ret.total_file_size = 0;
276        }
277
278        if self.enable_check_task_level_overlap {
279            self.verify_task_level_overlap(&ret, levels);
280        }
281
282        ret.sstable_infos
283            .retain(|(_sub_level_id, ssts)| !ssts.is_empty());
284
285        // To check whether the task is expected
286        if ret.total_file_size > self.max_compaction_bytes
287            || ret.total_file_count as u64 > self.max_file_count
288            || ret.sstable_infos.len() > self.max_expected_level_count
289        {
290            // rotate the sstables to meet the `max_file_count` and `max_compaction_bytes` and `max_expected_level_count`
291            let mut total_file_count = 0;
292            let mut total_file_size = 0;
293            let mut total_level_count = 0;
294            for (index, (_sub_level_id, sstables)) in ret.sstable_infos.iter().enumerate() {
295                total_file_count += sstables.len();
296                total_file_size += sstables.iter().map(|sst| sst.sst_size).sum::<u64>();
297                total_level_count += 1;
298
299                // Atleast `min_expected_level_count`` level should be selected
300                if total_level_count >= self.min_expected_level_count
301                    && (total_file_count as u64 >= self.max_file_count
302                        || total_file_size >= self.max_compaction_bytes
303                        || total_level_count >= self.max_expected_level_count)
304                {
305                    ret.total_file_count = total_file_count;
306                    ret.total_file_size = total_file_size;
307                    ret.sstable_infos.truncate(index + 1);
308                    break;
309                }
310            }
311        }
312
313        if ret.sstable_infos.is_empty() {
314            return None;
315        }
316
317        Some(ret)
318    }
319
320    /// Generates multiple compaction task candidates from L0 sub-levels.
321    ///
322    /// Iterates through candidate SSTs from a selected interval sub-level, calls
323    /// `pick_sub_level` for each to form tasks, then classifies and sorts them
324    /// by priority ("expected" tasks before "unexpected" ones).
325    ///
326    /// # Arguments
327    ///
328    /// * `l0` - L0 sub-levels ordered from oldest to newest
329    /// * `level_handler` - Tracks which SSTs are currently being compacted
330    ///
331    /// # Returns
332    ///
333    /// Sorted vector of task candidates, or empty if insufficient sub-levels exist.
334    /// Tasks are marked "expected" when satisfying configured size and level count thresholds.
335    pub fn pick_l0_multi_non_overlap_level(
336        &self,
337        l0: &[Level],
338        level_handler: &LevelHandler,
339    ) -> Vec<SubLevelSstables> {
340        if l0.len() < self.min_expected_level_count {
341            return vec![];
342        }
343
344        let mut scores = vec![];
345        let Some(intervals) = self.select_intervals(l0, level_handler) else {
346            return vec![];
347        };
348        for sst in intervals {
349            if level_handler.is_pending_compact(&sst.sst_id) {
350                continue;
351            }
352
353            if let Some(score) = self.pick_sub_level(l0, level_handler, &sst.key_range) {
354                scores.push(score);
355            }
356        }
357
358        if scores.is_empty() {
359            return vec![];
360        }
361
362        let mut expected = Vec::with_capacity(scores.len());
363        let mut unexpected = vec![];
364
365        for mut selected_task in scores {
366            if selected_task.sstable_infos.len() > self.max_expected_level_count
367                || selected_task.sstable_infos.len() < self.min_expected_level_count
368                || selected_task.total_file_size < self.min_compaction_bytes
369            {
370                selected_task.expected = false;
371                unexpected.push(selected_task);
372            } else {
373                selected_task.expected = true;
374                expected.push(selected_task);
375            }
376        }
377
378        // The logic of sorting depends on the interval we expect to select.
379        // 1. contain as many levels as possible
380        // 2. fewer files in the bottom sub level, containing as many smaller intervals as possible.
381        expected.sort_by(|a, b| {
382            b.sstable_infos
383                .len()
384                .cmp(&a.sstable_infos.len())
385                .then_with(|| a.total_file_count.cmp(&b.total_file_count))
386                .then_with(|| a.total_file_size.cmp(&b.total_file_size))
387        });
388
389        // For unexpected tasks, We devised a separate algorithm to evaluate the priority of a task, based on the limit passed in,
390        // we set tasks close to the limit to be high priority, here have three attributes:
391        // 1. The number of levels selected is close to the limit
392        // 2. The number of files selected is close to the limit
393        // 3. The size of the selected file is close to the limit
394        unexpected.sort_by(|a, b| {
395            let a_select_count_offset =
396                (a.sstable_infos.len() as i64 - self.max_expected_level_count as i64).abs();
397            let b_select_count_offset =
398                (b.sstable_infos.len() as i64 - self.max_expected_level_count as i64).abs();
399
400            let a_file_count_offset =
401                (a.total_file_count as i64 - self.max_file_count as i64).abs();
402            let b_file_count_offset =
403                (b.total_file_count as i64 - self.max_file_count as i64).abs();
404
405            let a_file_size_offset =
406                (a.total_file_size as i64 - self.max_compaction_bytes as i64).abs();
407            let b_file_size_offset =
408                (b.total_file_size as i64 - self.max_compaction_bytes as i64).abs();
409
410            a_select_count_offset
411                .cmp(&b_select_count_offset)
412                .then_with(|| a_file_count_offset.cmp(&b_file_count_offset))
413                .then_with(|| a_file_size_offset.cmp(&b_file_size_offset))
414        });
415
416        expected.extend(unexpected);
417
418        expected
419    }
420
421    fn verify_task_level_overlap(&self, ret: &SubLevelSstables, levels: &[Level]) {
422        let mut overlap_info: Option<Box<dyn OverlapInfo>> = None;
423        for (level_idx, (_sub_level_id, ssts)) in ret.sstable_infos.iter().enumerate().rev() {
424            if let Some(overlap_info) = overlap_info.as_mut() {
425                // skip the check if `overlap_info` is not initialized (i.e. the first non-empty level is not met)
426                let level = levels.get(level_idx).unwrap();
427                let overlap_sst_range = overlap_info.check_multiple_overlap(&level.table_infos);
428                if !overlap_sst_range.is_empty() {
429                    let expected_sst_ids = level.table_infos[overlap_sst_range.clone()]
430                        .iter()
431                        .map(|s| s.object_id)
432                        .collect_vec();
433                    let actual_sst_ids = ssts.iter().map(|s| s.object_id).collect_vec();
434                    // `actual_sst_ids` can be larger than `expected_sst_ids` because we may use a larger key range to select SSTs.
435                    // `expected_sst_ids` must be a sub-range of `actual_sst_ids` to ensure correctness.
436                    let start_idx = actual_sst_ids
437                        .iter()
438                        .position(|sst_id| sst_id == expected_sst_ids.first().unwrap());
439                    if start_idx.is_none_or(|idx| {
440                        actual_sst_ids[idx..idx + expected_sst_ids.len()] != expected_sst_ids
441                    }) {
442                        // Print SstableInfo for `actual_sst_ids`
443                        let mut actual_sst_infos = String::new();
444                        ssts.iter()
445                            .for_each(|s| append_sstable_info_to_string(&mut actual_sst_infos, s));
446
447                        // Print SstableInfo for `expected_sst_ids`
448                        let mut expected_sst_infos = String::new();
449                        level.table_infos[overlap_sst_range].iter().for_each(|s| {
450                            append_sstable_info_to_string(&mut expected_sst_infos, s)
451                        });
452
453                        // Print SstableInfo for selected ssts in all sub-levels
454                        let mut ret_sst_infos = String::new();
455                        ret.sstable_infos.iter().enumerate().for_each(
456                            |(idx, (_sub_level_id, ssts))| {
457                                writeln!(
458                                    ret_sst_infos,
459                                    "sub level {}",
460                                    levels.get(idx).unwrap().sub_level_id
461                                )
462                                .unwrap();
463                                ssts.iter().for_each(|s| {
464                                    append_sstable_info_to_string(&mut ret_sst_infos, s)
465                                });
466                            },
467                        );
468                        panic!(
469                            "Compact task overlap check fails. Actual: {} Expected: {} Ret {}",
470                            actual_sst_infos, expected_sst_infos, ret_sst_infos
471                        );
472                    }
473                }
474            } else if !ssts.is_empty() {
475                // init the `overlap_info` when meeting the first non-empty level.
476                overlap_info = Some(self.overlap_strategy.create_overlap_info());
477            }
478
479            for sst in ssts {
480                overlap_info.as_mut().unwrap().update(&sst.key_range);
481            }
482        }
483    }
484}
485
486#[cfg(test)]
487pub mod tests {
488    use std::collections::BTreeSet;
489
490    use risingwave_common::config::meta::default::compaction_config;
491
492    use super::*;
493    use crate::hummock::compaction::overlap_strategy::RangeOverlapStrategy;
494    use crate::hummock::compaction::picker::non_overlap_sub_level_picker::NonOverlapSubLevelPicker;
495    use crate::hummock::compaction::selector::tests::generate_table;
496
497    #[test]
498    fn test_select_intervals_tie_breaks() {
499        let l0 = vec![
500            Level {
501                level_type: LevelType::Nonoverlapping,
502                table_infos: vec![
503                    generate_table(0, 1, 0, 9, 1),
504                    generate_table(1, 1, 10, 19, 1),
505                ],
506                ..Default::default()
507            },
508            Level {
509                level_type: LevelType::Nonoverlapping,
510                table_infos: vec![
511                    generate_table(2, 1, 0, 9, 1),
512                    generate_table(3, 1, 10, 19, 1),
513                ],
514                ..Default::default()
515            },
516        ];
517        let picker = NonOverlapSubLevelPicker::new(
518            0,
519            10000,
520            1,
521            10000,
522            Arc::new(RangeOverlapStrategy::default()),
523            true,
524            10,
525            true,
526        );
527        let mut handler = LevelHandler::new(0);
528        // Equal scores retain the oldest sub-level.
529        assert_eq!(picker.select_intervals(&l0, &handler).unwrap()[0].sst_id, 0);
530        handler.test_add_pending_sst(0.into(), 1);
531        assert_eq!(picker.select_intervals(&l0, &handler).unwrap()[0].sst_id, 2);
532        handler.test_add_pending_sst(2.into(), 1);
533        assert_eq!(picker.select_intervals(&l0, &handler).unwrap()[0].sst_id, 0);
534        // With equal non-pending counts, prefer the larger total file count.
535        let mut l0 = l0;
536        l0[1].table_infos.push(generate_table(4, 1, 20, 29, 1));
537        handler.test_add_pending_sst(4.into(), 1);
538        assert_eq!(picker.select_intervals(&l0, &handler).unwrap()[0].sst_id, 2);
539    }
540
541    // Run explicitly with cargo test --release -p risingwave_meta bench_select_intervals -- --ignored --nocapture --test-threads=1.
542    #[test]
543    #[ignore = "microbenchmark"]
544    fn bench_select_intervals() {
545        use std::hint::black_box;
546        use std::time::Duration;
547
548        let mut criterion = criterion::Criterion::default()
549            .sample_size(30)
550            .warm_up_time(Duration::from_secs(1))
551            .measurement_time(Duration::from_secs(3));
552        let picker = NonOverlapSubLevelPicker::new(
553            0,
554            10000,
555            1,
556            10000,
557            Arc::new(RangeOverlapStrategy::default()),
558            false,
559            64,
560            true,
561        );
562        for (name, counts) in [
563            ("single", vec![1024]),
564            ("small", vec![8; 4]),
565            ("balanced", vec![256; 64]),
566            (
567                "skewed",
568                std::iter::once(10000)
569                    .chain(std::iter::repeat_n(1, 63))
570                    .collect(),
571            ),
572        ] {
573            let mut next_id = 0;
574            let mut handler = LevelHandler::new(0);
575            let levels: Vec<_> = counts
576                .into_iter()
577                .map(|count| {
578                    let table_infos = (0..count)
579                        .map(|idx| {
580                            let sst = generate_table(next_id, 1, idx * 10, idx * 10 + 9, 1);
581                            // Include pending lookups that cannot use the empty-map fast path.
582                            if next_id.is_multiple_of(4) {
583                                handler.test_add_pending_sst(next_id.into(), 1);
584                            }
585                            next_id += 1;
586                            sst
587                        })
588                        .collect();
589                    Level {
590                        level_type: LevelType::Nonoverlapping,
591                        table_infos,
592                        ..Default::default()
593                    }
594                })
595                .collect();
596            criterion.bench_function(&format!("select_intervals/{name}"), |b| {
597                b.iter(|| {
598                    black_box(picker.select_intervals(black_box(&levels), black_box(&handler)))
599                })
600            });
601        }
602        criterion.final_summary();
603    }
604
605    #[test]
606    fn test_pick_l0_multi_level() {
607        let levels = vec![
608            Level {
609                level_idx: 1,
610                level_type: LevelType::Nonoverlapping,
611                table_infos: vec![
612                    generate_table(0, 1, 50, 99, 2),
613                    generate_table(1, 1, 100, 149, 2),
614                    generate_table(2, 1, 150, 249, 2),
615                    generate_table(6, 1, 250, 300, 2),
616                    generate_table(7, 1, 350, 400, 2),
617                    generate_table(8, 1, 450, 500, 2),
618                ],
619                total_file_size: 800,
620                ..Default::default()
621            },
622            Level {
623                level_idx: 2,
624                level_type: LevelType::Nonoverlapping,
625                table_infos: vec![
626                    generate_table(4, 1, 50, 199, 1),
627                    generate_table(5, 1, 200, 249, 1),
628                    generate_table(9, 1, 250, 300, 2),
629                    generate_table(10, 1, 350, 400, 2),
630                    generate_table(11, 1, 450, 500, 2),
631                ],
632                total_file_size: 350,
633                ..Default::default()
634            },
635            Level {
636                level_idx: 3,
637                level_type: LevelType::Nonoverlapping,
638                table_infos: vec![
639                    generate_table(11, 1, 250, 300, 2),
640                    generate_table(12, 1, 350, 400, 2),
641                    generate_table(13, 1, 450, 500, 2),
642                ],
643                total_file_size: 150,
644                ..Default::default()
645            },
646            Level {
647                level_idx: 4,
648                level_type: LevelType::Nonoverlapping,
649                table_infos: vec![
650                    generate_table(14, 1, 250, 300, 2),
651                    generate_table(15, 1, 350, 400, 2),
652                    generate_table(16, 1, 450, 500, 2),
653                ],
654                total_file_size: 150,
655                ..Default::default()
656            },
657        ];
658
659        let levels_handlers = [
660            LevelHandler::new(0),
661            LevelHandler::new(1),
662            LevelHandler::new(2),
663        ];
664
665        {
666            // no limit
667            let picker = NonOverlapSubLevelPicker::new(
668                0,
669                10000,
670                1,
671                10000,
672                Arc::new(RangeOverlapStrategy::default()),
673                true,
674                compaction_config::max_l0_compact_level_count() as usize,
675                true,
676            );
677            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
678            assert_eq!(6, ret.len());
679        }
680
681        {
682            // limit max bytes
683            let picker = NonOverlapSubLevelPicker::new(
684                0,
685                100,
686                1,
687                10000,
688                Arc::new(RangeOverlapStrategy::default()),
689                true,
690                compaction_config::max_l0_compact_level_count() as usize,
691                true,
692            );
693            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
694            assert_eq!(6, ret.len());
695        }
696
697        {
698            // limit max file_count
699            let picker = NonOverlapSubLevelPicker::new(
700                0,
701                10000,
702                1,
703                5,
704                Arc::new(RangeOverlapStrategy::default()),
705                true,
706                compaction_config::max_l0_compact_level_count() as usize,
707                true,
708            );
709            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
710            assert_eq!(6, ret.len());
711        }
712    }
713
714    #[test]
715    fn test_pick_l0_multi_level2() {
716        let levels = vec![
717            Level {
718                level_idx: 1,
719                level_type: LevelType::Nonoverlapping,
720                table_infos: vec![
721                    generate_table(0, 1, 50, 99, 2),
722                    generate_table(1, 1, 100, 149, 2),
723                    generate_table(2, 1, 150, 249, 2),
724                    generate_table(6, 1, 250, 300, 2),
725                    generate_table(7, 1, 350, 400, 2),
726                    generate_table(8, 1, 450, 500, 2),
727                ],
728                total_file_size: 800,
729                ..Default::default()
730            },
731            Level {
732                level_idx: 2,
733                level_type: LevelType::Nonoverlapping,
734                table_infos: vec![
735                    generate_table(4, 1, 50, 99, 1),
736                    generate_table(5, 1, 150, 200, 1),
737                    generate_table(9, 1, 250, 300, 2),
738                    generate_table(10, 1, 350, 400, 2),
739                    generate_table(11, 1, 450, 500, 2),
740                ],
741                total_file_size: 250,
742                ..Default::default()
743            },
744            Level {
745                level_idx: 3,
746                level_type: LevelType::Nonoverlapping,
747                table_infos: vec![
748                    generate_table(11, 1, 250, 300, 2),
749                    generate_table(12, 1, 350, 400, 2),
750                    generate_table(13, 1, 450, 500, 2),
751                ],
752                total_file_size: 150,
753                ..Default::default()
754            },
755            Level {
756                level_idx: 4,
757                level_type: LevelType::Nonoverlapping,
758                table_infos: vec![
759                    generate_table(14, 1, 250, 300, 2),
760                    generate_table(15, 1, 350, 400, 2),
761                    generate_table(16, 1, 450, 500, 2),
762                ],
763                total_file_size: 150,
764                ..Default::default()
765            },
766        ];
767
768        let levels_handlers = [
769            LevelHandler::new(0),
770            LevelHandler::new(1),
771            LevelHandler::new(2),
772        ];
773
774        {
775            // no limit
776            let picker = NonOverlapSubLevelPicker::new(
777                0,
778                10000,
779                1,
780                10000,
781                Arc::new(RangeOverlapStrategy::default()),
782                true,
783                compaction_config::max_l0_compact_level_count() as usize,
784                true,
785            );
786            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
787            assert_eq!(6, ret.len());
788        }
789
790        {
791            // limit max bytes
792            let max_compaction_bytes = 100;
793            let picker = NonOverlapSubLevelPicker::new(
794                60,
795                max_compaction_bytes,
796                1,
797                10000,
798                Arc::new(RangeOverlapStrategy::default()),
799                true,
800                compaction_config::max_l0_compact_level_count() as usize,
801                true,
802            );
803            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
804            assert_eq!(6, ret.len());
805
806            for plan in &ret {
807                if plan.total_file_size >= max_compaction_bytes {
808                    assert!(plan.expected);
809                } else {
810                    assert!(!plan.expected);
811                }
812            }
813        }
814
815        {
816            // limit max file_count
817            let max_file_count = 2;
818            let picker = NonOverlapSubLevelPicker::new(
819                0,
820                10000,
821                1,
822                max_file_count,
823                Arc::new(RangeOverlapStrategy::default()),
824                true,
825                compaction_config::max_l0_compact_level_count() as usize,
826                true,
827            );
828            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
829            assert_eq!(6, ret.len());
830
831            for plan in ret {
832                let mut sst_id_set = BTreeSet::default();
833                for sst in &plan.sstable_infos {
834                    sst_id_set.insert(sst.1[0].sst_id);
835                }
836                assert!(sst_id_set.len() <= max_file_count as usize);
837            }
838        }
839
840        {
841            // limit min_depth
842            let min_depth = 3;
843            let picker = NonOverlapSubLevelPicker::new(
844                10,
845                10000,
846                min_depth,
847                100,
848                Arc::new(RangeOverlapStrategy::default()),
849                true,
850                compaction_config::max_l0_compact_level_count() as usize,
851                true,
852            );
853            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
854            assert_eq!(6, ret.len());
855
856            for plan in ret {
857                if plan.sstable_infos.len() >= min_depth {
858                    assert!(plan.expected);
859                } else {
860                    assert!(!plan.expected);
861                }
862            }
863        }
864    }
865
866    #[test]
867    fn test_pick_unexpected_task() {
868        let levels = vec![
869            Level {
870                level_idx: 1,
871                level_type: LevelType::Nonoverlapping,
872                table_infos: vec![generate_table(0, 1, 50, 100, 2)], // 50
873                total_file_size: 50,
874                ..Default::default()
875            },
876            Level {
877                level_idx: 2,
878                level_type: LevelType::Nonoverlapping,
879                table_infos: vec![
880                    generate_table(1, 1, 101, 150, 1), // 50
881                ],
882                total_file_size: 50,
883                ..Default::default()
884            },
885            Level {
886                level_idx: 3,
887                level_type: LevelType::Nonoverlapping,
888                table_infos: vec![
889                    generate_table(2, 1, 151, 200, 2), // 50
890                ],
891                total_file_size: 50,
892                ..Default::default()
893            },
894            Level {
895                level_idx: 4,
896                level_type: LevelType::Nonoverlapping,
897                table_infos: vec![
898                    generate_table(3, 1, 50, 300, 2), // 250
899                ],
900                total_file_size: 250,
901                ..Default::default()
902            },
903        ];
904
905        let levels_handlers = [
906            LevelHandler::new(0),
907            LevelHandler::new(1),
908            LevelHandler::new(2),
909            LevelHandler::new(3),
910            LevelHandler::new(4),
911        ];
912
913        {
914            // no limit
915            let picker = NonOverlapSubLevelPicker::new(
916                0,
917                10000,
918                1,
919                10000,
920                Arc::new(RangeOverlapStrategy::default()),
921                true,
922                compaction_config::max_l0_compact_level_count() as usize,
923                true,
924            );
925            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
926            {
927                let plan = &ret[0];
928                assert_eq!(4, plan.sstable_infos.len());
929            }
930        }
931
932        {
933            // limit size
934            let picker = NonOverlapSubLevelPicker::new(
935                0,
936                150,
937                1,
938                10000,
939                Arc::new(RangeOverlapStrategy::default()),
940                true,
941                compaction_config::max_l0_compact_level_count() as usize,
942                true,
943            );
944            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
945            {
946                let plan = &ret[0];
947                assert_eq!(3, plan.sstable_infos.len());
948            }
949        }
950
951        {
952            // limit count
953            let picker = NonOverlapSubLevelPicker::new(
954                0,
955                10000,
956                1,
957                3,
958                Arc::new(RangeOverlapStrategy::default()),
959                true,
960                compaction_config::max_l0_compact_level_count() as usize,
961                true,
962            );
963            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
964            {
965                let plan = &ret[0];
966                assert_eq!(3, plan.sstable_infos.len());
967            }
968        }
969
970        {
971            // limit expected level count
972            let max_expected_level_count = 3;
973            let picker = NonOverlapSubLevelPicker::for_test(
974                0,
975                10000,
976                1,
977                3,
978                Arc::new(RangeOverlapStrategy::default()),
979                true,
980                max_expected_level_count,
981                true,
982            );
983            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
984            {
985                let plan = &ret[0];
986                assert_eq!(max_expected_level_count, plan.sstable_infos.len());
987
988                assert_eq!(0, plan.sstable_infos[0].1[0].sst_id);
989                assert_eq!(1, plan.sstable_infos[1].1[0].sst_id);
990                assert_eq!(2, plan.sstable_infos[2].1[0].sst_id);
991            }
992        }
993
994        {
995            // limit min_compacaion_bytes
996            let max_expected_level_count = 100;
997            let picker = NonOverlapSubLevelPicker::for_test(
998                1000,
999                10000,
1000                1,
1001                100,
1002                Arc::new(RangeOverlapStrategy::default()),
1003                true,
1004                max_expected_level_count,
1005                true,
1006            );
1007            let ret = picker.pick_l0_multi_non_overlap_level(&levels, &levels_handlers[0]);
1008            {
1009                let plan = &ret[0];
1010
1011                assert_eq!(0, plan.sstable_infos[0].1[0].sst_id);
1012                assert_eq!(1, plan.sstable_infos[1].1[0].sst_id);
1013                assert_eq!(2, plan.sstable_infos[2].1[0].sst_id);
1014                assert_eq!(3, plan.sstable_infos[3].1[0].sst_id);
1015                assert!(!plan.expected);
1016            }
1017        }
1018    }
1019
1020    #[test]
1021    fn test_select_intervals_combined() {
1022        // no optimization: pick the first sub-level
1023        let l0 = vec![
1024            Level {
1025                level_idx: 1,
1026                level_type: LevelType::Nonoverlapping,
1027                table_infos: vec![generate_table(1, 1, 0, 10, 1)],
1028                ..Default::default()
1029            },
1030            Level {
1031                level_idx: 2,
1032                level_type: LevelType::Overlapping,
1033                table_infos: vec![generate_table(2, 1, 0, 10, 1)],
1034                ..Default::default()
1035            },
1036        ];
1037        let picker = NonOverlapSubLevelPicker::new(
1038            0,
1039            10000,
1040            1,
1041            10000,
1042            Arc::new(RangeOverlapStrategy::default()),
1043            true,
1044            compaction_config::max_l0_compact_level_count() as usize,
1045            false,
1046        );
1047        let level_handler = LevelHandler::new(0);
1048        let intervals = picker.select_intervals(&l0, &level_handler).unwrap();
1049        assert_eq!(intervals.len(), 1);
1050        assert_eq!(intervals[0].sst_id, 1);
1051
1052        // with optimization: prefer the sub-level with more non-pending files
1053        let l0 = vec![
1054            Level {
1055                level_idx: 1,
1056                level_type: LevelType::Nonoverlapping,
1057                table_infos: vec![generate_table(1, 1, 0, 10, 1)], // 1 file
1058                ..Default::default()
1059            },
1060            Level {
1061                level_idx: 2,
1062                level_type: LevelType::Nonoverlapping,
1063                table_infos: vec![
1064                    generate_table(2, 1, 0, 10, 1),
1065                    generate_table(3, 1, 20, 30, 1),
1066                ], // 2 files
1067                ..Default::default()
1068            },
1069        ];
1070        let picker = NonOverlapSubLevelPicker::new(
1071            0,
1072            10000,
1073            1,
1074            10000,
1075            Arc::new(RangeOverlapStrategy::default()),
1076            true,
1077            compaction_config::max_l0_compact_level_count() as usize,
1078            true,
1079        );
1080        let intervals = picker.select_intervals(&l0, &level_handler).unwrap();
1081        assert_eq!(intervals.len(), 2);
1082        assert_eq!(
1083            intervals.iter().map(|s| s.sst_id).collect::<Vec<_>>(),
1084            vec![2, 3]
1085        );
1086
1087        // all overlapping should return None
1088        let l0 = vec![Level {
1089            level_idx: 1,
1090            level_type: LevelType::Overlapping,
1091            table_infos: vec![generate_table(1, 1, 0, 10, 1)],
1092            ..Default::default()
1093        }];
1094        let picker = NonOverlapSubLevelPicker::new(
1095            0,
1096            10000,
1097            1,
1098            10000,
1099            Arc::new(RangeOverlapStrategy::default()),
1100            true,
1101            compaction_config::max_l0_compact_level_count() as usize,
1102            true,
1103        );
1104        assert!(picker.select_intervals(&l0, &level_handler).is_none());
1105    }
1106}