Skip to main content

risingwave_storage/hummock/compactor/
compactor_runner.rs

1// Copyright 2022 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::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    /// Build the merge iterator based on the given input ssts.
162    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                    // Overlapping sstables may contains ssts with same user key (generated by spilled), so we need to check concat with full key.
208                    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        // // The `SkipWatermarkIterator` is used to handle the table watermark state cleaning introduced
236        // // in https://github.com/risingwavelabs/risingwave/issues/13148
237        // The `Pk/NonPkPrefixSkipWatermarkIterator` is used to handle the table watermark state cleaning introduced
238        // in https://github.com/risingwavelabs/risingwave/issues/13148
239        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            // Pick group with the smallest right bound for every new sstable.
293            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        // Pick group with the smallest right bound for every new sstable. So do not check the larger one if the smallest one does not meet condition.
304        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
323/// Estimate transport and buffered payload memory for one compaction input stream.
324/// The allowance saturates at `u64::MAX` on overflow.
325fn 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    // OpenDAL's default prefetch is zero, so at most `concurrent` chunk payloads remain
335    // in its task queue or current buffer. Allow two more chunks for our bytes adapter's
336    // copy and the previously yielded buffer retained by BlockDataStream during that copy.
337    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
344/// Handles a compaction task and reports its status to hummock manager.
345/// Always return `Ok` and let hummock manager handle errors.
346pub 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    // Number of splits (key ranges) is equal to number of compaction tasks
390    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    // If the task does not have enough memory, it should cancel the task and let the meta
424    // reschedule it, so that it does not occupy the compactor's resources.
425    // Avoid overflowing the limiter's current usage when the estimate has saturated.
426    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        // After a compaction is done, mutate the compaction task.
528        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    // Sort by split/key range index.
621    if !output_ssts.is_empty() {
622        output_ssts.sort_by_key(|(split_index, ..)| *split_index);
623    }
624
625    // After a compaction is done, mutate the compaction task.
626    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
666/// Handles a compaction task and reports its status to hummock manager.
667/// Always return `Ok` and let hummock manager handle errors.
668pub 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
714/// Fills in the compact task and tries to report the task result to meta node.
715pub(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
765/// Compactor output must only contain table ids that are readable in the normalized task input.
766fn 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    // Keep table stats changes due to dropping KV.
816    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    // object id -> block id. For an object id, block id is updated in a monotonically increasing manner.
821    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        // CRITICAL WARN: Because of memtable spill, there may be several versions of the same user-key share the same `pure_epoch`. Do not change this code unless necessary.
835        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        // Among keys with same user key, only keep the latest key unless retain_multiple_version is true.
859        // In our design, frontend avoid to access keys which had be deleted, so we don't
860        // need to consider the epoch when the compaction_filter match (it
861        // means that mv had drop)
862        // Because of memtable spill, there may be a PUT key share the same `pure_epoch` with DELETE key.
863        // Do not assume that "the epoch of keys behind must be smaller than the current key."
864        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        // May drop stale columns
887        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                        // Under the assumption that all values in the same (object, block) group should share the same schema,
906                        // if one value drops no columns during a compaction, no need to check other values in the same group.
907                        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            // Don't allow two SSTs to share same user key
926            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}