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