Skip to main content

risingwave_storage/hummock/compactor/
mod.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
15mod compaction_executor;
16mod compaction_filter;
17pub mod compaction_utils;
18mod iceberg_compaction;
19use risingwave_hummock_sdk::compact_task::{CompactTask, ValidationTask};
20use risingwave_pb::compactor::{DispatchCompactionTaskRequest, dispatch_compaction_task_request};
21use risingwave_pb::hummock::PbCompactTask;
22use risingwave_pb::hummock::report_compaction_task_request::{
23    Event as ReportCompactionTaskEvent, HeartBeat as SharedHeartBeat,
24    ReportTask as ReportSharedTask,
25};
26use risingwave_pb::iceberg_compaction::{
27    SubscribeIcebergCompactionEventRequest, SubscribeIcebergCompactionEventResponse,
28    subscribe_iceberg_compaction_event_request,
29};
30use risingwave_rpc_client::GrpcCompactorProxyClient;
31use thiserror_ext::AsReport;
32use tokio::sync::mpsc;
33use tonic::Request;
34
35pub mod compactor_runner;
36mod context;
37pub mod fast_compactor_runner;
38mod iterator;
39mod shared_buffer_compact;
40pub(super) mod task_progress;
41
42use std::collections::hash_map::Entry;
43use std::collections::{HashMap, VecDeque};
44use std::marker::PhantomData;
45use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
46use std::sync::{Arc, Mutex};
47use std::time::{Duration, SystemTime};
48
49use await_tree::{InstrumentAwait, SpanExt};
50pub use compaction_executor::CompactionExecutor;
51pub use compaction_filter::{
52    CompactionFilter, DummyCompactionFilter, MultiCompactionFilter, TtlCompactionFilter,
53};
54pub use context::{
55    CompactionAwaitTreeRegRef, CompactorContext, await_tree_key, new_compaction_await_tree_reg_ref,
56};
57use futures::{StreamExt, pin_mut};
58// Import iceberg compactor runner types from the local `iceberg_compaction` module.
59use iceberg_compaction::iceberg_compactor_runner::IcebergCompactorRunnerConfigBuilder;
60use iceberg_compaction::{
61    IcebergPlanCompletion, IcebergTaskQueue, IcebergTaskReport, IcebergTaskTracker, PushResult,
62    ReportSendResult, build_iceberg_task_report, create_task_execution,
63    flush_pending_iceberg_task_reports, send_or_buffer_iceberg_task_report,
64};
65pub use iterator::{ConcatSstableIterator, SstableStreamIterator};
66use more_asserts::assert_ge;
67use risingwave_hummock_sdk::table_stats::{TableStatsMap, to_prost_table_stats_map};
68use risingwave_hummock_sdk::{
69    HummockCompactionTaskId, HummockSstableObjectId, LocalSstableInfo, compact_task_to_string,
70};
71use risingwave_pb::hummock::compact_task::TaskStatus;
72use risingwave_pb::hummock::subscribe_compaction_event_request::{
73    Event as RequestEvent, HeartBeat, PullTask, ReportTask,
74};
75use risingwave_pb::hummock::subscribe_compaction_event_response::Event as ResponseEvent;
76use risingwave_pb::hummock::{
77    CompactTaskProgress, PbSstableFilterLayout, PbSstableFilterType, ReportCompactionTaskRequest,
78    SubscribeCompactionEventRequest, SubscribeCompactionEventResponse,
79};
80use risingwave_rpc_client::HummockMetaClient;
81pub use shared_buffer_compact::compact;
82use tokio::sync::oneshot::Sender;
83use tokio::task::JoinHandle;
84use tokio::time::Instant;
85
86pub use self::compaction_utils::{
87    CompactionStatistics, RemoteBuilderFactory, TaskConfig, check_compaction_result,
88    check_flush_result,
89};
90pub use self::task_progress::TaskProgress;
91use super::multi_builder::CapacitySplitTableBuilder;
92use super::{
93    GetObjectId, HummockError, HummockErrorInner, HummockResult, ObjectIdManager,
94    SstableBuilderOptions, Xor8FilterBuilder, Xor16FilterBuilder,
95};
96use crate::compaction_catalog_manager::{
97    CompactionCatalogAgentRef, CompactionCatalogManager, CompactionCatalogManagerRef,
98};
99use crate::hummock::compactor::compaction_utils::calculate_task_parallelism;
100use crate::hummock::compactor::compactor_runner::{compact_and_build_sst, compact_done};
101use crate::hummock::compactor::iceberg_compaction::TaskKey;
102use crate::hummock::iterator::{Forward, HummockIterator};
103use crate::hummock::{
104    BlockedXor8FilterBuilder, BlockedXor16FilterBuilder, FilterBuilder, NoneFilterBuilder,
105    SharedComapctorObjectIdManager, SstableWriterFactory, UnifiedSstableWriterFactory,
106    validate_ssts,
107};
108use crate::monitor::CompactorMetrics;
109
110/// Heartbeat logging interval for compaction tasks
111const COMPACTION_HEARTBEAT_LOG_INTERVAL: Duration = Duration::from_secs(60);
112
113/// Represents the compaction task state for logging purposes
114#[derive(Debug, Clone, PartialEq, Eq)]
115struct CompactionLogState {
116    running_parallelism: u32,
117    pull_task_ack: bool,
118    pending_pull_task_count: u32,
119}
120
121/// Represents the iceberg compaction task state for logging purposes
122#[derive(Debug, Clone, PartialEq, Eq)]
123struct IcebergCompactionLogState {
124    running_parallelism: u32,
125    waiting_parallelism: u32,
126    available_parallelism: u32,
127    pull_task_ack: bool,
128    pending_pull_task_count: u32,
129}
130
131/// Controls periodic logging with state change detection
132struct LogThrottler<T: PartialEq> {
133    last_logged_state: Option<T>,
134    last_heartbeat: Instant,
135    heartbeat_interval: Duration,
136}
137
138impl<T: PartialEq> LogThrottler<T> {
139    fn new(heartbeat_interval: Duration) -> Self {
140        Self {
141            last_logged_state: None,
142            last_heartbeat: Instant::now(),
143            heartbeat_interval,
144        }
145    }
146
147    /// Returns true if logging should occur (state changed or heartbeat interval elapsed)
148    fn should_log(&self, current_state: &T) -> bool {
149        self.last_logged_state.as_ref() != Some(current_state)
150            || self.last_heartbeat.elapsed() >= self.heartbeat_interval
151    }
152
153    /// Updates the state and heartbeat timestamp after logging
154    fn update(&mut self, current_state: T) {
155        self.last_logged_state = Some(current_state);
156        self.last_heartbeat = Instant::now();
157    }
158}
159
160/// Implementation of Hummock compaction.
161pub struct Compactor {
162    /// The context of the compactor.
163    context: CompactorContext,
164    object_id_getter: Arc<dyn GetObjectId>,
165    task_config: TaskConfig,
166    options: SstableBuilderOptions,
167    get_id_time: Arc<AtomicU64>,
168}
169
170pub type CompactOutput = (usize, Vec<LocalSstableInfo>, CompactionStatistics);
171
172impl Compactor {
173    /// Create a new compactor.
174    pub fn new(
175        context: CompactorContext,
176        options: SstableBuilderOptions,
177        task_config: TaskConfig,
178        object_id_getter: Arc<dyn GetObjectId>,
179    ) -> Self {
180        Self {
181            context,
182            options,
183            task_config,
184            get_id_time: Arc::new(AtomicU64::new(0)),
185            object_id_getter,
186        }
187    }
188
189    /// Compact the given key range and merge iterator.
190    /// Upon a successful return, the built SSTs are already uploaded to object store.
191    ///
192    /// `task_progress` is only used for tasks on the compactor.
193    async fn compact_key_range(
194        &self,
195        iter: impl HummockIterator<Direction = Forward>,
196        compaction_filter: impl CompactionFilter,
197        compaction_catalog_agent_ref: CompactionCatalogAgentRef,
198        task_progress: Option<Arc<TaskProgress>>,
199        task_id: Option<HummockCompactionTaskId>,
200        split_index: Option<usize>,
201    ) -> HummockResult<(Vec<LocalSstableInfo>, CompactionStatistics)> {
202        // Monitor time cost building shared buffer to SSTs.
203        let compact_timer = if self.context.is_share_buffer_compact {
204            self.context
205                .compactor_metrics
206                .write_build_l0_sst_duration
207                .start_timer()
208        } else {
209            self.context
210                .compactor_metrics
211                .compact_sst_duration
212                .start_timer()
213        };
214
215        let (split_table_outputs, table_stats_map) = {
216            let factory = UnifiedSstableWriterFactory::new(self.context.sstable_store.clone());
217            match (
218                self.task_config.sstable_filter_type,
219                self.task_config.sstable_filter_layout,
220            ) {
221                (PbSstableFilterType::SstableFilterNone, _) => {
222                    self.compact_key_range_impl::<_, NoneFilterBuilder>(
223                        factory,
224                        iter,
225                        compaction_filter,
226                        compaction_catalog_agent_ref,
227                        task_progress.clone(),
228                        self.object_id_getter.clone(),
229                    )
230                    .instrument_await("compact".verbose())
231                    .await?
232                }
233                (PbSstableFilterType::SstableFilterXor8, PbSstableFilterLayout::Blocked) => {
234                    self.compact_key_range_impl::<_, BlockedXor8FilterBuilder>(
235                        factory,
236                        iter,
237                        compaction_filter,
238                        compaction_catalog_agent_ref,
239                        task_progress.clone(),
240                        self.object_id_getter.clone(),
241                    )
242                    .instrument_await("compact".verbose())
243                    .await?
244                }
245                (PbSstableFilterType::SstableFilterXor8, PbSstableFilterLayout::Plain) => {
246                    self.compact_key_range_impl::<_, Xor8FilterBuilder>(
247                        factory,
248                        iter,
249                        compaction_filter,
250                        compaction_catalog_agent_ref,
251                        task_progress.clone(),
252                        self.object_id_getter.clone(),
253                    )
254                    .instrument_await("compact".verbose())
255                    .await?
256                }
257                (PbSstableFilterType::SstableFilterXor16, PbSstableFilterLayout::Blocked) => {
258                    self.compact_key_range_impl::<_, BlockedXor16FilterBuilder>(
259                        factory,
260                        iter,
261                        compaction_filter,
262                        compaction_catalog_agent_ref,
263                        task_progress.clone(),
264                        self.object_id_getter.clone(),
265                    )
266                    .instrument_await("compact".verbose())
267                    .await?
268                }
269                (PbSstableFilterType::SstableFilterXor16, PbSstableFilterLayout::Plain) => {
270                    self.compact_key_range_impl::<_, Xor16FilterBuilder>(
271                        factory,
272                        iter,
273                        compaction_filter,
274                        compaction_catalog_agent_ref,
275                        task_progress.clone(),
276                        self.object_id_getter.clone(),
277                    )
278                    .instrument_await("compact".verbose())
279                    .await?
280                }
281                (filter_type, layout) => {
282                    return Err(HummockError::compaction_executor(format!(
283                        "unresolved SST filter layout in task config: {:?}, {:?}",
284                        filter_type, layout
285                    )));
286                }
287            }
288        };
289
290        compact_timer.observe_duration();
291
292        Self::report_progress(
293            self.context.compactor_metrics.clone(),
294            task_progress,
295            &split_table_outputs,
296            self.context.is_share_buffer_compact,
297        );
298
299        self.context
300            .compactor_metrics
301            .get_table_id_total_time_duration
302            .observe(self.get_id_time.load(Ordering::Relaxed) as f64 / 1000.0 / 1000.0);
303
304        debug_assert!(
305            split_table_outputs
306                .iter()
307                .all(|table_info| table_info.sst_info.table_ids.is_sorted())
308        );
309
310        if task_id.is_some() {
311            // skip shared buffer compaction
312            tracing::info!(
313                "Finish Task {:?} split_index {:?} sst count {}",
314                task_id,
315                split_index,
316                split_table_outputs.len()
317            );
318        }
319        Ok((split_table_outputs, table_stats_map))
320    }
321
322    pub fn report_progress(
323        metrics: Arc<CompactorMetrics>,
324        task_progress: Option<Arc<TaskProgress>>,
325        ssts: &Vec<LocalSstableInfo>,
326        is_share_buffer_compact: bool,
327    ) {
328        for sst_info in ssts {
329            let sst_size = sst_info.file_size();
330            if let Some(tracker) = &task_progress {
331                tracker.inc_ssts_uploaded();
332                tracker.dec_num_pending_write_io();
333            }
334            if is_share_buffer_compact {
335                metrics.shared_buffer_to_sstable_size.observe(sst_size as _);
336            } else {
337                metrics.compaction_upload_sst_counts.inc();
338            }
339        }
340    }
341
342    async fn compact_key_range_impl<F: SstableWriterFactory, B: FilterBuilder>(
343        &self,
344        writer_factory: F,
345        iter: impl HummockIterator<Direction = Forward>,
346        compaction_filter: impl CompactionFilter,
347        compaction_catalog_agent_ref: CompactionCatalogAgentRef,
348        task_progress: Option<Arc<TaskProgress>>,
349        object_id_getter: Arc<dyn GetObjectId>,
350    ) -> HummockResult<(Vec<LocalSstableInfo>, CompactionStatistics)> {
351        let builder_factory = RemoteBuilderFactory::<F, B> {
352            object_id_getter,
353            limiter: self.context.memory_limiter.clone(),
354            options: self.options.clone(),
355            policy: self.task_config.cache_policy,
356            remote_rpc_cost: self.get_id_time.clone(),
357            compaction_catalog_agent_ref: compaction_catalog_agent_ref.clone(),
358            sstable_writer_factory: writer_factory,
359            _phantom: PhantomData,
360        };
361
362        let mut sst_builder = CapacitySplitTableBuilder::new(
363            builder_factory,
364            self.context.compactor_metrics.clone(),
365            task_progress.clone(),
366            self.task_config.table_vnode_partition.clone(),
367            self.context
368                .storage_opts
369                .compactor_concurrent_uploading_sst_count,
370            compaction_catalog_agent_ref,
371        );
372        let compaction_statistics = compact_and_build_sst(
373            &mut sst_builder,
374            &self.task_config,
375            self.context.compactor_metrics.clone(),
376            iter,
377            compaction_filter,
378        )
379        .instrument_await("compact_and_build_sst".verbose())
380        .await?;
381
382        let ssts = sst_builder
383            .finish()
384            .instrument_await("builder_finish".verbose())
385            .await?;
386
387        Ok((ssts, compaction_statistics))
388    }
389}
390
391/// The background compaction thread that receives compaction tasks from hummock compaction
392/// manager and runs compaction tasks.
393#[must_use]
394pub fn start_iceberg_compactor(
395    compactor_context: CompactorContext,
396    hummock_meta_client: Arc<dyn HummockMetaClient>,
397) -> (JoinHandle<()>, Sender<()>) {
398    let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
399    let stream_retry_interval = Duration::from_secs(30);
400    let periodic_event_update_interval = Duration::from_millis(
401        compactor_context
402            .storage_opts
403            .iceberg_compaction_pull_interval_ms,
404    );
405    let worker_num = compactor_context.compaction_executor.worker_num();
406
407    let max_task_parallelism: u32 = (worker_num as f32
408        * compactor_context.storage_opts.compactor_max_task_multiplier)
409        .ceil() as u32;
410
411    const MAX_PULL_TASK_COUNT: u32 = 4;
412    let max_pull_task_count = std::cmp::min(max_task_parallelism, MAX_PULL_TASK_COUNT);
413
414    assert_ge!(
415        compactor_context.storage_opts.compactor_max_task_multiplier,
416        0.0
417    );
418
419    let join_handle = tokio::spawn(async move {
420        // Initialize task queue with event-driven scheduling
421        let pending_parallelism_budget = (max_task_parallelism as f32
422            * compactor_context
423                .storage_opts
424                .iceberg_compaction_pending_parallelism_budget_multiplier)
425            .ceil() as u32;
426        let mut task_queue =
427            IcebergTaskQueue::new(max_task_parallelism, pending_parallelism_budget);
428
429        // Shutdown tracking for running tasks (task_key -> shutdown_sender)
430        let shutdown_map = Arc::new(Mutex::new(HashMap::<TaskKey, Sender<()>>::new()));
431
432        // Channel for task completion notifications
433        let (task_completion_tx, mut task_completion_rx) =
434            tokio::sync::mpsc::unbounded_channel::<IcebergPlanCompletion>();
435        let mut task_trackers = HashMap::<u64, IcebergTaskTracker>::new();
436        // Buffers task reports that failed to send on the current stream.
437        // The queue is flushed in FIFO order after the stream reconnects.
438        let mut pending_task_reports = VecDeque::<IcebergTaskReport>::new();
439
440        let mut min_interval = tokio::time::interval(stream_retry_interval);
441        let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
442
443        // Track last logged state to avoid duplicate logs
444        let mut log_throttler =
445            LogThrottler::<IcebergCompactionLogState>::new(COMPACTION_HEARTBEAT_LOG_INTERVAL);
446
447        // This outer loop is to recreate stream.
448        'start_stream: loop {
449            // reset state
450            // pull_task_ack.store(true, Ordering::SeqCst);
451            let mut pull_task_ack = true;
452            tokio::select! {
453                // Wait for interval.
454                _ = min_interval.tick() => {},
455                // Shutdown compactor.
456                _ = &mut shutdown_rx => {
457                    tracing::info!("Compactor is shutting down");
458                    return;
459                }
460            }
461
462            let (request_sender, response_event_stream) = match hummock_meta_client
463                .subscribe_iceberg_compaction_event()
464                .await
465            {
466                Ok((request_sender, response_event_stream)) => {
467                    tracing::debug!("Succeeded subscribe_iceberg_compaction_event.");
468                    (request_sender, response_event_stream)
469                }
470
471                Err(e) => {
472                    tracing::warn!(
473                        error = %e.as_report(),
474                        "Subscribing to iceberg compaction tasks failed with error. Will retry.",
475                    );
476                    continue 'start_stream;
477                }
478            };
479
480            if matches!(
481                flush_pending_iceberg_task_reports(&request_sender, &mut pending_task_reports),
482                ReportSendResult::RestartStream
483            ) {
484                continue 'start_stream;
485            }
486
487            pin_mut!(response_event_stream);
488
489            let _executor = compactor_context.compaction_executor.clone();
490
491            // This inner loop is to consume stream or report task progress.
492            let mut event_loop_iteration_now = Instant::now();
493            'consume_stream: loop {
494                {
495                    // report
496                    compactor_context
497                        .compactor_metrics
498                        .compaction_event_loop_iteration_latency
499                        .observe(event_loop_iteration_now.elapsed().as_millis() as _);
500                    event_loop_iteration_now = Instant::now();
501                }
502
503                let request_sender = request_sender.clone();
504                let event: Option<Result<SubscribeIcebergCompactionEventResponse, _>> = tokio::select! {
505                    // Handle task completion notifications
506                    Some(plan_completion) = task_completion_rx.recv() => {
507                        let task_key = plan_completion.task_key;
508                        let error_message = plan_completion.error_message;
509                        tracing::debug!(
510                            task_id = task_key.0,
511                            plan_index = task_key.1,
512                            success = error_message.is_none(),
513                            "Plan completed, updating queue state"
514                        );
515                        task_queue.finish_running(task_key);
516
517                        let completed_task_id = task_key.0;
518                        let Entry::Occupied(mut tracker_entry) =
519                            task_trackers.entry(completed_task_id)
520                        else {
521                            continue 'consume_stream;
522                        };
523                        tracker_entry.get_mut().record_completion(error_message);
524                        if !tracker_entry.get().is_finished() {
525                            continue 'consume_stream;
526                        }
527
528                        let tracker = tracker_entry.remove();
529                        let sink_id = tracker.sink_id();
530                        let total_plans = tracker.total_plans();
531                        let successful_plans = tracker.successful_plans();
532                        let failed_plans = tracker.failed_plans();
533                        let report = tracker.into_report(completed_task_id);
534                        let task_succeeded = report.error_message.is_none();
535                        if matches!(
536                            send_or_buffer_iceberg_task_report(
537                                &request_sender,
538                                &mut pending_task_reports,
539                                report,
540                            ),
541                            ReportSendResult::RestartStream
542                        ) {
543                            continue 'start_stream;
544                        }
545                        if task_succeeded {
546                            tracing::info!(
547                                iceberg_component = "compaction_worker",
548                                iceberg_operation = "report_task",
549                                task_id = completed_task_id,
550                                sink_id = sink_id,
551                                total_plans = total_plans,
552                                successful_plans = successful_plans,
553                                failed_plans = failed_plans,
554                                "iceberg_compaction_task_succeeded",
555                            );
556                        }
557                        continue 'consume_stream;
558                    }
559
560                    // Event-driven task scheduling - wait for tasks to become schedulable
561                    _ = task_queue.wait_schedulable() => {
562                        schedule_queued_tasks(
563                            &mut task_queue,
564                            &compactor_context,
565                            &shutdown_map,
566                            &task_completion_tx,
567                        );
568                        continue 'consume_stream;
569                    }
570
571                    _ = periodic_event_interval.tick() => {
572                        // Only handle meta task pulling in periodic tick
573                        let should_restart_stream = handle_meta_task_pulling(
574                            &mut pull_task_ack,
575                            &task_queue,
576                            max_task_parallelism,
577                            max_pull_task_count,
578                            &request_sender,
579                            &mut log_throttler,
580                        );
581
582                        if should_restart_stream {
583                            continue 'start_stream;
584                        }
585                        continue;
586                    }
587                    event = response_event_stream.next() => {
588                        event
589                    }
590
591                    _ = &mut shutdown_rx => {
592                        tracing::info!("Iceberg Compactor is shutting down");
593                        return
594                    }
595                };
596
597                match event {
598                    Some(Ok(SubscribeIcebergCompactionEventResponse {
599                        event,
600                        create_at: _create_at,
601                    })) => {
602                        let event = match event {
603                            Some(event) => event,
604                            None => continue 'consume_stream,
605                        };
606
607                        match event {
608                            risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::CompactTask(iceberg_compaction_task) => {
609                                let task_id = iceberg_compaction_task.task_id;
610                                let sink_id = iceberg_compaction_task.sink_id;
611                                // Note: write_parquet_properties is now built from sink config (IcebergConfig) in create_task_execution
612                                let compactor_runner_config = match IcebergCompactorRunnerConfigBuilder::default()
613                                    .max_parallelism((worker_num as f32 * compactor_context.storage_opts.iceberg_compaction_task_parallelism_ratio) as u32)
614                                    .min_size_per_partition(compactor_context.storage_opts.iceberg_compaction_min_size_per_partition_mb as u64 * 1024 * 1024)
615                                    .max_file_count_per_partition(compactor_context.storage_opts.iceberg_compaction_max_file_count_per_partition)
616                                    .enable_validate_compaction(compactor_context.storage_opts.iceberg_compaction_enable_validate)
617                                    .max_record_batch_rows(compactor_context.storage_opts.iceberg_compaction_max_record_batch_rows)
618                                    .enable_heuristic_output_parallelism(compactor_context.storage_opts.iceberg_compaction_enable_heuristic_output_parallelism)
619                                    .max_concurrent_closes(compactor_context.storage_opts.iceberg_compaction_max_concurrent_closes)
620                                    .enable_prefetch(compactor_context.storage_opts.iceberg_compaction_enable_prefetch)
621                                    .target_binpack_group_size_mb(
622                                        compactor_context.storage_opts.iceberg_compaction_target_binpack_group_size_mb
623                                    )
624                                    .min_group_size_mb(
625                                        compactor_context.storage_opts.iceberg_compaction_min_group_size_mb
626                                    )
627                                    .min_group_file_count(
628                                        compactor_context.storage_opts.iceberg_compaction_min_group_file_count
629                                    )
630                                    .build() {
631                                    Ok(config) => config,
632                                    Err(e) => {
633                                        tracing::warn!(
634                                            iceberg_component = "compaction_worker",
635                                            iceberg_operation = "build_runner_config",
636                                            error = %e.as_report(),
637                                            task_id = task_id,
638                                            sink_id = sink_id,
639                                            "iceberg_compaction_runner_config_failed",
640                                        );
641                                        let report = build_iceberg_task_report(
642                                            task_id,
643                                            sink_id,
644                                            Some(format!(
645                                                "Failed to build iceberg compactor runner config: {}",
646                                                e.as_report()
647                                            )),
648                                        );
649                                        if matches!(
650                                            send_or_buffer_iceberg_task_report(
651                                                &request_sender,
652                                                &mut pending_task_reports,
653                                                report,
654                                            ),
655                                            ReportSendResult::RestartStream
656                                        ) {
657                                            continue 'start_stream;
658                                        }
659                                        continue 'consume_stream;
660                                    }
661                                };
662
663                                // Create task execution context and plan runners from the task
664                                let task_execution = match create_task_execution(
665                                    iceberg_compaction_task,
666                                    compactor_runner_config,
667                                    compactor_context.compactor_metrics.clone(),
668                                ).await {
669                                    Ok(task_execution) => task_execution,
670                                    Err(e) => {
671                                        tracing::warn!(
672                                            iceberg_component = "compaction_worker",
673                                            iceberg_operation = "plan_task",
674                                            error = %e.as_report(),
675                                            task_id = task_id,
676                                            sink_id = sink_id,
677                                            "iceberg_compaction_task_plan_failed",
678                                        );
679                                        let report = build_iceberg_task_report(
680                                            task_id,
681                                            sink_id,
682                                            Some(format!(
683                                                "Failed to create iceberg compaction task execution: {}",
684                                                e.as_report()
685                                            )),
686                                        );
687                                        if matches!(
688                                            send_or_buffer_iceberg_task_report(
689                                                &request_sender,
690                                                &mut pending_task_reports,
691                                                report,
692                                            ),
693                                            ReportSendResult::RestartStream
694                                        ) {
695                                            continue 'start_stream;
696                                        }
697                                        continue 'consume_stream;
698                                    }
699                                };
700
701                                let sink_id = task_execution.sink_id;
702                                let plan_runners = task_execution.plan_runners;
703
704                                if plan_runners.is_empty() {
705                                    tracing::info!(
706                                        iceberg_component = "compaction_worker",
707                                        iceberg_operation = "enqueue_plan",
708                                        task_id = task_id,
709                                        sink_id = sink_id,
710                                        "iceberg_compaction_task_skipped_no_plans",
711                                    );
712                                    let report = build_iceberg_task_report(task_id, sink_id, None);
713                                    if matches!(
714                                        send_or_buffer_iceberg_task_report(
715                                            &request_sender,
716                                            &mut pending_task_reports,
717                                            report,
718                                        ),
719                                        ReportSendResult::RestartStream
720                                    ) {
721                                        continue 'start_stream;
722                                    }
723                                    continue 'consume_stream;
724                                }
725
726                                // Enqueue each plan runner independently
727                                let total_plans = plan_runners.len();
728                                let mut enqueued_count = 0;
729
730                                for runner in plan_runners {
731                                    let meta = runner.to_meta();
732                                    let plan_index = meta.plan_index;
733                                    let required_parallelism = runner.required_parallelism();
734                                    let runner_sink_id = runner.sink_id;
735                                    let runner_task_type = runner.task_type;
736                                    let runner_table = runner.table_ident.to_string();
737                                    let push_result = task_queue.push(meta, Some(runner));
738
739                                    match push_result {
740                                        PushResult::Added => {
741                                            enqueued_count += 1;
742                                            tracing::debug!(
743                                                iceberg_component = "compaction_worker",
744                                                iceberg_operation = "enqueue_plan",
745                                                task_id = task_id,
746                                                sink_id = runner_sink_id,
747                                                plan_index = plan_index,
748                                                task_type = ?runner_task_type,
749                                                table = %runner_table,
750                                                required_parallelism = required_parallelism,
751                                                "iceberg_compaction_plan_enqueued",
752                                            );
753                                        },
754                                        PushResult::RejectedCapacity => {
755                                            tracing::warn!(
756                                                iceberg_component = "compaction_worker",
757                                                iceberg_operation = "enqueue_plan",
758                                                task_id = task_id,
759                                                sink_id = runner_sink_id,
760                                                plan_index = plan_index,
761                                                task_type = ?runner_task_type,
762                                                table = %runner_table,
763                                                required_parallelism = required_parallelism,
764                                                pending_budget = pending_parallelism_budget,
765                                                enqueued_count = enqueued_count,
766                                                total_plans = total_plans,
767                                                "iceberg_compaction_plan_rejected_capacity",
768                                            );
769                                            // Stop enqueuing remaining plans
770                                            break;
771                                        },
772                                        PushResult::RejectedTooLarge => {
773                                            tracing::error!(
774                                                iceberg_component = "compaction_worker",
775                                                iceberg_operation = "enqueue_plan",
776                                                task_id = task_id,
777                                                sink_id = runner_sink_id,
778                                                plan_index = plan_index,
779                                                task_type = ?runner_task_type,
780                                                table = %runner_table,
781                                                required_parallelism = required_parallelism,
782                                                max_parallelism = max_task_parallelism,
783                                                "iceberg_compaction_plan_rejected_too_large",
784                                            );
785                                        },
786                                        PushResult::RejectedInvalidParallelism => {
787                                            tracing::error!(
788                                                iceberg_component = "compaction_worker",
789                                                iceberg_operation = "enqueue_plan",
790                                                task_id = task_id,
791                                                sink_id = runner_sink_id,
792                                                plan_index = plan_index,
793                                                task_type = ?runner_task_type,
794                                                table = %runner_table,
795                                                required_parallelism = required_parallelism,
796                                                "iceberg_compaction_plan_rejected_invalid_parallelism",
797                                            );
798                                        },
799                                        PushResult::RejectedDuplicate => {
800                                            tracing::error!(
801                                                iceberg_component = "compaction_worker",
802                                                iceberg_operation = "enqueue_plan",
803                                                task_id = task_id,
804                                                sink_id = runner_sink_id,
805                                                plan_index = plan_index,
806                                                task_type = ?runner_task_type,
807                                                table = %runner_table,
808                                                "iceberg_compaction_plan_rejected_duplicate",
809                                            );
810                                        }
811                                    }
812                                }
813
814                                if enqueued_count == 0 {
815                                    let report = build_iceberg_task_report(
816                                        task_id,
817                                        sink_id,
818                                        Some("Failed to enqueue all iceberg compaction plans".to_owned()),
819                                    );
820                                    if matches!(
821                                        send_or_buffer_iceberg_task_report(
822                                            &request_sender,
823                                            &mut pending_task_reports,
824                                            report,
825                                        ),
826                                        ReportSendResult::RestartStream
827                                    ) {
828                                        continue 'start_stream;
829                                    }
830                                } else {
831                                    task_trackers.insert(
832                                        task_id,
833                                        IcebergTaskTracker::new(sink_id, enqueued_count),
834                                    );
835                                }
836
837                                tracing::info!(
838                                    iceberg_component = "compaction_worker",
839                                    iceberg_operation = "enqueue_plan",
840                                    task_id = task_id,
841                                    sink_id = sink_id,
842                                    total_plans = total_plans,
843                                    enqueued_count = enqueued_count,
844                                    "iceberg_compaction_task_enqueue_finished",
845                                );
846                            },
847                            risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::PullTaskAck(_) => {
848                                // set flag
849                                pull_task_ack = true;
850                            },
851                            risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::CancelCompactTask(cancel_compact_task) => {
852                                cancel_iceberg_task(
853                                    cancel_compact_task.task_id,
854                                    &mut task_queue,
855                                    &shutdown_map,
856                                    &mut task_trackers,
857                                );
858                            },
859                        }
860                    }
861                    Some(Err(e)) => {
862                        tracing::warn!("Failed to consume stream. {}", e.message());
863                        continue 'start_stream;
864                    }
865                    _ => {
866                        // The stream is exhausted
867                        continue 'start_stream;
868                    }
869                }
870            }
871        }
872    });
873
874    (join_handle, shutdown_tx)
875}
876
877/// The background compaction thread that receives compaction tasks from hummock compaction
878/// manager and runs compaction tasks.
879#[must_use]
880pub fn start_compactor(
881    compactor_context: CompactorContext,
882    hummock_meta_client: Arc<dyn HummockMetaClient>,
883    object_id_manager: Arc<ObjectIdManager>,
884    compaction_catalog_manager_ref: CompactionCatalogManagerRef,
885) -> (JoinHandle<()>, Sender<()>) {
886    type CompactionShutdownMap = Arc<Mutex<HashMap<u64, Sender<()>>>>;
887    let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
888    let stream_retry_interval = Duration::from_secs(30);
889    let task_progress = compactor_context.task_progress_manager.clone();
890    let periodic_event_update_interval = Duration::from_millis(1000);
891
892    let max_task_parallelism: u32 = (compactor_context.compaction_executor.worker_num() as f32
893        * compactor_context.storage_opts.compactor_max_task_multiplier)
894        .ceil() as u32;
895    let running_task_parallelism = Arc::new(AtomicU32::new(0));
896
897    const MAX_PULL_TASK_COUNT: u32 = 4;
898    let max_pull_task_count = std::cmp::min(max_task_parallelism, MAX_PULL_TASK_COUNT);
899
900    assert_ge!(
901        compactor_context.storage_opts.compactor_max_task_multiplier,
902        0.0
903    );
904
905    let join_handle = tokio::spawn(async move {
906        let shutdown_map = CompactionShutdownMap::default();
907        let mut min_interval = tokio::time::interval(stream_retry_interval);
908        let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
909
910        // Track last logged state to avoid duplicate logs
911        let mut log_throttler =
912            LogThrottler::<CompactionLogState>::new(COMPACTION_HEARTBEAT_LOG_INTERVAL);
913
914        // This outer loop is to recreate stream.
915        'start_stream: loop {
916            // reset state
917            // pull_task_ack.store(true, Ordering::SeqCst);
918            let mut pull_task_ack = true;
919            tokio::select! {
920                // Wait for interval.
921                _ = min_interval.tick() => {},
922                // Shutdown compactor.
923                _ = &mut shutdown_rx => {
924                    tracing::info!("Compactor is shutting down");
925                    return;
926                }
927            }
928
929            let (request_sender, response_event_stream) =
930                match hummock_meta_client.subscribe_compaction_event().await {
931                    Ok((request_sender, response_event_stream)) => {
932                        tracing::debug!("Succeeded subscribe_compaction_event.");
933                        (request_sender, response_event_stream)
934                    }
935
936                    Err(e) => {
937                        tracing::warn!(
938                            error = %e.as_report(),
939                            "Subscribing to compaction tasks failed with error. Will retry.",
940                        );
941                        continue 'start_stream;
942                    }
943                };
944
945            pin_mut!(response_event_stream);
946
947            let executor = compactor_context.compaction_executor.clone();
948            let object_id_manager = object_id_manager.clone();
949
950            // This inner loop is to consume stream or report task progress.
951            let mut event_loop_iteration_now = Instant::now();
952            'consume_stream: loop {
953                {
954                    // report
955                    compactor_context
956                        .compactor_metrics
957                        .compaction_event_loop_iteration_latency
958                        .observe(event_loop_iteration_now.elapsed().as_millis() as _);
959                    event_loop_iteration_now = Instant::now();
960                }
961
962                let running_task_parallelism = running_task_parallelism.clone();
963                let request_sender = request_sender.clone();
964                let event: Option<Result<SubscribeCompactionEventResponse, _>> = tokio::select! {
965                    _ = periodic_event_interval.tick() => {
966                        let progress_list = get_task_progress(task_progress.clone());
967
968                        if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
969                            event: Some(RequestEvent::HeartBeat(
970                                HeartBeat {
971                                    progress: progress_list
972                                }
973                            )),
974                            create_at: SystemTime::now()
975                                .duration_since(std::time::UNIX_EPOCH)
976                                .expect("Clock may have gone backwards")
977                                .as_millis() as u64,
978                        }) {
979                            tracing::warn!(error = %e.as_report(), "Failed to report task progress");
980                            // re subscribe stream
981                            continue 'start_stream;
982                        }
983
984
985                        let mut pending_pull_task_count = 0;
986                        if pull_task_ack {
987                            // TODO: Compute parallelism on meta side
988                            pending_pull_task_count = (max_task_parallelism - running_task_parallelism.load(Ordering::SeqCst)).min(max_pull_task_count);
989
990                            if pending_pull_task_count > 0 {
991                                if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
992                                    event: Some(RequestEvent::PullTask(
993                                        PullTask {
994                                            pull_task_count: pending_pull_task_count,
995                                        }
996                                    )),
997                                    create_at: SystemTime::now()
998                                        .duration_since(std::time::UNIX_EPOCH)
999                                        .expect("Clock may have gone backwards")
1000                                        .as_millis() as u64,
1001                                }) {
1002                                    tracing::warn!(error = %e.as_report(), "Failed to pull task");
1003
1004                                    // re subscribe stream
1005                                    continue 'start_stream;
1006                                } else {
1007                                    pull_task_ack = false;
1008                                }
1009                            }
1010                        }
1011
1012                        let running_count = running_task_parallelism.load(Ordering::SeqCst);
1013                        let current_state = CompactionLogState {
1014                            running_parallelism: running_count,
1015                            pull_task_ack,
1016                            pending_pull_task_count,
1017                        };
1018
1019                        // Log only when state changes or periodically as heartbeat
1020                        if log_throttler.should_log(&current_state) {
1021                            tracing::info!(
1022                                running_parallelism_count = %current_state.running_parallelism,
1023                                pull_task_ack = %current_state.pull_task_ack,
1024                                pending_pull_task_count = %current_state.pending_pull_task_count
1025                            );
1026                            log_throttler.update(current_state);
1027                        }
1028
1029                        continue;
1030                    }
1031                    event = response_event_stream.next() => {
1032                        event
1033                    }
1034
1035                    _ = &mut shutdown_rx => {
1036                        tracing::info!("Compactor is shutting down");
1037                        return
1038                    }
1039                };
1040
1041                fn send_report_task_event(
1042                    compact_task: &CompactTask,
1043                    table_stats: TableStatsMap,
1044                    object_timestamps: HashMap<HummockSstableObjectId, u64>,
1045                    request_sender: &mpsc::UnboundedSender<SubscribeCompactionEventRequest>,
1046                ) {
1047                    if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
1048                        event: Some(RequestEvent::ReportTask(ReportTask {
1049                            task_id: compact_task.task_id,
1050                            task_status: compact_task.task_status.into(),
1051                            sorted_output_ssts: compact_task
1052                                .sorted_output_ssts
1053                                .iter()
1054                                .map(|sst| sst.into())
1055                                .collect(),
1056                            table_stats_change: to_prost_table_stats_map(table_stats),
1057                            object_timestamps,
1058                        })),
1059                        create_at: SystemTime::now()
1060                            .duration_since(std::time::UNIX_EPOCH)
1061                            .expect("Clock may have gone backwards")
1062                            .as_millis() as u64,
1063                    }) {
1064                        let task_id = compact_task.task_id;
1065                        tracing::warn!(error = %e.as_report(), "Failed to report task {task_id:?}");
1066                    }
1067                }
1068
1069                match event {
1070                    Some(Ok(SubscribeCompactionEventResponse { event, create_at })) => {
1071                        let event = match event {
1072                            Some(event) => event,
1073                            None => continue 'consume_stream,
1074                        };
1075                        let shutdown = shutdown_map.clone();
1076                        let context = compactor_context.clone();
1077                        let consumed_latency_ms = SystemTime::now()
1078                            .duration_since(std::time::UNIX_EPOCH)
1079                            .expect("Clock may have gone backwards")
1080                            .as_millis() as u64
1081                            - create_at;
1082                        context
1083                            .compactor_metrics
1084                            .compaction_event_consumed_latency
1085                            .observe(consumed_latency_ms as _);
1086
1087                        let object_id_manager = object_id_manager.clone();
1088                        let compaction_catalog_manager_ref = compaction_catalog_manager_ref.clone();
1089
1090                        match event {
1091                            ResponseEvent::CompactTask(compact_task) => {
1092                                let compact_task = CompactTask::from(compact_task);
1093                                let parallelism =
1094                                    calculate_task_parallelism(&compact_task, &context);
1095
1096                                assert_ne!(parallelism, 0, "splits cannot be empty");
1097
1098                                if (max_task_parallelism
1099                                    - running_task_parallelism.load(Ordering::SeqCst))
1100                                    < parallelism as u32
1101                                {
1102                                    tracing::warn!(
1103                                        "Not enough core parallelism to serve the task {} task_parallelism {} running_task_parallelism {} max_task_parallelism {}",
1104                                        compact_task.task_id,
1105                                        parallelism,
1106                                        max_task_parallelism,
1107                                        running_task_parallelism.load(Ordering::Relaxed),
1108                                    );
1109                                    let (compact_task, table_stats, object_timestamps) =
1110                                        compact_done(
1111                                            compact_task,
1112                                            context.clone(),
1113                                            vec![],
1114                                            TaskStatus::NoAvailCpuResourceCanceled,
1115                                        );
1116
1117                                    send_report_task_event(
1118                                        &compact_task,
1119                                        table_stats,
1120                                        object_timestamps,
1121                                        &request_sender,
1122                                    );
1123
1124                                    continue 'consume_stream;
1125                                }
1126
1127                                running_task_parallelism
1128                                    .fetch_add(parallelism as u32, Ordering::SeqCst);
1129                                executor.spawn(async move {
1130                                    let (tx, rx) = tokio::sync::oneshot::channel();
1131                                    let task_id = compact_task.task_id;
1132                                    shutdown.lock().unwrap().insert(task_id, tx);
1133
1134                                    let (
1135                                        (compact_task, table_stats, object_timestamps),
1136                                        _memory_tracker,
1137                                    ) = compactor_runner::compact(
1138                                        context.clone(),
1139                                        compact_task,
1140                                        rx,
1141                                        object_id_manager.clone(),
1142                                        compaction_catalog_manager_ref.clone(),
1143                                    )
1144                                    .await;
1145
1146                                    shutdown.lock().unwrap().remove(&task_id);
1147                                    running_task_parallelism
1148                                        .fetch_sub(parallelism as u32, Ordering::SeqCst);
1149
1150                                    send_report_task_event(
1151                                        &compact_task,
1152                                        table_stats,
1153                                        object_timestamps,
1154                                        &request_sender,
1155                                    );
1156
1157                                    let enable_check_compaction_result =
1158                                        context.storage_opts.check_compaction_result;
1159                                    let need_check_task =
1160                                        !compact_task.sorted_output_ssts.is_empty()
1161                                            && compact_task.task_status == TaskStatus::Success;
1162
1163                                    if enable_check_compaction_result && need_check_task {
1164                                        let read_table_ids = compact_task
1165                                            .get_table_ids_from_input_ssts()
1166                                            .collect::<Vec<_>>();
1167                                        match compaction_catalog_manager_ref.acquire(read_table_ids).await {
1168                                            Ok(compaction_catalog_agent_ref) =>  {
1169                                                match check_compaction_result(&compact_task, context.clone(), compaction_catalog_agent_ref).await
1170                                                {
1171                                                    Err(e) => {
1172                                                        tracing::warn!(error = %e.as_report(), "Failed to check compaction task {}",compact_task.task_id);
1173                                                    }
1174                                                    Ok(true) => (),
1175                                                    Ok(false) => {
1176                                                        panic!("Failed to pass consistency check for result of compaction task:\n{:?}", compact_task_to_string(&compact_task));
1177                                                    }
1178                                                }
1179                                            },
1180                                            Err(e) => {
1181                                                tracing::warn!(error = %e.as_report(), "failed to acquire compaction catalog agent");
1182                                            }
1183                                        }
1184                                    }
1185                                });
1186                            }
1187                            #[expect(deprecated)]
1188                            ResponseEvent::VacuumTask(_) => {
1189                                unreachable!("unexpected vacuum task");
1190                            }
1191                            #[expect(deprecated)]
1192                            ResponseEvent::FullScanTask(_) => {
1193                                unreachable!("unexpected scan task");
1194                            }
1195                            #[expect(deprecated)]
1196                            ResponseEvent::ValidationTask(validation_task) => {
1197                                let validation_task = ValidationTask::from(validation_task);
1198                                executor.spawn(async move {
1199                                    validate_ssts(validation_task, context.sstable_store.clone())
1200                                        .await;
1201                                });
1202                            }
1203                            ResponseEvent::CancelCompactTask(cancel_compact_task) => match shutdown
1204                                .lock()
1205                                .unwrap()
1206                                .remove(&cancel_compact_task.task_id)
1207                            {
1208                                Some(tx) => {
1209                                    if tx.send(()).is_err() {
1210                                        tracing::warn!(
1211                                            "Cancellation of compaction task failed. task_id: {}",
1212                                            cancel_compact_task.task_id
1213                                        );
1214                                    }
1215                                }
1216                                _ => {
1217                                    tracing::warn!(
1218                                        "Attempting to cancel non-existent compaction task. task_id: {}",
1219                                        cancel_compact_task.task_id
1220                                    );
1221                                }
1222                            },
1223
1224                            ResponseEvent::PullTaskAck(_pull_task_ack) => {
1225                                // set flag
1226                                pull_task_ack = true;
1227                            }
1228                        }
1229                    }
1230                    Some(Err(e)) => {
1231                        tracing::warn!("Failed to consume stream. {}", e.message());
1232                        continue 'start_stream;
1233                    }
1234                    _ => {
1235                        // The stream is exhausted
1236                        continue 'start_stream;
1237                    }
1238                }
1239            }
1240        }
1241    });
1242
1243    (join_handle, shutdown_tx)
1244}
1245
1246/// The background compaction thread that receives compaction tasks from hummock compaction
1247/// manager and runs compaction tasks.
1248#[must_use]
1249pub fn start_shared_compactor(
1250    grpc_proxy_client: GrpcCompactorProxyClient,
1251    mut receiver: mpsc::UnboundedReceiver<Request<DispatchCompactionTaskRequest>>,
1252    context: CompactorContext,
1253) -> (JoinHandle<()>, Sender<()>) {
1254    type CompactionShutdownMap = Arc<Mutex<HashMap<u64, Sender<()>>>>;
1255    let task_progress = context.task_progress_manager.clone();
1256    let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
1257    let periodic_event_update_interval = Duration::from_millis(1000);
1258
1259    let join_handle = tokio::spawn(async move {
1260        let shutdown_map = CompactionShutdownMap::default();
1261
1262        let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
1263        let executor = context.compaction_executor.clone();
1264        let report_heartbeat_client = grpc_proxy_client.clone();
1265        'consume_stream: loop {
1266            let request: Option<Request<DispatchCompactionTaskRequest>> = tokio::select! {
1267                _ = periodic_event_interval.tick() => {
1268                    let progress_list = get_task_progress(task_progress.clone());
1269                    let report_compaction_task_request = ReportCompactionTaskRequest{
1270                        event: Some(ReportCompactionTaskEvent::HeartBeat(
1271                            SharedHeartBeat {
1272                                progress: progress_list
1273                            }
1274                        )),
1275                     };
1276                    if let Err(e) = report_heartbeat_client.report_compaction_task(report_compaction_task_request).await{
1277                        tracing::warn!(error = %e.as_report(), "Failed to report heartbeat");
1278                    }
1279                    continue
1280                }
1281
1282
1283                _ = &mut shutdown_rx => {
1284                    tracing::info!("Compactor is shutting down");
1285                    return
1286                }
1287
1288                request = receiver.recv() => {
1289                    request
1290                }
1291
1292            };
1293            match request {
1294                Some(request) => {
1295                    let context = context.clone();
1296                    let shutdown = shutdown_map.clone();
1297
1298                    let cloned_grpc_proxy_client = grpc_proxy_client.clone();
1299                    executor.spawn(async move {
1300                        let DispatchCompactionTaskRequest {
1301                            tables,
1302                            output_object_ids,
1303                            task: dispatch_task,
1304                        } = request.into_inner();
1305                        let table_id_to_catalog = tables.into_iter().fold(HashMap::new(), |mut acc, table| {
1306                            acc.insert(table.id, table);
1307                            acc
1308                        });
1309
1310                        let mut output_object_ids_deque: VecDeque<_> = VecDeque::new();
1311                        output_object_ids_deque.extend(output_object_ids.into_iter().map(Into::<HummockSstableObjectId>::into));
1312                        let shared_compactor_object_id_manager =
1313                            SharedComapctorObjectIdManager::new(output_object_ids_deque, cloned_grpc_proxy_client.clone(), context.storage_opts.sstable_id_remote_fetch_number);
1314                            match dispatch_task.unwrap() {
1315                                dispatch_compaction_task_request::Task::CompactTask(compact_task) => {
1316                                    let compact_task = CompactTask::from(&compact_task);
1317                                    let (tx, rx) = tokio::sync::oneshot::channel();
1318                                    let task_id = compact_task.task_id;
1319                                    shutdown.lock().unwrap().insert(task_id, tx);
1320
1321                                    let compaction_catalog_manager_ref =
1322                                        Arc::new(CompactionCatalogManager::new_preloaded(
1323                                            table_id_to_catalog,
1324                                        ));
1325                                    let ((compact_task, table_stats, object_timestamps), _memory_tracker)= compactor_runner::compact(
1326                                        context.clone(),
1327                                        compact_task,
1328                                        rx,
1329                                        shared_compactor_object_id_manager,
1330                                        compaction_catalog_manager_ref.clone(),
1331                                    )
1332                                    .await;
1333                                    shutdown.lock().unwrap().remove(&task_id);
1334                                    let report_compaction_task_request = ReportCompactionTaskRequest {
1335                                        event: Some(ReportCompactionTaskEvent::ReportTask(ReportSharedTask {
1336                                            compact_task: Some(PbCompactTask::from(&compact_task)),
1337                                            table_stats_change: to_prost_table_stats_map(table_stats),
1338                                            object_timestamps,
1339                                    })),
1340                                    };
1341
1342                                    match cloned_grpc_proxy_client
1343                                        .report_compaction_task(report_compaction_task_request)
1344                                        .await
1345                                    {
1346                                        Ok(_) => {
1347                                            // TODO: remove this method after we have running risingwave cluster with fast compact algorithm stably for a long time.
1348                                            let enable_check_compaction_result =
1349                                                context.storage_opts.check_compaction_result;
1350                                            let need_check_task =
1351                                                !compact_task.sorted_output_ssts.is_empty()
1352                                                    && compact_task.task_status
1353                                                        == TaskStatus::Success;
1354                                            if enable_check_compaction_result && need_check_task {
1355                                                let read_table_ids = compact_task
1356                                                    .get_table_ids_from_input_ssts()
1357                                                    .collect::<Vec<_>>();
1358                                                match compaction_catalog_manager_ref.acquire(read_table_ids).await {
1359                                                    Ok(compaction_catalog_agent_ref) => {
1360                                                        match check_compaction_result(&compact_task, context.clone(), compaction_catalog_agent_ref).await
1361                                                        {
1362                                                            Err(e) => {
1363                                                                tracing::warn!(error = %e.as_report(), "Failed to check compaction task {}", task_id);
1364                                                            }
1365                                                            Ok(true) => (),
1366                                                            Ok(false) => {
1367                                                                panic!("Failed to pass consistency check for result of compaction task:\n{:?}", compact_task_to_string(&compact_task));
1368                                                            }
1369                                                        }
1370                                                    }
1371                                                    Err(e) => {
1372                                                        tracing::warn!(error = %e.as_report(), "failed to acquire compaction catalog agent");
1373                                                    }
1374                                                }
1375                                            }
1376                                        }
1377                                        Err(e) => tracing::warn!(error = %e.as_report(), "Failed to report task {task_id:?}"),
1378                                    }
1379
1380                                }
1381                                dispatch_compaction_task_request::Task::VacuumTask(_) => {
1382                                    unreachable!("unexpected vacuum task");
1383                                }
1384                                dispatch_compaction_task_request::Task::FullScanTask(_) => {
1385                                    unreachable!("unexpected scan task");
1386                                }
1387                                dispatch_compaction_task_request::Task::ValidationTask(validation_task) => {
1388                                    let validation_task = ValidationTask::from(validation_task);
1389                                    validate_ssts(validation_task, context.sstable_store.clone()).await;
1390                                }
1391                                dispatch_compaction_task_request::Task::CancelCompactTask(cancel_compact_task) => {
1392                                    match shutdown
1393                                        .lock()
1394                                        .unwrap()
1395                                        .remove(&cancel_compact_task.task_id)
1396                                    { Some(tx) => {
1397                                        if tx.send(()).is_err() {
1398                                            tracing::warn!(
1399                                                "Cancellation of compaction task failed. task_id: {}",
1400                                                cancel_compact_task.task_id
1401                                            );
1402                                        }
1403                                    } _ => {
1404                                        tracing::warn!(
1405                                            "Attempting to cancel non-existent compaction task. task_id: {}",
1406                                            cancel_compact_task.task_id
1407                                        );
1408                                    }}
1409                                }
1410                            }
1411                    });
1412                }
1413                None => continue 'consume_stream,
1414            }
1415        }
1416    });
1417    (join_handle, shutdown_tx)
1418}
1419
1420fn get_task_progress(
1421    task_progress: Arc<
1422        parking_lot::lock_api::Mutex<parking_lot::RawMutex, HashMap<u64, Arc<TaskProgress>>>,
1423    >,
1424) -> Vec<CompactTaskProgress> {
1425    let mut progress_list = Vec::new();
1426    for (&task_id, progress) in &*task_progress.lock() {
1427        progress_list.push(progress.snapshot(task_id));
1428    }
1429    progress_list
1430}
1431
1432/// Schedule queued tasks if we have capacity
1433fn schedule_queued_tasks(
1434    task_queue: &mut IcebergTaskQueue,
1435    compactor_context: &CompactorContext,
1436    shutdown_map: &Arc<Mutex<HashMap<TaskKey, Sender<()>>>>,
1437    task_completion_tx: &tokio::sync::mpsc::UnboundedSender<IcebergPlanCompletion>,
1438) {
1439    while let Some(popped_task) = task_queue.pop() {
1440        let task_id = popped_task.meta.task_id;
1441        let plan_index = popped_task.meta.plan_index;
1442        let task_key = (task_id, plan_index);
1443
1444        // Get unique_ident before moving runner
1445        let unique_ident = popped_task.runner.as_ref().map(|r| r.unique_ident());
1446
1447        let Some(runner) = popped_task.runner else {
1448            tracing::error!(
1449                iceberg_component = "compaction_worker",
1450                iceberg_operation = "schedule_plan",
1451                task_id = task_id,
1452                plan_index = plan_index,
1453                "iceberg_compaction_plan_missing_runner",
1454            );
1455            task_queue.finish_running(task_key);
1456            continue;
1457        };
1458        let runner_sink_id = runner.sink_id;
1459        let runner_task_type = runner.task_type;
1460        let runner_table = runner.table_ident.to_string();
1461
1462        let executor = compactor_context.compaction_executor.clone();
1463        let shutdown_map_clone = shutdown_map.clone();
1464        let completion_tx_clone = task_completion_tx.clone();
1465        let (tx, rx) = tokio::sync::oneshot::channel();
1466
1467        {
1468            let mut shutdown_guard = shutdown_map.lock().unwrap();
1469            shutdown_guard.insert(task_key, tx);
1470        }
1471
1472        tracing::info!(
1473            iceberg_component = "compaction_worker",
1474            iceberg_operation = "schedule_plan",
1475            task_id = task_id,
1476            sink_id = runner_sink_id,
1477            plan_index = plan_index,
1478            task_type = ?runner_task_type,
1479            table = %runner_table,
1480            unique_ident = ?unique_ident,
1481            required_parallelism = popped_task.meta.required_parallelism,
1482            "iceberg_compaction_plan_started_from_queue",
1483        );
1484
1485        executor.spawn(async move {
1486            let _cleanup_guard = scopeguard::guard(shutdown_map_clone, move |shutdown_map| {
1487                let mut shutdown_guard = shutdown_map.lock().unwrap();
1488                shutdown_guard.remove(&task_key);
1489            });
1490
1491            let result = Box::pin(runner.compact(rx)).await;
1492
1493            let completion = match result {
1494                Ok(_) => IcebergPlanCompletion {
1495                    task_key,
1496                    error_message: None,
1497                },
1498                Err(e) => {
1499                    if is_cancelled_iceberg_compaction_error(&e) {
1500                        tracing::info!(
1501                            iceberg_component = "compaction_worker",
1502                            iceberg_operation = "execute_plan",
1503                            task_id = task_key.0,
1504                            sink_id = runner_sink_id,
1505                            plan_index = task_key.1,
1506                            task_type = ?runner_task_type,
1507                            table = %runner_table,
1508                            "iceberg_compaction_plan_cancelled",
1509                        );
1510                    } else {
1511                        tracing::warn!(
1512                            iceberg_component = "compaction_worker",
1513                            iceberg_operation = "execute_plan",
1514                            error = %e.as_report(),
1515                            task_id = task_key.0,
1516                            sink_id = runner_sink_id,
1517                            plan_index = task_key.1,
1518                            task_type = ?runner_task_type,
1519                            table = %runner_table,
1520                            "iceberg_compaction_plan_failed",
1521                        );
1522                    }
1523                    IcebergPlanCompletion {
1524                        task_key,
1525                        error_message: Some(e.to_report_string()),
1526                    }
1527                }
1528            };
1529
1530            if completion_tx_clone.send(completion).is_err() {
1531                tracing::warn!(
1532                    iceberg_component = "compaction_worker",
1533                    iceberg_operation = "notify_plan_completion",
1534                    task_id = task_key.0,
1535                    sink_id = runner_sink_id,
1536                    plan_index = task_key.1,
1537                    task_type = ?runner_task_type,
1538                    table = %runner_table,
1539                    "iceberg_compaction_plan_completion_send_failed",
1540                );
1541            }
1542        });
1543    }
1544}
1545
1546fn is_cancelled_iceberg_compaction_error(error: &crate::hummock::HummockError) -> bool {
1547    matches!(
1548        error.inner(),
1549        HummockErrorInner::CompactionExecutor(message) if message == "Plan cancelled"
1550    )
1551}
1552
1553fn cancel_iceberg_task(
1554    task_id: u64,
1555    task_queue: &mut IcebergTaskQueue,
1556    shutdown_map: &Arc<Mutex<HashMap<TaskKey, Sender<()>>>>,
1557    task_trackers: &mut HashMap<u64, IcebergTaskTracker>,
1558) {
1559    // Meta assigns one task id to an Iceberg compact task, but the compactor
1560    // splits it into multiple plan runners tracked by `(task_id, plan_index)`.
1561    // A cancel event only carries `task_id`, so it must cancel all waiting and
1562    // running plan runners that belong to that task.
1563    let cancelled_waiting = task_queue.cancel_waiting_task(task_id);
1564    let removed_tracker = task_trackers.remove(&task_id).is_some();
1565
1566    let cancelled_running = {
1567        let mut shutdown_guard = shutdown_map.lock().unwrap();
1568        let task_keys: Vec<_> = shutdown_guard
1569            .keys()
1570            .filter(|(running_task_id, _)| *running_task_id == task_id)
1571            .copied()
1572            .collect();
1573
1574        for task_key in &task_keys {
1575            if let Some(tx) = shutdown_guard.remove(task_key)
1576                && tx.send(()).is_err()
1577            {
1578                tracing::debug!(
1579                    task_id = task_key.0,
1580                    plan_index = task_key.1,
1581                    "Iceberg compaction plan shutdown receiver already closed during cancellation"
1582                );
1583            }
1584        }
1585
1586        task_keys.len()
1587    };
1588
1589    if cancelled_waiting == 0 && cancelled_running == 0 && !removed_tracker {
1590        tracing::warn!(
1591            task_id = task_id,
1592            "Attempting to cancel non-existent iceberg compaction task"
1593        );
1594    } else {
1595        tracing::info!(
1596            task_id = task_id,
1597            cancelled_waiting = cancelled_waiting,
1598            cancelled_running = cancelled_running,
1599            removed_tracker = removed_tracker,
1600            "Cancelled iceberg compaction task"
1601        );
1602    }
1603}
1604
1605/// Handle pulling new tasks from meta service
1606/// Returns true if the stream should be restarted
1607fn handle_meta_task_pulling(
1608    pull_task_ack: &mut bool,
1609    task_queue: &IcebergTaskQueue,
1610    max_task_parallelism: u32,
1611    max_pull_task_count: u32,
1612    request_sender: &mpsc::UnboundedSender<SubscribeIcebergCompactionEventRequest>,
1613    log_throttler: &mut LogThrottler<IcebergCompactionLogState>,
1614) -> bool {
1615    let mut pending_pull_task_count = 0;
1616    if *pull_task_ack {
1617        // Use queue's running parallelism for pull decision
1618        let current_running_parallelism = task_queue.running_parallelism_sum();
1619        pending_pull_task_count =
1620            (max_task_parallelism - current_running_parallelism).min(max_pull_task_count);
1621
1622        if pending_pull_task_count > 0 {
1623            if let Err(e) = request_sender.send(SubscribeIcebergCompactionEventRequest {
1624                event: Some(subscribe_iceberg_compaction_event_request::Event::PullTask(
1625                    subscribe_iceberg_compaction_event_request::PullTask {
1626                        pull_task_count: pending_pull_task_count,
1627                    },
1628                )),
1629                create_at: SystemTime::now()
1630                    .duration_since(std::time::UNIX_EPOCH)
1631                    .expect("Clock may have gone backwards")
1632                    .as_millis() as u64,
1633            }) {
1634                tracing::warn!(error = %e.as_report(), "Failed to pull task - will retry on stream restart");
1635                return true; // Signal to restart stream
1636            } else {
1637                *pull_task_ack = false;
1638            }
1639        }
1640    }
1641
1642    let running_count = task_queue.running_parallelism_sum();
1643    let waiting_count = task_queue.waiting_parallelism_sum();
1644    let available_count = max_task_parallelism.saturating_sub(running_count);
1645    let current_state = IcebergCompactionLogState {
1646        running_parallelism: running_count,
1647        waiting_parallelism: waiting_count,
1648        available_parallelism: available_count,
1649        pull_task_ack: *pull_task_ack,
1650        pending_pull_task_count,
1651    };
1652
1653    // Log only when state changes or periodically as heartbeat
1654    if log_throttler.should_log(&current_state) {
1655        tracing::info!(
1656            running_parallelism_count = %current_state.running_parallelism,
1657            waiting_parallelism_count = %current_state.waiting_parallelism,
1658            available_parallelism = %current_state.available_parallelism,
1659            pull_task_ack = %current_state.pull_task_ack,
1660            pending_pull_task_count = %current_state.pending_pull_task_count
1661        );
1662        log_throttler.update(current_state);
1663    }
1664
1665    false // No need to restart stream
1666}
1667
1668#[cfg(test)]
1669mod tests {
1670    use super::*;
1671
1672    #[test]
1673    fn test_cancel_iceberg_task_removes_waiting_plans_and_tracker() {
1674        let task_id = 42;
1675        let mut task_queue = IcebergTaskQueue::new(10, 30);
1676        assert_eq!(
1677            task_queue.push(
1678                iceberg_compaction::IcebergTaskMeta {
1679                    task_id,
1680                    plan_index: 0,
1681                    required_parallelism: 3,
1682                },
1683                None,
1684            ),
1685            PushResult::Added
1686        );
1687        assert_eq!(
1688            task_queue.push(
1689                iceberg_compaction::IcebergTaskMeta {
1690                    task_id,
1691                    plan_index: 1,
1692                    required_parallelism: 4,
1693                },
1694                None,
1695            ),
1696            PushResult::Added
1697        );
1698        assert_eq!(task_queue.waiting_parallelism_sum(), 7);
1699
1700        let shutdown_map = Arc::new(Mutex::new(HashMap::new()));
1701        let mut task_trackers = HashMap::from([(task_id, IcebergTaskTracker::new(10, 2))]);
1702
1703        cancel_iceberg_task(task_id, &mut task_queue, &shutdown_map, &mut task_trackers);
1704
1705        assert_eq!(task_queue.waiting_parallelism_sum(), 0);
1706        assert!(!task_trackers.contains_key(&task_id));
1707    }
1708
1709    #[test]
1710    fn test_cancel_iceberg_task_shuts_down_running_plans_and_tracker() {
1711        let task_id = 43;
1712        let task_key = (task_id, 0);
1713        let mut task_queue = IcebergTaskQueue::new(10, 30);
1714        let shutdown_map = Arc::new(Mutex::new(HashMap::new()));
1715        let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
1716        shutdown_map.lock().unwrap().insert(task_key, shutdown_tx);
1717        let mut task_trackers = HashMap::from([(task_id, IcebergTaskTracker::new(10, 1))]);
1718
1719        cancel_iceberg_task(task_id, &mut task_queue, &shutdown_map, &mut task_trackers);
1720
1721        assert!(shutdown_rx.try_recv().is_ok());
1722        assert!(shutdown_map.lock().unwrap().is_empty());
1723        assert!(!task_trackers.contains_key(&task_id));
1724    }
1725
1726    #[test]
1727    fn test_log_state_equality() {
1728        // Test CompactionLogState
1729        let state1 = CompactionLogState {
1730            running_parallelism: 10,
1731            pull_task_ack: true,
1732            pending_pull_task_count: 2,
1733        };
1734        let state2 = CompactionLogState {
1735            running_parallelism: 10,
1736            pull_task_ack: true,
1737            pending_pull_task_count: 2,
1738        };
1739        let state3 = CompactionLogState {
1740            running_parallelism: 11,
1741            pull_task_ack: true,
1742            pending_pull_task_count: 2,
1743        };
1744        assert_eq!(state1, state2);
1745        assert_ne!(state1, state3);
1746
1747        // Test IcebergCompactionLogState
1748        let ice_state1 = IcebergCompactionLogState {
1749            running_parallelism: 10,
1750            waiting_parallelism: 5,
1751            available_parallelism: 15,
1752            pull_task_ack: true,
1753            pending_pull_task_count: 2,
1754        };
1755        let ice_state2 = IcebergCompactionLogState {
1756            running_parallelism: 10,
1757            waiting_parallelism: 6,
1758            available_parallelism: 15,
1759            pull_task_ack: true,
1760            pending_pull_task_count: 2,
1761        };
1762        assert_ne!(ice_state1, ice_state2);
1763    }
1764
1765    #[test]
1766    fn test_log_throttler_state_change_detection() {
1767        let mut throttler = LogThrottler::<CompactionLogState>::new(Duration::from_secs(60));
1768        let state1 = CompactionLogState {
1769            running_parallelism: 10,
1770            pull_task_ack: true,
1771            pending_pull_task_count: 2,
1772        };
1773        let state2 = CompactionLogState {
1774            running_parallelism: 11,
1775            pull_task_ack: true,
1776            pending_pull_task_count: 2,
1777        };
1778
1779        // First call should always log
1780        assert!(throttler.should_log(&state1));
1781        throttler.update(state1.clone());
1782
1783        // Same state should not log
1784        assert!(!throttler.should_log(&state1));
1785
1786        // Changed state should log
1787        assert!(throttler.should_log(&state2));
1788        throttler.update(state2.clone());
1789
1790        // Same state again should not log
1791        assert!(!throttler.should_log(&state2));
1792    }
1793
1794    #[test]
1795    fn test_log_throttler_heartbeat() {
1796        let mut throttler = LogThrottler::<CompactionLogState>::new(Duration::from_millis(10));
1797        let state = CompactionLogState {
1798            running_parallelism: 10,
1799            pull_task_ack: true,
1800            pending_pull_task_count: 2,
1801        };
1802
1803        // First call should log
1804        assert!(throttler.should_log(&state));
1805        throttler.update(state.clone());
1806
1807        // Same state immediately should not log
1808        assert!(!throttler.should_log(&state));
1809
1810        // Wait for heartbeat interval to pass
1811        std::thread::sleep(Duration::from_millis(15));
1812
1813        // Same state after interval should log (heartbeat)
1814        assert!(throttler.should_log(&state));
1815    }
1816}