1use std::collections::{BTreeMap, HashMap};
16use std::marker::PhantomData;
17use std::ops::Bound;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use await_tree::{InstrumentAwait, SpanExt};
22use bytes::Bytes;
23use itertools::Itertools;
24use risingwave_common::catalog::TableId;
25use risingwave_common::config::meta::default::compaction_config;
26use risingwave_common::constants::hummock::CompactionFilterFlag;
27use risingwave_hummock_sdk::compact_task::CompactTask;
28use risingwave_hummock_sdk::key::FullKey;
29use risingwave_hummock_sdk::key_range::KeyRange;
30use risingwave_hummock_sdk::sstable_info::SstableInfo;
31use risingwave_hummock_sdk::table_stats::TableStatsMap;
32use risingwave_hummock_sdk::{EpochWithGap, KeyComparator, can_concat};
33use risingwave_pb::hummock::compact_task::PbTaskType;
34use risingwave_pb::hummock::{
35 PbLevelType, PbSstableFilterLayout, PbSstableFilterType, PbTableSchema,
36};
37use tokio::time::Instant;
38
39pub use super::context::CompactorContext;
40use crate::compaction_catalog_manager::CompactionCatalogAgentRef;
41use crate::hummock::compactor::{
42 ConcatSstableIterator, MultiCompactionFilter, TaskProgress, TtlCompactionFilter,
43};
44use crate::hummock::iterator::{
45 Forward, HummockIterator, MergeIterator, NonPkPrefixSkipWatermarkIterator,
46 NonPkPrefixSkipWatermarkState, PkPrefixSkipWatermarkIterator, PkPrefixSkipWatermarkState,
47 UserIterator,
48};
49use crate::hummock::multi_builder::TableBuilderFactory;
50use crate::hummock::{
51 CachePolicy, FilterBuilder, GetObjectId, HummockResult, MemoryLimiter, SstableBuilder,
52 SstableBuilderOptions, SstableWriterFactory, SstableWriterOptions,
53};
54use crate::monitor::StoreLocalStatistic;
55
56pub struct RemoteBuilderFactory<W: SstableWriterFactory, F: FilterBuilder> {
57 pub object_id_getter: Arc<dyn GetObjectId>,
58 pub limiter: Arc<MemoryLimiter>,
59 pub options: SstableBuilderOptions,
60 pub policy: CachePolicy,
61 pub remote_rpc_cost: Arc<AtomicU64>,
62 pub compaction_catalog_agent_ref: CompactionCatalogAgentRef,
63 pub sstable_writer_factory: W,
64 pub _phantom: PhantomData<F>,
65}
66
67#[async_trait::async_trait]
68impl<W: SstableWriterFactory, F: FilterBuilder> TableBuilderFactory for RemoteBuilderFactory<W, F> {
69 type Filter = F;
70 type Writer = W::Writer;
71
72 async fn open_builder(&mut self) -> HummockResult<SstableBuilder<Self::Writer, Self::Filter>> {
73 let timer = Instant::now();
74 let table_id = self
75 .object_id_getter
76 .get_new_sst_object_id()
77 .instrument_await("remote_builder_get_new_sst_object_id".verbose())
78 .await?;
79 let cost = (timer.elapsed().as_secs_f64() * 1000000.0).round() as u64;
80 self.remote_rpc_cost.fetch_add(cost, Ordering::Relaxed);
81 let writer_options = SstableWriterOptions {
82 capacity_hint: Some(self.options.capacity + self.options.block_capacity),
83 tracker: None,
84 policy: self.policy,
85 };
86 let writer = self
87 .sstable_writer_factory
88 .create_sst_writer(table_id, writer_options)
89 .instrument_await("remote_builder_create_sst_writer".verbose())
90 .await?;
91 let builder = SstableBuilder::new(
92 table_id,
93 writer,
94 Self::Filter::create(self.options.filter_builder_options()),
95 self.options.clone(),
96 self.compaction_catalog_agent_ref.clone(),
97 Some(self.limiter.clone()),
98 );
99 Ok(builder)
100 }
101}
102
103#[derive(Default, Debug)]
105pub struct CompactionStatistics {
106 pub delta_drop_stat: TableStatsMap,
108
109 pub iter_total_key_counts: u64,
111 pub iter_drop_key_counts: u64,
112}
113
114#[derive(Clone, Default)]
115pub struct TaskConfig {
116 pub(crate) key_range: KeyRange,
117 pub(crate) cache_policy: CachePolicy,
118 pub(crate) gc_delete_keys: bool,
119 pub(crate) retain_multiple_version: bool,
120 pub(crate) sstable_filter_layout: PbSstableFilterLayout,
122 pub(crate) sstable_filter_type: PbSstableFilterType,
123
124 pub(crate) table_vnode_partition: BTreeMap<TableId, u32>,
125 pub(crate) table_schemas: HashMap<TableId, PbTableSchema>,
129 pub(crate) disable_drop_column_optimization: bool,
131}
132
133impl TaskConfig {
134 pub fn for_test(
136 key_range: KeyRange,
137 cache_policy: CachePolicy,
138 gc_delete_keys: bool,
139 sstable_filter_layout: PbSstableFilterLayout,
140 table_schemas: HashMap<TableId, PbTableSchema>,
141 ) -> Self {
142 Self {
143 key_range,
144 cache_policy,
145 gc_delete_keys,
146 retain_multiple_version: false,
147 sstable_filter_layout,
148 sstable_filter_type: PbSstableFilterType::SstableFilterXor16,
149 table_vnode_partition: BTreeMap::default(),
150 table_schemas,
151 disable_drop_column_optimization: false,
152 }
153 }
154
155 pub fn with_disable_drop_column_optimization(mut self, disable: bool) -> Self {
156 self.disable_drop_column_optimization = disable;
157 self
158 }
159}
160
161pub fn build_multi_compaction_filter(compact_task: &CompactTask) -> MultiCompactionFilter {
162 let mut multi_filter = MultiCompactionFilter::default();
163 let compaction_filter_flag =
164 CompactionFilterFlag::from_bits(compact_task.compaction_filter_mask).unwrap_or_default();
165 if compaction_filter_flag.contains(CompactionFilterFlag::TTL) {
169 let id_to_ttl = compact_task
170 .table_options
171 .iter()
172 .filter_map(|(id, option)| {
173 option
174 .retention_seconds
175 .and_then(|ttl| if ttl > 0 { Some((*id, ttl)) } else { None })
176 })
177 .collect();
178
179 let ttl_filter = Box::new(TtlCompactionFilter::new(
180 id_to_ttl,
181 compact_task.current_epoch_time,
182 ));
183 multi_filter.register(ttl_filter);
184 }
185
186 multi_filter
187}
188
189fn generate_splits_fast(
190 sstable_infos: &[&SstableInfo],
191 compaction_size: u64,
192 context: &CompactorContext,
193 max_sub_compaction: u32,
194) -> Vec<KeyRange> {
195 let worker_num = context.compaction_executor.worker_num();
196 let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
197
198 let parallelism = calculate_task_parallelism_impl(
199 worker_num,
200 parallel_compact_size,
201 compaction_size,
202 max_sub_compaction,
203 );
204 let mut indexes = vec![];
205 for sst in sstable_infos {
206 let key_range = &sst.key_range;
207 indexes.push(
208 FullKey {
209 user_key: FullKey::decode(&key_range.left).user_key,
210 epoch_with_gap: EpochWithGap::new_max_epoch(),
211 }
212 .encode(),
213 );
214 indexes.push(
215 FullKey {
216 user_key: FullKey::decode(&key_range.right).user_key,
217 epoch_with_gap: EpochWithGap::new_max_epoch(),
218 }
219 .encode(),
220 );
221 }
222 indexes.sort_by(|a, b| KeyComparator::compare_encoded_full_key(a.as_ref(), b.as_ref()));
223 indexes.dedup();
224 if indexes.len() <= parallelism {
225 return vec![];
226 }
227
228 let mut splits = vec![];
229 splits.push(KeyRange::default());
230 let parallel_key_count = indexes.len() / parallelism;
231 let mut last_split_key_count = 0;
232 for key in indexes {
233 if last_split_key_count >= parallel_key_count {
234 splits.last_mut().unwrap().right = Bytes::from(key.clone());
235 splits.push(KeyRange::new(Bytes::from(key.clone()), Bytes::default()));
236 last_split_key_count = 0;
237 }
238 last_split_key_count += 1;
239 }
240
241 splits
242}
243
244pub async fn generate_splits(
245 sstable_infos: &[&SstableInfo],
246 compaction_size: u64,
247 context: &CompactorContext,
248 max_sub_compaction: u32,
249) -> HummockResult<Vec<KeyRange>> {
250 let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
251 if compaction_size > parallel_compact_size {
252 if sstable_infos.len() > context.storage_opts.compactor_max_preload_meta_file_count {
253 return Ok(generate_splits_fast(
254 sstable_infos,
255 compaction_size,
256 context,
257 max_sub_compaction,
258 ));
259 }
260 let mut indexes = vec![];
261 for sstable_info in sstable_infos {
263 let sstable = context
264 .sstable_store
265 .sstable(sstable_info, &mut StoreLocalStatistic::default())
266 .await?;
267 indexes.extend(sstable.meta.block_metas.iter().map(|block| {
268 let data_size = block.len;
269 let full_key = FullKey {
270 user_key: FullKey::decode(&block.smallest_key).user_key,
271 epoch_with_gap: EpochWithGap::new_max_epoch(),
272 }
273 .encode();
274 (data_size as u64, full_key)
275 }));
276 }
277 indexes.sort_by(|a, b| KeyComparator::compare_encoded_full_key(a.1.as_ref(), b.1.as_ref()));
279 let mut splits = vec![];
280 splits.push(KeyRange::default());
281
282 let parallelism = calculate_task_parallelism_impl(
283 context.compaction_executor.worker_num(),
284 parallel_compact_size,
285 compaction_size,
286 max_sub_compaction,
287 );
288
289 let sub_compaction_data_size =
290 std::cmp::max(compaction_size / parallelism as u64, parallel_compact_size);
291
292 if parallelism > 1 {
293 let mut last_buffer_size = 0;
294 let mut last_key: Vec<u8> = vec![];
295 let mut remaining_size = indexes.iter().map(|block| block.0).sum::<u64>();
296 for (data_size, key) in indexes {
297 if last_buffer_size >= sub_compaction_data_size
298 && !last_key.eq(&key)
299 && remaining_size > parallel_compact_size
300 {
301 splits.last_mut().unwrap().right = Bytes::from(key.clone());
302 splits.push(KeyRange::new(Bytes::from(key.clone()), Bytes::default()));
303 last_buffer_size = data_size;
304 } else {
305 last_buffer_size += data_size;
306 }
307 remaining_size -= data_size;
308 last_key = key;
309 }
310 return Ok(splits);
311 }
312 }
313
314 Ok(vec![])
315}
316
317pub fn estimate_task_output_capacity(context: CompactorContext, task: &CompactTask) -> usize {
318 let max_target_file_size = context.storage_opts.sstable_size_mb as usize * (1 << 20);
319 let total_input_uncompressed_file_size = task
320 .read_input_ssts()
321 .map(|table| table.uncompressed_file_size)
322 .sum::<u64>();
323
324 let capacity = std::cmp::min(task.target_file_size as usize, max_target_file_size);
325 std::cmp::min(capacity, total_input_uncompressed_file_size as usize)
326}
327
328pub fn estimate_output_key_count_by_size(
329 total_key_count: u64,
330 total_size: u64,
331 output_capacity: usize,
332) -> usize {
333 if total_key_count == 0 {
334 return 0;
335 }
336 if total_size == 0 {
337 return total_key_count.try_into().unwrap_or(usize::MAX);
338 }
339 if output_capacity == 0 {
340 return 0;
341 }
342
343 let estimated = (total_key_count as u128 * output_capacity as u128)
347 .div_ceil(total_size as u128)
348 .min(total_key_count as u128);
349 estimated.try_into().unwrap_or(usize::MAX)
350}
351
352pub fn estimate_output_key_count_for_input_ssts<'a>(
353 input_ssts: impl Iterator<Item = &'a SstableInfo>,
354 output_capacity: usize,
355) -> usize {
356 let (total_key_count, total_uncompressed_size) =
357 input_ssts.fold((0u64, 0u64), |(key_count, size), sst| {
358 (
359 key_count + sst.total_key_count,
360 size + sst.uncompressed_file_size,
361 )
362 });
363
364 estimate_output_key_count_by_size(total_key_count, total_uncompressed_size, output_capacity)
365}
366
367pub fn estimate_output_key_count_for_task(task: &CompactTask, output_capacity: usize) -> usize {
368 estimate_output_key_count_for_input_ssts(task.read_input_ssts(), output_capacity)
369}
370
371pub fn blocked_xor_filter_key_count_threshold(
372 blocked_xor_filter_kv_count_threshold: Option<u64>,
373) -> usize {
374 blocked_xor_filter_kv_count_threshold
375 .unwrap_or(compaction_config::DEFAULT_BLOCKED_XOR_FILTER_KV_COUNT_THRESHOLD)
376 .try_into()
377 .unwrap_or(usize::MAX)
378}
379
380pub async fn check_compaction_result(
382 compact_task: &CompactTask,
383 context: CompactorContext,
384 compaction_catalog_agent_ref: CompactionCatalogAgentRef,
385) -> HummockResult<bool> {
386 if compact_task.contains_ttl() {
388 return Ok(true);
389 }
390
391 let mut table_iters = Vec::new();
392
393 for level in &compact_task.input_ssts {
394 if level.level_type == PbLevelType::Nonoverlapping {
395 let tables = level.read_sstable_infos().cloned().collect_vec();
396 if tables.is_empty() {
397 continue;
398 }
399 debug_assert!(can_concat(&tables));
400
401 table_iters.push(ConcatSstableIterator::new(
402 tables,
403 KeyRange::inf(),
404 context.sstable_store.clone(),
405 Arc::new(TaskProgress::default()),
406 context.storage_opts.compactor_iter_max_io_retry_times,
407 ));
408 } else {
409 for table_info in level.read_sstable_infos().cloned() {
410 table_iters.push(ConcatSstableIterator::new(
411 vec![table_info],
412 KeyRange::inf(),
413 context.sstable_store.clone(),
414 Arc::new(TaskProgress::default()),
415 context.storage_opts.compactor_iter_max_io_retry_times,
416 ));
417 }
418 }
419 }
420
421 let iter = MergeIterator::for_compactor(table_iters);
422 let left_iter = {
423 let skip_watermark_iter = PkPrefixSkipWatermarkIterator::new(
424 iter,
425 PkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
426 compact_task.pk_prefix_table_watermarks.clone(),
427 ),
428 );
429
430 let combine_iter = NonPkPrefixSkipWatermarkIterator::new(
431 skip_watermark_iter,
432 NonPkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
433 compact_task.non_pk_prefix_table_watermarks.clone(),
434 compaction_catalog_agent_ref.clone(),
435 ),
436 );
437
438 UserIterator::new(
439 combine_iter,
440 (Bound::Unbounded, Bound::Unbounded),
441 u64::MAX,
442 0,
443 None,
444 )
445 };
446 let iter = ConcatSstableIterator::new(
447 compact_task.sorted_output_ssts.clone(),
448 KeyRange::inf(),
449 context.sstable_store.clone(),
450 Arc::new(TaskProgress::default()),
451 context.storage_opts.compactor_iter_max_io_retry_times,
452 );
453 let right_iter = {
454 let skip_watermark_iter = PkPrefixSkipWatermarkIterator::new(
455 iter,
456 PkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
457 compact_task.pk_prefix_table_watermarks.clone(),
458 ),
459 );
460
461 let combine_iter = NonPkPrefixSkipWatermarkIterator::new(
462 skip_watermark_iter,
463 NonPkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
464 compact_task.non_pk_prefix_table_watermarks.clone(),
465 compaction_catalog_agent_ref,
466 ),
467 );
468
469 UserIterator::new(
470 combine_iter,
471 (Bound::Unbounded, Bound::Unbounded),
472 u64::MAX,
473 0,
474 None,
475 )
476 };
477
478 check_result(left_iter, right_iter).await
479}
480
481pub async fn check_flush_result<I: HummockIterator<Direction = Forward>>(
482 left_iter: UserIterator<I>,
483 sort_ssts: Vec<SstableInfo>,
484 context: CompactorContext,
485) -> HummockResult<bool> {
486 let iter = ConcatSstableIterator::new(
487 sort_ssts,
488 KeyRange::inf(),
489 context.sstable_store.clone(),
490 Arc::new(TaskProgress::default()),
491 0,
492 );
493 let right_iter = UserIterator::new(
494 iter,
495 (Bound::Unbounded, Bound::Unbounded),
496 u64::MAX,
497 0,
498 None,
499 );
500 check_result(left_iter, right_iter).await
501}
502
503async fn check_result<
504 I1: HummockIterator<Direction = Forward>,
505 I2: HummockIterator<Direction = Forward>,
506>(
507 mut left_iter: UserIterator<I1>,
508 mut right_iter: UserIterator<I2>,
509) -> HummockResult<bool> {
510 left_iter.rewind().await?;
511 right_iter.rewind().await?;
512 let mut right_count = 0;
513 let mut left_count = 0;
514 while left_iter.is_valid() && right_iter.is_valid() {
515 if left_iter.key() != right_iter.key() {
516 tracing::error!(
517 "The key of input and output not equal. key: {:?} vs {:?}",
518 left_iter.key(),
519 right_iter.key()
520 );
521 return Ok(false);
522 }
523 if left_iter.value() != right_iter.value() {
524 tracing::error!(
525 "The value of input and output not equal. key: {:?}, value: {:?} vs {:?}",
526 left_iter.key(),
527 left_iter.value(),
528 right_iter.value()
529 );
530 return Ok(false);
531 }
532 left_iter.next().await?;
533 right_iter.next().await?;
534 left_count += 1;
535 right_count += 1;
536 }
537 while left_iter.is_valid() {
538 left_count += 1;
539 left_iter.next().await?;
540 }
541 while right_iter.is_valid() {
542 right_count += 1;
543 right_iter.next().await?;
544 }
545 if left_count != right_count {
546 tracing::error!(
547 "The key count of input and output not equal: {} vs {}",
548 left_count,
549 right_count
550 );
551 return Ok(false);
552 }
553 Ok(true)
554}
555
556pub fn optimize_by_copy_block(compact_task: &CompactTask, context: &CompactorContext) -> bool {
557 let input_ssts = compact_task.read_input_ssts().collect_vec();
558 let compaction_size = input_ssts_size(&input_ssts);
559 optimize_by_copy_block_with_input(compact_task, context, &input_ssts, compaction_size)
560}
561
562fn optimize_by_copy_block_with_input(
563 compact_task: &CompactTask,
564 context: &CompactorContext,
565 input_ssts: &[&SstableInfo],
566 compaction_size: u64,
567) -> bool {
568 let all_ssts_are_blocked_filter = input_ssts
569 .iter()
570 .all(|table_info| table_info.filter_layout == PbSstableFilterLayout::Blocked);
571 let current_filter_type = compact_task.sstable_filter_type;
572 let all_ssts_match_filter_type = input_ssts
573 .iter()
574 .all(|table_info| table_info.filter_type == current_filter_type);
575 let output_capacity = estimate_task_output_capacity(context.clone(), compact_task);
580 let estimated_output_key_count =
581 estimate_output_key_count_for_input_ssts(input_ssts.iter().copied(), output_capacity);
582 let output_filter_layout =
583 compact_task.sstable_filter_layout_for_output(estimated_output_key_count as u64);
584
585 let delete_key_count = input_ssts
586 .iter()
587 .map(|table_info| table_info.stale_key_count + table_info.range_tombstone_count)
588 .sum::<u64>();
589 let total_key_count = input_ssts
590 .iter()
591 .map(|table_info| table_info.total_key_count)
592 .sum::<u64>();
593
594 let single_table = compact_task.get_table_ids_from_input_ssts().count() == 1;
595 context.storage_opts.enable_fast_compaction
596 && matches!(
597 current_filter_type,
598 PbSstableFilterType::SstableFilterXor8 | PbSstableFilterType::SstableFilterXor16
599 )
600 && all_ssts_are_blocked_filter
601 && all_ssts_match_filter_type
602 && output_filter_layout == PbSstableFilterLayout::Blocked
603 && !compact_task.contains_range_tombstone()
604 && !compact_task.contains_ttl()
605 && !compact_task.contains_split_sst()
606 && single_table
607 && compact_task.target_level > 0
608 && compact_task.input_ssts.len() == 2
609 && compaction_size < context.storage_opts.compactor_fast_max_compact_task_size
610 && delete_key_count * 100
611 < context.storage_opts.compactor_fast_max_compact_delete_ratio as u64 * total_key_count
612 && compact_task.task_type == PbTaskType::Dynamic
613}
614
615pub async fn generate_splits_for_task(
616 compact_task: &mut CompactTask,
617 context: &CompactorContext,
618 optimize_by_copy_block: bool,
619) -> HummockResult<()> {
620 let input_ssts = compact_task.read_input_ssts().collect_vec();
621 let compaction_size = input_ssts_size(&input_ssts);
622
623 if !optimize_by_copy_block {
624 let splits = generate_splits(
625 &input_ssts,
626 compaction_size,
627 context,
628 compact_task.max_sub_compaction,
629 )
630 .await?;
631 if !splits.is_empty() {
632 compact_task.splits = splits;
633 }
634 return Ok(());
635 }
636
637 Ok(())
638}
639
640pub fn metrics_report_for_task(compact_task: &CompactTask, context: &CompactorContext) {
641 let group_label = compact_task.compaction_group_id.to_string();
642 let cur_level_label = compact_task.input_ssts[0].level_idx.to_string();
643
644 let (select_size, select_count) = read_sstable_size_and_count(
645 compact_task
646 .input_ssts
647 .iter()
648 .filter(|level| level.level_idx != compact_task.target_level)
649 .flat_map(|level| level.read_sstable_infos()),
650 );
651 let (target_level_read_bytes, target_count) = read_sstable_size_and_count(
652 compact_task
653 .input_ssts
654 .iter()
655 .filter(|level| level.level_idx == compact_task.target_level)
656 .flat_map(|level| level.read_sstable_infos()),
657 );
658
659 context
660 .compactor_metrics
661 .compact_read_current_level
662 .with_label_values(&[&group_label, &cur_level_label])
663 .inc_by(select_size);
664 context
665 .compactor_metrics
666 .compact_read_sstn_current_level
667 .with_label_values(&[&group_label, &cur_level_label])
668 .inc_by(select_count as u64);
669
670 let next_level_label = compact_task.target_level.to_string();
671 context
672 .compactor_metrics
673 .compact_read_next_level
674 .with_label_values(&[&group_label, &next_level_label])
675 .inc_by(target_level_read_bytes);
676 context
677 .compactor_metrics
678 .compact_read_sstn_next_level
679 .with_label_values(&[&group_label, &next_level_label])
680 .inc_by(target_count as u64);
681}
682
683fn read_sstable_size_and_count<'a>(
684 sstable_infos: impl IntoIterator<Item = &'a SstableInfo>,
685) -> (u64, usize) {
686 sstable_infos
687 .into_iter()
688 .fold((0, 0), |(size, count), table_info| {
689 (size + table_info.sst_size, count + 1)
690 })
691}
692
693pub fn calculate_task_parallelism(compact_task: &CompactTask, context: &CompactorContext) -> usize {
694 let input_ssts = compact_task.read_input_ssts().collect_vec();
695 let compaction_size = input_ssts_size(&input_ssts);
696 let optimize_by_copy_block =
697 optimize_by_copy_block_with_input(compact_task, context, &input_ssts, compaction_size);
698
699 if optimize_by_copy_block {
700 return 1;
701 }
702
703 let parallel_compact_size = (context.storage_opts.parallel_compact_size_mb as u64) << 20;
704 calculate_task_parallelism_impl(
705 context.compaction_executor.worker_num(),
706 parallel_compact_size,
707 compaction_size,
708 compact_task.max_sub_compaction,
709 )
710}
711
712fn input_ssts_size(input_ssts: &[&SstableInfo]) -> u64 {
713 input_ssts
714 .iter()
715 .map(|table_info| table_info.sst_size)
716 .sum()
717}
718
719pub fn calculate_task_parallelism_impl(
720 worker_num: usize,
721 parallel_compact_size: u64,
722 compaction_size: u64,
723 max_sub_compaction: u32,
724) -> usize {
725 let parallelism = compaction_size.div_ceil(parallel_compact_size);
726 worker_num.min(parallelism.min(max_sub_compaction as u64) as usize)
727}
728
729#[cfg(test)]
730mod tests {
731 use std::sync::Arc;
732
733 use risingwave_common::catalog::TableId;
734 use risingwave_hummock_sdk::level::InputLevel;
735 use risingwave_hummock_sdk::sstable_info::SstableInfoInner;
736 use risingwave_pb::hummock::compact_task::PbTaskType;
737 use risingwave_pb::hummock::{PbLevelType, PbSstableFilterLayout, PbSstableFilterType};
738
739 use super::{
740 CompactTask, CompactorContext, estimate_output_key_count_by_size, optimize_by_copy_block,
741 };
742 use crate::hummock::compactor::new_compaction_await_tree_reg_ref;
743 use crate::hummock::iterator::test_utils::mock_sstable_store;
744 use crate::monitor::CompactorMetrics;
745 use crate::opts::StorageOpts;
746
747 fn test_sstable(
748 table_id: TableId,
749 total_key_count: u64,
750 filter_type: PbSstableFilterType,
751 ) -> risingwave_hummock_sdk::sstable_info::SstableInfo {
752 SstableInfoInner {
753 object_id: 1.into(),
754 sst_id: 1.into(),
755 table_ids: vec![table_id],
756 total_key_count,
757 sst_size: 1024,
758 uncompressed_file_size: 1024,
759 filter_type,
760 filter_layout: PbSstableFilterLayout::Blocked,
761 ..Default::default()
762 }
763 .into()
764 }
765
766 async fn test_context() -> CompactorContext {
767 CompactorContext::new_local_compact_context(
768 Arc::new(StorageOpts::default()),
769 mock_sstable_store().await,
770 Arc::new(CompactorMetrics::unused()),
771 Some(new_compaction_await_tree_reg_ref(
772 await_tree::Config::default(),
773 )),
774 )
775 }
776
777 fn test_compact_task(
778 layout: PbSstableFilterLayout,
779 blocked_xor_filter_kv_count_threshold: Option<u64>,
780 filter_type: PbSstableFilterType,
781 ) -> CompactTask {
782 let table_id = TableId::new(1);
783 CompactTask {
784 input_ssts: vec![
785 InputLevel {
786 level_idx: 1,
787 level_type: PbLevelType::Nonoverlapping,
788 table_infos: vec![test_sstable(table_id, 10, filter_type)],
789 },
790 InputLevel {
791 level_idx: 2,
792 level_type: PbLevelType::Nonoverlapping,
793 table_infos: vec![test_sstable(table_id, 10, filter_type)],
794 },
795 ],
796 existing_table_ids: vec![table_id],
797 target_level: 2,
798 target_file_size: 1024,
799 task_type: PbTaskType::Dynamic,
800 sstable_filter_type: filter_type,
801 sstable_filter_layout: layout,
802 blocked_xor_filter_kv_count_threshold,
803 ..Default::default()
804 }
805 }
806
807 #[test]
808 fn test_estimate_output_key_count_by_size_scales_to_output_sst() {
809 let estimated_key_count =
810 estimate_output_key_count_by_size(1024 * 1024, 512 * 1024 * 1024, 128 * 1024 * 1024);
811
812 assert_eq!(estimated_key_count, 256 * 1024);
813 assert_eq!(estimate_output_key_count_by_size(100, 0, 0), 100);
814 }
815
816 #[tokio::test]
817 async fn test_optimize_by_copy_block_respects_layout_policy() {
818 let context = test_context().await;
819 let compact_task = test_compact_task(
820 PbSstableFilterLayout::Plain,
821 Some(1),
822 PbSstableFilterType::SstableFilterXor16,
823 );
824
825 assert!(!optimize_by_copy_block(&compact_task, &context));
826
827 let compact_task = test_compact_task(
828 PbSstableFilterLayout::Auto,
829 Some(1024),
830 PbSstableFilterType::SstableFilterXor16,
831 );
832
833 assert!(!optimize_by_copy_block(&compact_task, &context));
834 }
835
836 #[tokio::test]
837 async fn test_optimize_by_copy_block_supports_blocked_xor_filters() {
838 let context = test_context().await;
839 let compact_task = test_compact_task(
840 PbSstableFilterLayout::Blocked,
841 Some(1024),
842 PbSstableFilterType::SstableFilterXor16,
843 );
844
845 assert!(optimize_by_copy_block(&compact_task, &context));
846
847 let compact_task = test_compact_task(
848 PbSstableFilterLayout::Blocked,
849 Some(1024),
850 PbSstableFilterType::SstableFilterXor8,
851 );
852
853 assert!(optimize_by_copy_block(&compact_task, &context));
854 }
855}