1use std::collections::{BinaryHeap, HashMap, HashSet};
16use std::sync::Arc;
17
18use await_tree::{InstrumentAwait, SpanExt};
19use bytes::Bytes;
20use futures::{FutureExt, StreamExt, stream};
21use itertools::Itertools;
22use risingwave_common::catalog::TableId;
23use risingwave_common::config::ObjectStoreConfig;
24use risingwave_common::util::value_encoding::column_aware_row_encoding::try_drop_invalid_columns;
25use risingwave_hummock_sdk::compact::{
26 compact_task_to_string, estimate_memory_for_compact_task, statistics_compact_task,
27};
28use risingwave_hummock_sdk::compact_task::CompactTask;
29use risingwave_hummock_sdk::compaction_group::StateTableId;
30use risingwave_hummock_sdk::key::{FullKey, FullKeyTracker};
31use risingwave_hummock_sdk::key_range::{KeyRange, KeyRangeCommon};
32use risingwave_hummock_sdk::sstable_info::SstableInfo;
33use risingwave_hummock_sdk::table_stats::{TableStats, TableStatsMap, add_table_stats_map};
34use risingwave_hummock_sdk::{
35 HummockSstableObjectId, KeyComparator, can_concat, compact_task_output_to_string,
36 full_key_can_concat,
37};
38use risingwave_object_store::object::ObjectStoreImpl;
39use risingwave_pb::hummock::compact_task::TaskStatus;
40use risingwave_pb::hummock::{LevelType, PbSstableFilterType};
41use thiserror_ext::AsReport;
42use tokio::sync::oneshot::Receiver;
43
44use super::iterator::MonitoredCompactorIterator;
45use super::task_progress::TaskProgress;
46use super::{CompactionStatistics, TaskConfig};
47use crate::compaction_catalog_manager::{CompactionCatalogAgentRef, CompactionCatalogManagerRef};
48use crate::hummock::compactor::compaction_utils::{
49 blocked_xor_filter_key_count_threshold, build_multi_compaction_filter,
50 estimate_output_key_count_for_task, estimate_task_output_capacity, generate_splits_for_task,
51 metrics_report_for_task, optimize_by_copy_block,
52};
53use crate::hummock::compactor::iterator::ConcatSstableIterator;
54use crate::hummock::compactor::task_progress::TaskProgressGuard;
55use crate::hummock::compactor::{
56 CompactOutput, CompactionFilter, Compactor, CompactorContext, await_tree_key,
57 fast_compactor_runner,
58};
59use crate::hummock::iterator::{
60 Forward, HummockIterator, MergeIterator, NonPkPrefixSkipWatermarkIterator,
61 NonPkPrefixSkipWatermarkState, PkPrefixSkipWatermarkIterator, PkPrefixSkipWatermarkState,
62 ValueMeta, ValueSkipWatermarkIterator, ValueSkipWatermarkState,
63};
64use crate::hummock::multi_builder::{CapacitySplitTableBuilder, TableBuilderFactory};
65use crate::hummock::utils::MemoryTracker;
66use crate::hummock::value::HummockValue;
67use crate::hummock::{
68 BlockedXor8FilterBuilder, BlockedXor16FilterBuilder, CachePolicy, CompressionAlgorithm,
69 GetObjectId, HummockError, HummockResult, SstableBuilderOptions, SstableStoreRef,
70};
71use crate::monitor::{CompactorMetrics, StoreLocalStatistic};
72pub struct CompactorRunner {
73 compact_task: CompactTask,
74 compactor: Compactor,
75 sstable_store: SstableStoreRef,
76 key_range: KeyRange,
77 split_index: usize,
78}
79
80impl CompactorRunner {
81 pub fn new(
82 split_index: usize,
83 context: CompactorContext,
84 task: CompactTask,
85 object_id_getter: Arc<dyn GetObjectId>,
86 ) -> Self {
87 let mut options: SstableBuilderOptions = context.storage_opts.as_ref().into();
88 options.compression_algorithm = match task.compression_algorithm {
89 0 => CompressionAlgorithm::None,
90 1 => CompressionAlgorithm::Lz4,
91 _ => CompressionAlgorithm::Zstd,
92 };
93
94 options.capacity = estimate_task_output_capacity(context.clone(), &task);
95 let estimated_output_key_count =
96 estimate_output_key_count_for_task(&task, options.capacity);
97 options.estimated_output_key_count = Some(estimated_output_key_count);
98 options.filter_hash_prealloc_key_count_cap =
99 blocked_xor_filter_key_count_threshold(task.blocked_xor_filter_kv_count_threshold);
100 options.max_vnode_key_range_bytes = task.effective_max_vnode_key_range_bytes();
101 let sstable_filter_layout =
102 task.sstable_filter_layout_for_output(estimated_output_key_count as u64);
103
104 let key_range = KeyRange {
105 left: task.splits[split_index].left.clone(),
106 right: task.splits[split_index].right.clone(),
107 right_exclusive: true,
108 };
109 let compactor = Compactor::new(
110 context.clone(),
111 options,
112 TaskConfig {
113 key_range: key_range.clone(),
114 cache_policy: CachePolicy::NotFill,
115 gc_delete_keys: task.gc_delete_keys,
116 retain_multiple_version: false,
117 sstable_filter_layout,
118 sstable_filter_type: task.sstable_filter_type,
119 table_vnode_partition: task.table_vnode_partition.clone(),
120 table_schemas: task
121 .table_schemas
122 .iter()
123 .map(|(k, v)| (*k, v.clone()))
124 .collect(),
125 disable_drop_column_optimization: false,
126 },
127 object_id_getter,
128 );
129
130 Self {
131 compactor,
132 compact_task: task,
133 sstable_store: context.sstable_store,
134 key_range,
135 split_index,
136 }
137 }
138
139 pub async fn run(
140 &self,
141 compaction_filter: impl CompactionFilter,
142 compaction_catalog_agent_ref: CompactionCatalogAgentRef,
143 task_progress: Arc<TaskProgress>,
144 ) -> HummockResult<CompactOutput> {
145 let iter =
146 self.build_sst_iter(task_progress.clone(), compaction_catalog_agent_ref.clone())?;
147 let (ssts, compaction_stat) = self
148 .compactor
149 .compact_key_range(
150 iter,
151 compaction_filter,
152 compaction_catalog_agent_ref,
153 Some(task_progress),
154 Some(self.compact_task.task_id),
155 Some(self.split_index),
156 )
157 .await?;
158 Ok((self.split_index, ssts, compaction_stat))
159 }
160
161 fn build_sst_iter(
163 &self,
164 task_progress: Arc<TaskProgress>,
165 compaction_catalog_agent_ref: CompactionCatalogAgentRef,
166 ) -> HummockResult<impl HummockIterator<Direction = Forward> + use<>> {
167 let compactor_iter_max_io_retry_times = self
168 .compactor
169 .context
170 .storage_opts
171 .compactor_iter_max_io_retry_times;
172 let mut table_iters = Vec::new();
173
174 for level in &self.compact_task.input_ssts {
175 let tables = level
176 .read_sstable_infos()
177 .filter(|table_info| self.key_range.full_key_overlap(&table_info.key_range))
178 .cloned()
179 .collect_vec();
180 if tables.is_empty() {
181 continue;
182 }
183
184 if level.level_type == LevelType::Nonoverlapping {
185 debug_assert!(can_concat(&tables));
186 table_iters.push(ConcatSstableIterator::new(
187 tables,
188 self.compactor.task_config.key_range.clone(),
189 self.sstable_store.clone(),
190 task_progress.clone(),
191 compactor_iter_max_io_retry_times,
192 ));
193 } else if tables.len()
194 > self
195 .compactor
196 .context
197 .storage_opts
198 .compactor_max_overlap_sst_count
199 {
200 let sst_groups = partition_overlapping_sstable_infos(tables);
201 tracing::warn!(
202 "COMPACT A LARGE OVERLAPPING LEVEL: try to partition {} ssts with {} groups",
203 sst_groups.iter().map(Vec::len).sum::<usize>(),
204 sst_groups.len()
205 );
206 for (idx, table_infos) in sst_groups.into_iter().enumerate() {
207 assert!(
209 full_key_can_concat(&table_infos),
210 "sst_group idx {:?} table_infos: {:?}",
211 idx,
212 table_infos
213 );
214 table_iters.push(ConcatSstableIterator::new(
215 table_infos,
216 self.compactor.task_config.key_range.clone(),
217 self.sstable_store.clone(),
218 task_progress.clone(),
219 compactor_iter_max_io_retry_times,
220 ));
221 }
222 } else {
223 for table_info in tables {
224 table_iters.push(ConcatSstableIterator::new(
225 vec![table_info],
226 self.compactor.task_config.key_range.clone(),
227 self.sstable_store.clone(),
228 task_progress.clone(),
229 compactor_iter_max_io_retry_times,
230 ));
231 }
232 }
233 }
234
235 let combine_iter = {
240 let skip_watermark_iter = PkPrefixSkipWatermarkIterator::new(
241 MonitoredCompactorIterator::new(
242 MergeIterator::for_compactor(table_iters),
243 task_progress,
244 ),
245 PkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
246 self.compact_task.pk_prefix_table_watermarks.clone(),
247 ),
248 );
249
250 let pk_skip_watermark_iter = NonPkPrefixSkipWatermarkIterator::new(
251 skip_watermark_iter,
252 NonPkPrefixSkipWatermarkState::from_safe_epoch_watermarks(
253 self.compact_task.non_pk_prefix_table_watermarks.clone(),
254 compaction_catalog_agent_ref.clone(),
255 ),
256 );
257
258 ValueSkipWatermarkIterator::new(
259 pk_skip_watermark_iter,
260 ValueSkipWatermarkState::from_safe_epoch_watermarks(
261 self.compact_task.value_table_watermarks.clone(),
262 compaction_catalog_agent_ref,
263 ),
264 )
265 };
266
267 Ok(combine_iter)
268 }
269}
270
271pub fn partition_overlapping_sstable_infos(
272 mut origin_infos: Vec<SstableInfo>,
273) -> Vec<Vec<SstableInfo>> {
274 pub struct SstableGroup {
275 ssts: Vec<SstableInfo>,
276 max_right_bound: Bytes,
277 }
278
279 impl PartialEq for SstableGroup {
280 fn eq(&self, other: &SstableGroup) -> bool {
281 self.max_right_bound == other.max_right_bound
282 }
283 }
284 impl PartialOrd for SstableGroup {
285 fn partial_cmp(&self, other: &SstableGroup) -> Option<std::cmp::Ordering> {
286 Some(self.cmp(other))
287 }
288 }
289 impl Eq for SstableGroup {}
290 impl Ord for SstableGroup {
291 fn cmp(&self, other: &SstableGroup) -> std::cmp::Ordering {
292 KeyComparator::compare_encoded_full_key(&other.max_right_bound, &self.max_right_bound)
294 }
295 }
296 let mut groups: BinaryHeap<SstableGroup> = BinaryHeap::default();
297 origin_infos.sort_by(|a, b| {
298 let x = &a.key_range;
299 let y = &b.key_range;
300 KeyComparator::compare_encoded_full_key(&x.left, &y.left)
301 });
302 for sst in origin_infos {
303 if let Some(mut prev_group) = groups.peek_mut()
305 && KeyComparator::encoded_full_key_less_than(
306 &prev_group.max_right_bound,
307 &sst.key_range.left,
308 )
309 {
310 prev_group.max_right_bound.clone_from(&sst.key_range.right);
311 prev_group.ssts.push(sst);
312 continue;
313 }
314 groups.push(SstableGroup {
315 max_right_bound: sst.key_range.right.clone(),
316 ssts: vec![sst],
317 });
318 }
319 assert!(!groups.is_empty());
320 groups.into_iter().map(|group| group.ssts).collect_vec()
321}
322
323fn estimate_input_stream_buffer_size(store: &ObjectStoreImpl, config: &ObjectStoreConfig) -> u64 {
326 let recv_buffer_size = config.s3.recv_buffer_size.unwrap_or(6 * 1024 * 1024) as u64;
327 if !matches!(store, ObjectStoreImpl::Opendal(_))
328 || config.opendal_streaming_read_chunk_size == 0
329 {
330 return recv_buffer_size;
331 }
332
333 let concurrent = config.opendal_streaming_read_chunk_concurrency.max(1) as u64;
334 let chunk_buffers = (config.opendal_streaming_read_chunk_size as u64)
338 .saturating_mul(concurrent.saturating_add(2));
339 recv_buffer_size
340 .saturating_mul(concurrent)
341 .saturating_add(chunk_buffers)
342}
343
344pub async fn compact_with_agent(
347 compactor_context: CompactorContext,
348 mut compact_task: CompactTask,
349 mut shutdown_rx: Receiver<()>,
350 object_id_getter: Arc<dyn GetObjectId>,
351 compaction_catalog_agent_ref: CompactionCatalogAgentRef,
352) -> (
353 (
354 CompactTask,
355 HashMap<TableId, TableStats>,
356 HashMap<HummockSstableObjectId, u64>,
357 ),
358 Option<MemoryTracker>,
359) {
360 let context = compactor_context.clone();
361 let group_label = compact_task.compaction_group_id.to_string();
362 metrics_report_for_task(&compact_task, &context);
363
364 let timer = context
365 .compactor_metrics
366 .compact_task_duration
367 .with_label_values(&[
368 &group_label,
369 &compact_task.input_ssts[0].level_idx.to_string(),
370 ])
371 .start_timer();
372
373 let multi_filter = build_multi_compaction_filter(&compact_task);
374 let mut task_status = TaskStatus::Success;
375 let optimize_by_copy_block = optimize_by_copy_block(&compact_task, &context);
376
377 if let Err(e) =
378 generate_splits_for_task(&mut compact_task, &context, optimize_by_copy_block).await
379 {
380 tracing::warn!(error = %e.as_report(), "Failed to generate_splits");
381 task_status = TaskStatus::ExecuteFailed;
382 return (
383 compact_done(compact_task, context.clone(), vec![], task_status),
384 None,
385 );
386 }
387
388 let compact_task_statistics = statistics_compact_task(&compact_task);
389 let parallelism = compact_task.splits.len();
391 assert_ne!(parallelism, 0, "splits cannot be empty");
392 let mut output_ssts = Vec::with_capacity(parallelism);
393 let mut compaction_futures = vec![];
394 let mut abort_handles = vec![];
395 let task_progress_guard =
396 TaskProgressGuard::new(compact_task.task_id, context.task_progress_manager.clone());
397
398 let capacity = estimate_task_output_capacity(context.clone(), &compact_task);
399
400 let input_buffer_size = estimate_input_stream_buffer_size(
401 context.sstable_store.store().as_ref(),
402 &context.storage_opts.object_store_config,
403 );
404 let task_memory_capacity_with_parallelism = estimate_memory_for_compact_task(
405 &compact_task,
406 (context.storage_opts.block_size_kb as u64) * (1 << 10),
407 input_buffer_size,
408 capacity as u64,
409 )
410 .saturating_mul(parallelism as u64);
411
412 tracing::info!(
413 "Ready to handle task: {} compact_task_statistics {:?} compression_algorithm {:?} parallelism {} task_memory_capacity_with_parallelism {}, enable fast runner: {}, {}",
414 compact_task.task_id,
415 compact_task_statistics,
416 compact_task.compression_algorithm,
417 parallelism,
418 task_memory_capacity_with_parallelism,
419 optimize_by_copy_block,
420 compact_task_to_string(&compact_task),
421 );
422
423 let memory_detector = if task_memory_capacity_with_parallelism == u64::MAX {
427 None
428 } else {
429 context
430 .memory_limiter
431 .try_require_memory(task_memory_capacity_with_parallelism)
432 };
433 if memory_detector.is_none() {
434 tracing::warn!(
435 "Not enough memory to serve the task {} task_memory_capacity_with_parallelism {} memory_usage {} memory_quota {}",
436 compact_task.task_id,
437 task_memory_capacity_with_parallelism,
438 context.memory_limiter.get_memory_usage(),
439 context.memory_limiter.quota()
440 );
441 task_status = TaskStatus::NoAvailMemoryResourceCanceled;
442 return (
443 compact_done(compact_task, context.clone(), output_ssts, task_status),
444 memory_detector,
445 );
446 }
447
448 context.compactor_metrics.compact_task_pending_num.inc();
449 context
450 .compactor_metrics
451 .compact_task_pending_parallelism
452 .add(parallelism as _);
453 let _release_metrics_guard =
454 scopeguard::guard((parallelism, context.clone()), |(parallelism, context)| {
455 context.compactor_metrics.compact_task_pending_num.dec();
456 context
457 .compactor_metrics
458 .compact_task_pending_parallelism
459 .sub(parallelism as _);
460 });
461
462 if optimize_by_copy_block {
463 let fast_compaction = {
464 let context = context.clone();
465 let task = compact_task.clone();
466 let compaction_catalog_agent_ref = compaction_catalog_agent_ref.clone();
467 let object_id_getter = object_id_getter.clone();
468 let task_progress = task_progress_guard.progress.clone();
469
470 async move {
471 match task.sstable_filter_type {
472 PbSstableFilterType::SstableFilterXor8 => {
473 fast_compactor_runner::CompactorRunner::<BlockedXor8FilterBuilder, _>::new(
474 context,
475 task,
476 compaction_catalog_agent_ref,
477 object_id_getter,
478 task_progress,
479 multi_filter,
480 )
481 .run()
482 .await
483 }
484 PbSstableFilterType::SstableFilterXor16 => {
485 fast_compactor_runner::CompactorRunner::<BlockedXor16FilterBuilder, _>::new(
486 context,
487 task,
488 compaction_catalog_agent_ref,
489 object_id_getter,
490 task_progress,
491 multi_filter,
492 )
493 .run()
494 .await
495 }
496 filter_type => Err(HummockError::compaction_executor(format!(
497 "fast compaction only supports blocked xor filters, got {:?}",
498 filter_type
499 ))),
500 }
501 }
502 };
503
504 tokio::select! {
505 _ = &mut shutdown_rx => {
506 tracing::warn!("Compaction task cancelled externally:\n{}", compact_task_to_string(&compact_task));
507 task_status = TaskStatus::ManualCanceled;
508 },
509
510 ret = fast_compaction => {
511 match ret {
512 Ok((ssts, statistics)) => {
513 output_ssts.push((0, ssts, statistics));
514 }
515 Err(e) => {
516 task_status = TaskStatus::ExecuteFailed;
517 tracing::warn!(
518 error = %e.as_report(),
519 "Compaction task {} failed with error",
520 compact_task.task_id,
521 );
522 }
523 }
524 }
525 }
526
527 let (compact_task, table_stats, object_timestamps) =
529 compact_done(compact_task, context.clone(), output_ssts, task_status);
530 let cost_time = timer.stop_and_record() * 1000.0;
531 tracing::info!(
532 "Finished fast compaction task in {:?}ms: {}",
533 cost_time,
534 compact_task_to_string(&compact_task)
535 );
536 return (
537 (compact_task, table_stats, object_timestamps),
538 memory_detector,
539 );
540 }
541 for (split_index, _) in compact_task.splits.iter().enumerate() {
542 let filter = multi_filter.clone();
543 let compaction_catalog_agent_ref = compaction_catalog_agent_ref.clone();
544 let compactor_runner = CompactorRunner::new(
545 split_index,
546 compactor_context.clone(),
547 compact_task.clone(),
548 object_id_getter.clone(),
549 );
550 let task_progress = task_progress_guard.progress.clone();
551 let runner = async move {
552 compactor_runner
553 .run(filter, compaction_catalog_agent_ref, task_progress)
554 .await
555 };
556 let traced = match context.await_tree_reg.as_ref() {
557 None => runner.right_future(),
558 Some(await_tree_reg) => await_tree_reg
559 .register(
560 await_tree_key::CompactRunner {
561 task_id: compact_task.task_id,
562 split_index,
563 },
564 format!(
565 "Compaction Task {} Split {} ",
566 compact_task.task_id, split_index
567 ),
568 )
569 .instrument(runner)
570 .left_future(),
571 };
572 let handle = tokio::spawn(traced);
573 abort_handles.push(handle.abort_handle());
574 compaction_futures.push(handle);
575 }
576
577 let mut buffered = stream::iter(compaction_futures).buffer_unordered(parallelism);
578 loop {
579 tokio::select! {
580 _ = &mut shutdown_rx => {
581 tracing::warn!("Compaction task cancelled externally:\n{}", compact_task_to_string(&compact_task));
582 task_status = TaskStatus::ManualCanceled;
583 break;
584 }
585 future_result = buffered.next() => {
586 match future_result {
587 Some(Ok(Ok((split_index, ssts, compact_stat)))) => {
588 output_ssts.push((split_index, ssts, compact_stat));
589 }
590 Some(Ok(Err(e))) => {
591 task_status = TaskStatus::ExecuteFailed;
592 tracing::warn!(
593 error = %e.as_report(),
594 "Compaction task {} failed with error",
595 compact_task.task_id,
596 );
597 break;
598 }
599 Some(Err(e)) => {
600 task_status = TaskStatus::JoinHandleFailed;
601 tracing::warn!(
602 error = %e.as_report(),
603 "Compaction task {} failed with join handle error",
604 compact_task.task_id,
605 );
606 break;
607 }
608 None => break,
609 }
610 }
611 }
612 }
613
614 if task_status != TaskStatus::Success {
615 for abort_handle in abort_handles {
616 abort_handle.abort();
617 }
618 output_ssts.clear();
619 }
620 if !output_ssts.is_empty() {
622 output_ssts.sort_by_key(|(split_index, ..)| *split_index);
623 }
624
625 let (compact_task, table_stats, object_timestamps) =
627 compact_done(compact_task, context.clone(), output_ssts, task_status);
628 let cost_time = timer.stop_and_record() * 1000.0;
629 tracing::info!(
630 "Finished compaction task in {:?}ms: {}",
631 cost_time,
632 compact_task_output_to_string(&compact_task)
633 );
634 (
635 (compact_task, table_stats, object_timestamps),
636 memory_detector,
637 )
638}
639
640pub(crate) async fn acquire_complete_catalog_agent(
641 compaction_catalog_manager_ref: &CompactionCatalogManagerRef,
642 read_table_ids: Vec<StateTableId>,
643) -> HummockResult<CompactionCatalogAgentRef> {
644 let expected_table_ids = read_table_ids.iter().copied().collect::<HashSet<_>>();
645 let compaction_catalog_agent_ref = compaction_catalog_manager_ref
646 .acquire(read_table_ids)
647 .await?;
648 let acquired_table_ids = compaction_catalog_agent_ref
649 .table_ids()
650 .collect::<HashSet<_>>();
651
652 if acquired_table_ids != expected_table_ids {
653 let diff = expected_table_ids
654 .symmetric_difference(&acquired_table_ids)
655 .cloned()
656 .collect::<Vec<_>>();
657 return Err(HummockError::other(format!(
658 "some table ids are not acquired: {:?}",
659 diff
660 )));
661 }
662
663 Ok(compaction_catalog_agent_ref)
664}
665
666pub async fn compact(
669 compactor_context: CompactorContext,
670 compact_task: CompactTask,
671 shutdown_rx: Receiver<()>,
672 object_id_getter: Arc<dyn GetObjectId>,
673 compaction_catalog_manager_ref: CompactionCatalogManagerRef,
674) -> (
675 (
676 CompactTask,
677 HashMap<TableId, TableStats>,
678 HashMap<HummockSstableObjectId, u64>,
679 ),
680 Option<MemoryTracker>,
681) {
682 let read_table_ids = compact_task.get_table_ids_from_input_ssts().collect_vec();
683 let compaction_catalog_agent_ref =
684 match acquire_complete_catalog_agent(&compaction_catalog_manager_ref, read_table_ids).await
685 {
686 Ok(compaction_catalog_agent_ref) => compaction_catalog_agent_ref,
687 Err(e) => {
688 tracing::warn!(
689 error = %e.as_report(),
690 "Failed to acquire complete compaction catalog agent"
691 );
692 return (
693 compact_done(
694 compact_task,
695 compactor_context.clone(),
696 vec![],
697 TaskStatus::ExecuteFailed,
698 ),
699 None,
700 );
701 }
702 };
703
704 compact_with_agent(
705 compactor_context,
706 compact_task,
707 shutdown_rx,
708 object_id_getter,
709 compaction_catalog_agent_ref,
710 )
711 .await
712}
713
714pub(crate) fn compact_done(
716 mut compact_task: CompactTask,
717 context: CompactorContext,
718 output_ssts: Vec<CompactOutput>,
719 task_status: TaskStatus,
720) -> (
721 CompactTask,
722 HashMap<TableId, TableStats>,
723 HashMap<HummockSstableObjectId, u64>,
724) {
725 let mut table_stats_map = TableStatsMap::default();
726 let mut object_timestamps = HashMap::default();
727 compact_task.task_status = task_status;
728 compact_task
729 .sorted_output_ssts
730 .reserve(compact_task.splits.len());
731 let mut compaction_write_bytes = 0;
732 for (
733 _,
734 ssts,
735 CompactionStatistics {
736 delta_drop_stat, ..
737 },
738 ) in output_ssts
739 {
740 add_table_stats_map(&mut table_stats_map, &delta_drop_stat);
741 for sst_info in ssts {
742 compaction_write_bytes += sst_info.file_size();
743 object_timestamps.insert(sst_info.sst_info.object_id, sst_info.created_at);
744 compact_task.sorted_output_ssts.push(sst_info.sst_info);
745 }
746 }
747 assert_output_table_ids_subset_of_read_input(&compact_task);
748
749 let group_label = compact_task.compaction_group_id.to_string();
750 let level_label = compact_task.target_level.to_string();
751 context
752 .compactor_metrics
753 .compact_write_bytes
754 .with_label_values(&[&group_label, &level_label])
755 .inc_by(compaction_write_bytes);
756 context
757 .compactor_metrics
758 .compact_write_sstn
759 .with_label_values(&[&group_label, &level_label])
760 .inc_by(compact_task.sorted_output_ssts.len() as u64);
761
762 (compact_task, table_stats_map, object_timestamps)
763}
764
765fn assert_output_table_ids_subset_of_read_input(compact_task: &CompactTask) {
767 let read_table_ids = compact_task
768 .get_table_ids_from_input_ssts()
769 .collect::<HashSet<_>>();
770
771 for output_sst in &compact_task.sorted_output_ssts {
772 assert!(
773 output_sst
774 .table_ids
775 .iter()
776 .all(|table_id| read_table_ids.contains(table_id)),
777 "compaction output contains table ids outside task read input: task_id={}, output_sst_id={}, read_table_ids={:?}, output_table_ids={:?}",
778 compact_task.task_id,
779 output_sst.sst_id,
780 read_table_ids,
781 output_sst.table_ids,
782 );
783 }
784}
785
786pub async fn compact_and_build_sst<F>(
787 sst_builder: &mut CapacitySplitTableBuilder<F>,
788 task_config: &TaskConfig,
789 compactor_metrics: Arc<CompactorMetrics>,
790 mut iter: impl HummockIterator<Direction = Forward>,
791 mut compaction_filter: impl CompactionFilter,
792) -> HummockResult<CompactionStatistics>
793where
794 F: TableBuilderFactory,
795{
796 if !task_config.key_range.left.is_empty() {
797 let full_key = FullKey::decode(&task_config.key_range.left);
798 iter.seek(full_key)
799 .instrument_await("iter_seek".verbose())
800 .await?;
801 } else {
802 iter.rewind().instrument_await("rewind".verbose()).await?;
803 };
804
805 let end_key = if task_config.key_range.right.is_empty() {
806 FullKey::default()
807 } else {
808 FullKey::decode(&task_config.key_range.right).to_vec()
809 };
810 let max_key = end_key.to_ref();
811
812 let mut full_key_tracker = FullKeyTracker::<Vec<u8>>::new(FullKey::default());
813 let mut local_stats = StoreLocalStatistic::default();
814
815 let mut table_stats_drop = TableStatsMap::default();
817 let mut last_table_stats = TableStats::default();
818 let mut last_table_id = None;
819 let mut compaction_statistics = CompactionStatistics::default();
820 let mut skip_schema_check: HashMap<HummockSstableObjectId, u64> = HashMap::default();
822 let schemas: HashMap<TableId, HashSet<i32>> = task_config
823 .table_schemas
824 .iter()
825 .map(|(table_id, schema)| (*table_id, schema.column_ids.iter().copied().collect()))
826 .collect();
827 while iter.is_valid() {
828 let iter_key = iter.key();
829 compaction_statistics.iter_total_key_counts += 1;
830
831 let is_new_user_key = full_key_tracker.observe(iter.key());
832 let mut drop = false;
833
834 let value = iter.value();
836 let ValueMeta {
837 object_id,
838 block_id,
839 } = iter.value_meta();
840 if is_new_user_key {
841 if !max_key.is_empty() && iter_key >= max_key {
842 break;
843 }
844 if value.is_delete() {
845 local_stats.skip_delete_key_count += 1;
846 }
847 } else {
848 local_stats.skip_multi_version_key_count += 1;
849 }
850
851 if last_table_id != Some(iter_key.user_key.table_id) {
852 if let Some(last_table_id) = last_table_id.take() {
853 table_stats_drop.insert(last_table_id, std::mem::take(&mut last_table_stats));
854 }
855 last_table_id = Some(iter_key.user_key.table_id);
856 }
857
858 if (!task_config.retain_multiple_version && task_config.gc_delete_keys && value.is_delete())
865 || (!task_config.retain_multiple_version && !is_new_user_key)
866 {
867 drop = true;
868 }
869
870 if !drop && compaction_filter.should_delete(iter_key) {
871 drop = true;
872 }
873
874 if drop {
875 compaction_statistics.iter_drop_key_counts += 1;
876
877 last_table_stats.total_key_count -= 1;
878 last_table_stats.total_key_size -= iter_key.encoded_len() as i64;
879 last_table_stats.total_value_size -= iter.value().encoded_len() as i64;
880 iter.next()
881 .instrument_await("iter_next_in_drop".verbose())
882 .await?;
883 continue;
884 }
885
886 let check_table_id = iter_key.user_key.table_id;
888 let mut is_value_rewritten = false;
889 if let HummockValue::Put(v) = value
890 && let Some(object_id) = object_id
891 && let Some(block_id) = block_id
892 && !skip_schema_check
893 .get(&object_id)
894 .map(|prev_block_id| {
895 assert!(*prev_block_id <= block_id);
896 *prev_block_id == block_id
897 })
898 .unwrap_or(false)
899 && let Some(schema) = schemas.get(&check_table_id)
900 {
901 let value_size = v.len();
902 match try_drop_invalid_columns(v, schema) {
903 None => {
904 if !task_config.disable_drop_column_optimization {
905 skip_schema_check.insert(object_id, block_id);
908 }
909 }
910 Some(new_value) => {
911 is_value_rewritten = true;
912 let new_put = HummockValue::put(new_value.as_slice());
913 sst_builder
914 .add_full_key(iter_key, new_put, is_new_user_key)
915 .instrument_await("add_rewritten_full_key".verbose())
916 .await?;
917 let value_size_change = value_size as i64 - new_value.len() as i64;
918 assert!(value_size_change >= 0);
919 last_table_stats.total_value_size -= value_size_change;
920 }
921 }
922 }
923
924 if !is_value_rewritten {
925 sst_builder
927 .add_full_key(iter_key, value, is_new_user_key)
928 .instrument_await("add_full_key".verbose())
929 .await?;
930 }
931
932 iter.next().instrument_await("iter_next".verbose()).await?;
933 }
934
935 if let Some(last_table_id) = last_table_id.take() {
936 table_stats_drop.insert(last_table_id, std::mem::take(&mut last_table_stats));
937 }
938 iter.collect_local_statistic(&mut local_stats);
939 add_table_stats_map(
940 &mut table_stats_drop,
941 &local_stats.skipped_by_watermark_table_stats,
942 );
943 local_stats.report_compactor(compactor_metrics.as_ref());
944 compaction_statistics.delta_drop_stat = table_stats_drop;
945
946 Ok(compaction_statistics)
947}
948
949#[cfg(test)]
950pub mod tests {
951 use std::sync::Arc;
952
953 use risingwave_common::config::ObjectStoreConfig;
954 use risingwave_hummock_sdk::can_concat;
955 use risingwave_hummock_sdk::compact::estimate_memory_for_compact_task;
956 use risingwave_hummock_sdk::compact_task::CompactTask;
957 use risingwave_hummock_sdk::level::InputLevel;
958 use risingwave_object_store::object::object_metrics::ObjectStoreMetrics;
959 use risingwave_object_store::object::{
960 InMemObjectStore, ObjectStore, ObjectStoreImpl, OpendalObjectStore,
961 };
962 use risingwave_pb::hummock::{LevelType, PbSstableInfo};
963
964 use crate::hummock::compactor::compactor_runner::{
965 estimate_input_stream_buffer_size, partition_overlapping_sstable_infos,
966 };
967 use crate::hummock::iterator::test_utils::mock_sstable_store;
968 use crate::hummock::test_utils::{
969 default_builder_opt_for_test, gen_test_sstable_info, test_key_of, test_value_of,
970 };
971 use crate::hummock::value::HummockValue;
972
973 #[test]
974 fn test_estimate_input_stream_buffer_size() {
975 const MIB: u64 = 1024 * 1024;
976 let metrics = Arc::new(ObjectStoreMetrics::unused());
977 let config = Arc::new(ObjectStoreConfig::default());
978 let opendal = ObjectStoreImpl::Opendal(
979 OpendalObjectStore::test_new_memory_engine()
980 .unwrap()
981 .monitored(metrics.clone(), config.clone()),
982 );
983 let in_mem =
984 ObjectStoreImpl::InMem(InMemObjectStore::default().monitored(metrics, config.clone()));
985 assert_eq!(
986 estimate_input_stream_buffer_size(&opendal, &config),
987 2 * MIB
988 );
989
990 let mut config = ObjectStoreConfig::default();
991 config.s3.recv_buffer_size = None;
992 assert_eq!(
993 estimate_input_stream_buffer_size(&opendal, &config),
994 6 * MIB
995 );
996 config.s3.recv_buffer_size = Some(MIB as usize);
997 for (chunk_size, concurrent, expected) in [
998 (0, 4, MIB),
999 (0, usize::MAX, MIB),
1000 (16 * MIB, 0, 49 * MIB),
1001 (16 * MIB, 1, 49 * MIB),
1002 (16 * MIB, 4, 100 * MIB),
1003 ] {
1004 config.opendal_streaming_read_chunk_size = chunk_size as usize;
1005 config.opendal_streaming_read_chunk_concurrency = concurrent;
1006 assert_eq!(
1007 estimate_input_stream_buffer_size(&opendal, &config),
1008 expected
1009 );
1010 assert_eq!(estimate_input_stream_buffer_size(&in_mem, &config), MIB);
1011 }
1012
1013 config.opendal_streaming_read_chunk_size = usize::MAX;
1014 assert_eq!(
1015 estimate_input_stream_buffer_size(&opendal, &config),
1016 u64::MAX
1017 );
1018 config.opendal_streaming_read_chunk_size = 1;
1019 config.opendal_streaming_read_chunk_concurrency = usize::MAX;
1020 assert_eq!(
1021 estimate_input_stream_buffer_size(&opendal, &config),
1022 u64::MAX
1023 );
1024 assert_eq!(estimate_input_stream_buffer_size(&in_mem, &config), MIB);
1025 }
1026
1027 #[test]
1028 fn test_chunk_buffer_allowance_saturates_task_estimate() {
1029 let sst = PbSstableInfo {
1030 file_size: 1000,
1031 meta_offset: 900,
1032 table_ids: vec![1.into()],
1033 ..Default::default()
1034 };
1035 let task = CompactTask {
1036 input_ssts: vec![InputLevel {
1037 level_type: LevelType::Overlapping,
1038 table_infos: vec![sst.clone().into(), sst.into()],
1039 ..Default::default()
1040 }],
1041 ..Default::default()
1042 };
1043 for buffer_size in [u64::MAX, u64::MAX / 2] {
1044 assert_eq!(
1045 estimate_memory_for_compact_task(&task, 128, buffer_size, 1000),
1046 u64::MAX
1047 );
1048 }
1049 }
1050
1051 #[tokio::test]
1052 async fn test_partition_overlapping_level() {
1053 const TEST_KEYS_COUNT: usize = 10;
1054 let sstable_store = mock_sstable_store().await;
1055 let mut table_infos = vec![];
1056 for object_id in 0..10 {
1057 let start_index = object_id * TEST_KEYS_COUNT;
1058 let end_index = start_index + 2 * TEST_KEYS_COUNT;
1059 let table_info = gen_test_sstable_info(
1060 default_builder_opt_for_test(),
1061 object_id as u64,
1062 (start_index..end_index)
1063 .map(|i| (test_key_of(i), HummockValue::put(test_value_of(i)))),
1064 sstable_store.clone(),
1065 )
1066 .await;
1067 table_infos.push(table_info);
1068 }
1069 let table_infos = partition_overlapping_sstable_infos(table_infos);
1070 assert_eq!(table_infos.len(), 2);
1071 for ssts in table_infos {
1072 assert!(can_concat(&ssts));
1073 }
1074 }
1075}