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_plan_parallelism = iceberg_compaction_max_plan_parallelism(
420 worker_num,
421 compactor_context
422 .storage_opts
423 .iceberg_compaction_task_parallelism_ratio,
424 max_task_parallelism,
425 );
426
427 let max_pull_task_count = std::cmp::min(
428 max_task_parallelism,
429 compactor_context
430 .storage_opts
431 .iceberg_compaction_max_pull_task_count,
432 );
433
434 assert_ge!(
435 compactor_context.storage_opts.compactor_max_task_multiplier,
436 0.0
437 );
438
439 let join_handle = tokio::spawn(async move {
440 let pending_parallelism_budget = (max_task_parallelism as f32
442 * compactor_context
443 .storage_opts
444 .iceberg_compaction_pending_parallelism_budget_multiplier)
445 .ceil() as u32;
446 let mut task_queue = IcebergTaskQueue::new_with_metrics(
447 max_task_parallelism,
448 pending_parallelism_budget,
449 iceberg_compaction_memory_limit_bytes,
450 compactor_context.compactor_metrics.clone(),
451 );
452
453 let shutdown_map = Arc::new(Mutex::new(HashMap::<TaskKey, Sender<()>>::new()));
455
456 let (task_completion_tx, mut task_completion_rx) =
458 tokio::sync::mpsc::unbounded_channel::<IcebergPlanCompletion>();
459 let mut task_trackers = HashMap::<IcebergCompactionTaskId, IcebergTaskTracker>::new();
460 let mut pending_task_reports = VecDeque::<IcebergTaskReport>::new();
463
464 let mut min_interval = tokio::time::interval(stream_retry_interval);
465 let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
466
467 let mut log_throttler =
469 LogThrottler::<IcebergCompactionLogState>::new(COMPACTION_HEARTBEAT_LOG_INTERVAL);
470
471 'start_stream: loop {
473 let mut pull_task_ack = true;
476 tokio::select! {
477 _ = min_interval.tick() => {},
479 _ = &mut shutdown_rx => {
481 tracing::info!("Compactor is shutting down");
482 return;
483 }
484 }
485
486 let (request_sender, response_event_stream) = match hummock_meta_client
487 .subscribe_iceberg_compaction_event()
488 .await
489 {
490 Ok((request_sender, response_event_stream)) => {
491 tracing::debug!("Succeeded subscribe_iceberg_compaction_event.");
492 (request_sender, response_event_stream)
493 }
494
495 Err(e) => {
496 tracing::warn!(
497 error = %e.as_report(),
498 "Subscribing to iceberg compaction tasks failed with error. Will retry.",
499 );
500 continue 'start_stream;
501 }
502 };
503
504 if matches!(
505 flush_pending_iceberg_task_reports(&request_sender, &mut pending_task_reports),
506 ReportSendResult::RestartStream
507 ) {
508 continue 'start_stream;
509 }
510
511 pin_mut!(response_event_stream);
512
513 let _executor = compactor_context.compaction_executor.clone();
514
515 let mut event_loop_iteration_now = Instant::now();
517 'consume_stream: loop {
518 {
519 compactor_context
521 .compactor_metrics
522 .compaction_event_loop_iteration_latency
523 .observe(event_loop_iteration_now.elapsed().as_millis() as _);
524 event_loop_iteration_now = Instant::now();
525 }
526
527 let request_sender = request_sender.clone();
528 let event: Option<Result<SubscribeIcebergCompactionEventResponse, _>> = tokio::select! {
529 Some(plan_completion) = task_completion_rx.recv() => {
531 let task_key = plan_completion.task_key;
532 let error_message = plan_completion.error_message;
533 let pk_index_result = plan_completion.pk_index_result;
534 tracing::debug!(
535 task_id = %task_key.0,
536 plan_index = task_key.1,
537 success = error_message.is_none(),
538 "Plan completed, updating queue state"
539 );
540 task_queue.finish_running(task_key);
541
542 let completed_task_id = task_key.0;
543 let Entry::Occupied(mut tracker_entry) =
544 task_trackers.entry(completed_task_id)
545 else {
546 continue 'consume_stream;
547 };
548 tracker_entry
549 .get_mut()
550 .record_completion(error_message, pk_index_result);
551 if !tracker_entry.get().is_finished() {
552 continue 'consume_stream;
553 }
554
555 let tracker = tracker_entry.remove();
556 let sink_id = tracker.sink_id();
557 let admitted_plans = tracker.admitted_plans();
558 let successful_plans = tracker.successful_plans();
559 let failed_plans = tracker.failed_plans();
560 let report = tracker.into_report(completed_task_id);
561 let task_succeeded = report.error_message.is_none();
562 if matches!(
563 send_or_buffer_iceberg_task_report(
564 &request_sender,
565 &mut pending_task_reports,
566 report,
567 ),
568 ReportSendResult::RestartStream
569 ) {
570 continue 'start_stream;
571 }
572 if task_succeeded {
573 tracing::info!(
574 iceberg_component = "compaction_worker",
575 iceberg_operation = "report_task",
576 task_id = %completed_task_id,
577 sink_id = sink_id,
578 admitted_plans = admitted_plans,
579 successful_plans = successful_plans,
580 failed_plans = failed_plans,
581 "iceberg_compaction_task_succeeded",
582 );
583 }
584 continue 'consume_stream;
585 }
586
587 _ = task_queue.wait_schedulable() => {
589 schedule_queued_tasks(
590 &mut task_queue,
591 &compactor_context,
592 &shutdown_map,
593 &task_completion_tx,
594 );
595 continue 'consume_stream;
596 }
597
598 _ = periodic_event_interval.tick() => {
599 let should_restart_stream = handle_meta_task_pulling(
601 &mut pull_task_ack,
602 &task_queue,
603 max_task_parallelism,
604 max_pull_task_count,
605 &request_sender,
606 &mut log_throttler,
607 );
608
609 if should_restart_stream {
610 continue 'start_stream;
611 }
612 continue;
613 }
614 event = response_event_stream.next() => {
615 event
616 }
617
618 _ = &mut shutdown_rx => {
619 tracing::info!("Iceberg Compactor is shutting down");
620 return
621 }
622 };
623
624 match event {
625 Some(Ok(SubscribeIcebergCompactionEventResponse {
626 event,
627 create_at: _create_at,
628 })) => {
629 let event = match event {
630 Some(event) => event,
631 None => continue 'consume_stream,
632 };
633
634 match event {
635 risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::CompactTask(iceberg_compaction_task) => {
636 let task_id = iceberg_compaction_task.task_id;
637 let sink_id = iceberg_compaction_task.sink_id;
638 let is_bounded_round = iceberg_compaction_task.max_file_sequence_number.is_some();
639 let pk_index_coordinated = iceberg_compaction_task.pk_index_coordinated;
640 #[cfg(madsim)]
645 if iceberg_compaction_task.pk_index_coordinated
646 && fail::eval(
647 "iceberg_pk_index_simulated_compaction_report",
648 |_| (),
649 )
650 .is_some()
651 {
652 let result = simulated_pk_index_compaction_result();
655 let mut report = if result.is_none() && is_bounded_round {
656 build_drained_iceberg_task_report(task_id, sink_id)
657 } else {
658 build_iceberg_task_report(task_id, sink_id, None)
659 };
660 report.pk_index_result = result;
661 if matches!(
662 send_or_buffer_iceberg_task_report(
663 &request_sender,
664 &mut pending_task_reports,
665 report,
666 ),
667 ReportSendResult::RestartStream
668 ) {
669 continue 'start_stream;
670 }
671 continue 'consume_stream;
672 }
673 let compactor_runner_config = match IcebergCompactorRunnerConfigBuilder::default()
675 .max_parallelism(max_plan_parallelism)
676 .min_size_per_partition(compactor_context.storage_opts.iceberg_compaction_min_size_per_partition_mb as u64 * 1024 * 1024)
677 .max_file_count_per_partition(compactor_context.storage_opts.iceberg_compaction_max_file_count_per_partition)
678 .enable_validate_compaction(compactor_context.storage_opts.iceberg_compaction_enable_validate)
679 .max_record_batch_rows(compactor_context.storage_opts.iceberg_compaction_max_record_batch_rows)
680 .enable_heuristic_output_parallelism(compactor_context.storage_opts.iceberg_compaction_enable_heuristic_output_parallelism)
681 .max_concurrent_closes(compactor_context.storage_opts.iceberg_compaction_max_concurrent_closes)
682 .enable_prefetch(compactor_context.storage_opts.iceberg_compaction_enable_prefetch)
683 .target_binpack_group_size_mb(
684 compactor_context.storage_opts.iceberg_compaction_target_binpack_group_size_mb
685 )
686 .min_group_size_mb(
687 compactor_context.storage_opts.iceberg_compaction_min_group_size_mb
688 )
689 .min_group_file_count(
690 compactor_context.storage_opts.iceberg_compaction_min_group_file_count
691 )
692 .build() {
693 Ok(config) => config,
694 Err(e) => {
695 tracing::warn!(
696 iceberg_component = "compaction_worker",
697 iceberg_operation = "build_runner_config",
698 error = %e.as_report(),
699 task_id = %task_id,
700 sink_id = sink_id,
701 "iceberg_compaction_runner_config_failed",
702 );
703 let report = build_iceberg_task_report(
704 task_id,
705 sink_id,
706 Some(format!(
707 "Failed to build iceberg compactor runner config: {}",
708 e.as_report()
709 )),
710 );
711 if matches!(
712 send_or_buffer_iceberg_task_report(
713 &request_sender,
714 &mut pending_task_reports,
715 report,
716 ),
717 ReportSendResult::RestartStream
718 ) {
719 continue 'start_stream;
720 }
721 continue 'consume_stream;
722 }
723 };
724
725 let task_execution = match create_task_execution(
727 iceberg_compaction_task,
728 compactor_runner_config,
729 compactor_context.compactor_metrics.clone(),
730 ).await {
731 Ok(task_execution) => task_execution,
732 Err(e) => {
733 tracing::warn!(
734 iceberg_component = "compaction_worker",
735 iceberg_operation = "plan_task",
736 error = %e.as_report(),
737 task_id = %task_id,
738 sink_id = sink_id,
739 "iceberg_compaction_task_plan_failed",
740 );
741 let report = build_iceberg_task_report(
742 task_id,
743 sink_id,
744 Some(format!(
745 "Failed to create iceberg compaction task execution: {}",
746 e.as_report()
747 )),
748 );
749 if matches!(
750 send_or_buffer_iceberg_task_report(
751 &request_sender,
752 &mut pending_task_reports,
753 report,
754 ),
755 ReportSendResult::RestartStream
756 ) {
757 continue 'start_stream;
758 }
759 continue 'consume_stream;
760 }
761 };
762
763 let (sink_id, plan_runners) = match task_execution {
764 IcebergTaskExecution::Ready { sink_id, plan_runners } => {
765 (sink_id, plan_runners)
766 }
767 IcebergTaskExecution::Blocked { sink_id, status, reason } => {
768 let mut report = build_iceberg_task_report(task_id, sink_id, Some(reason));
769 report.status = status as i32;
770 if matches!(
771 send_or_buffer_iceberg_task_report(&request_sender, &mut pending_task_reports, report),
772 ReportSendResult::RestartStream
773 ) {
774 continue 'start_stream;
775 }
776 continue 'consume_stream;
777 }
778 IcebergTaskExecution::NoWork { sink_id } => {
779 let report = if is_bounded_round {
781 build_drained_iceberg_task_report(task_id, sink_id)
782 } else {
783 build_iceberg_task_report(task_id, sink_id, None)
784 };
785 if matches!(
786 send_or_buffer_iceberg_task_report(
787 &request_sender,
788 &mut pending_task_reports,
789 report,
790 ),
791 ReportSendResult::RestartStream
792 ) {
793 continue 'start_stream;
794 }
795 continue 'consume_stream;
796 }
797 };
798
799 let planned_count = plan_runners.len();
801 let mut admitted_count = 0;
802 let mut rejection_reason = None;
803 let mut rejection_status = None;
804
805 for runner in plan_runners {
806 let meta = runner.to_meta();
807 let plan_index = meta.plan_index;
808 let required_parallelism = runner.required_parallelism();
809 let memory_reservation_bytes = meta.memory_reservation_bytes;
810 let runner_sink_id = runner.sink_id;
811 let runner_compaction_kind = runner.compaction_kind();
812 let runner_table = runner.table_ident.to_string();
813 let push_result = task_queue.push(meta, Some(runner));
814
815 match push_result {
816 PushResult::Added => {
817 admitted_count += 1;
818 tracing::debug!(
819 iceberg_component = "compaction_worker",
820 iceberg_operation = "enqueue_plan",
821 task_id = %task_id,
822 sink_id = runner_sink_id,
823 plan_index = plan_index,
824 task_type = ?runner_compaction_kind,
825 table = %runner_table,
826 required_parallelism = required_parallelism,
827 "iceberg_compaction_plan_enqueued",
828 );
829 },
830 PushResult::RejectedCapacity => {
831 rejection_status = Some(risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_request::report_task::Status::Busy);
832 tracing::warn!(
833 iceberg_component = "compaction_worker",
834 iceberg_operation = "enqueue_plan",
835 task_id = %task_id,
836 sink_id = runner_sink_id,
837 plan_index = plan_index,
838 task_type = ?runner_compaction_kind,
839 table = %runner_table,
840 required_parallelism = required_parallelism,
841 pending_budget = pending_parallelism_budget,
842 admitted_count = admitted_count,
843 planned_count = planned_count,
844 "iceberg_compaction_plan_rejected_capacity",
845 );
846 break;
848 },
849 PushResult::RejectedTooLarge => {
850 rejection_status = Some(risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_request::report_task::Status::BlockedCapacity);
851 rejection_reason.get_or_insert_with(|| {
852 format!(
853 "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"
854 )
855 });
856 tracing::error!(
857 iceberg_component = "compaction_worker",
858 iceberg_operation = "enqueue_plan",
859 task_id = %task_id,
860 sink_id = runner_sink_id,
861 plan_index = plan_index,
862 task_type = ?runner_compaction_kind,
863 table = %runner_table,
864 required_parallelism = required_parallelism,
865 max_parallelism = max_task_parallelism,
866 memory_reservation_bytes,
867 memory_budget_bytes = iceberg_compaction_memory_limit_bytes,
868 "iceberg_compaction_plan_rejected_too_large",
869 );
870 },
871 PushResult::RejectedInvalidParallelism => {
872 tracing::error!(
873 iceberg_component = "compaction_worker",
874 iceberg_operation = "enqueue_plan",
875 task_id = %task_id,
876 sink_id = runner_sink_id,
877 plan_index = plan_index,
878 task_type = ?runner_compaction_kind,
879 table = %runner_table,
880 required_parallelism = required_parallelism,
881 "iceberg_compaction_plan_rejected_invalid_parallelism",
882 );
883 },
884 PushResult::RejectedDuplicate => {
885 tracing::error!(
886 iceberg_component = "compaction_worker",
887 iceberg_operation = "enqueue_plan",
888 task_id = %task_id,
889 sink_id = runner_sink_id,
890 plan_index = plan_index,
891 task_type = ?runner_compaction_kind,
892 table = %runner_table,
893 "iceberg_compaction_plan_rejected_duplicate",
894 );
895 }
896 }
897 }
898
899 if admitted_count == 0 {
900 let mut report = build_iceberg_task_report(
901 task_id,
902 sink_id,
903 Some(rejection_reason.unwrap_or_else(|| {
904 "Failed to enqueue all iceberg compaction plans".to_owned()
905 })),
906 );
907 if let Some(status) = rejection_status {
908 report.status = status as i32;
909 }
910 if matches!(
911 send_or_buffer_iceberg_task_report(
912 &request_sender,
913 &mut pending_task_reports,
914 report,
915 ),
916 ReportSendResult::RestartStream
917 ) {
918 continue 'start_stream;
919 }
920 } else {
921 let fully_admitted_bounded_round =
925 is_bounded_round
926 && admitted_count == planned_count;
927 task_trackers.insert(
928 task_id,
929 IcebergTaskTracker::new(
930 sink_id,
931 admitted_count,
932 pk_index_coordinated,
933 fully_admitted_bounded_round,
934 ),
935 );
936 }
937
938 tracing::info!(
939 iceberg_component = "compaction_worker",
940 iceberg_operation = "enqueue_plan",
941 task_id = %task_id,
942 sink_id = sink_id,
943 planned_count = planned_count,
944 admitted_count = admitted_count,
945 "iceberg_compaction_task_enqueue_finished",
946 );
947 },
948 risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::PullTaskAck(_) => {
949 pull_task_ack = true;
951 },
952 risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event::CancelCompactTask(cancel_compact_task) => {
953 cancel_iceberg_task(
954 cancel_compact_task.task_id,
955 &mut task_queue,
956 &shutdown_map,
957 &mut task_trackers,
958 );
959 },
960 }
961 }
962 Some(Err(e)) => {
963 tracing::warn!("Failed to consume stream. {}", e.message());
964 continue 'start_stream;
965 }
966 _ => {
967 continue 'start_stream;
969 }
970 }
971 }
972 }
973 });
974
975 (join_handle, shutdown_tx)
976}
977
978#[must_use]
981pub fn start_compactor(
982 compactor_context: CompactorContext,
983 hummock_meta_client: Arc<dyn HummockMetaClient>,
984 object_id_manager: Arc<ObjectIdManager>,
985 compaction_catalog_manager_ref: CompactionCatalogManagerRef,
986) -> (JoinHandle<()>, Sender<()>) {
987 type CompactionShutdownMap = Arc<Mutex<HashMap<u64, Sender<()>>>>;
988 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
989 let stream_retry_interval = Duration::from_secs(30);
990 let task_progress = compactor_context.task_progress_manager.clone();
991 let periodic_event_update_interval = Duration::from_millis(1000);
992
993 let max_task_parallelism: u32 = (compactor_context.compaction_executor.worker_num() as f32
994 * compactor_context.storage_opts.compactor_max_task_multiplier)
995 .ceil() as u32;
996 let running_task_parallelism = Arc::new(AtomicU32::new(0));
997
998 const MAX_PULL_TASK_COUNT: u32 = 4;
999 let max_pull_task_count = std::cmp::min(max_task_parallelism, MAX_PULL_TASK_COUNT);
1000
1001 assert_ge!(
1002 compactor_context.storage_opts.compactor_max_task_multiplier,
1003 0.0
1004 );
1005
1006 let join_handle = tokio::spawn(async move {
1007 let shutdown_map = CompactionShutdownMap::default();
1008 let mut min_interval = tokio::time::interval(stream_retry_interval);
1009 let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
1010
1011 let mut log_throttler =
1013 LogThrottler::<CompactionLogState>::new(COMPACTION_HEARTBEAT_LOG_INTERVAL);
1014
1015 'start_stream: loop {
1017 let mut pull_task_ack = true;
1020 tokio::select! {
1021 _ = min_interval.tick() => {},
1023 _ = &mut shutdown_rx => {
1025 tracing::info!("Compactor is shutting down");
1026 return;
1027 }
1028 }
1029
1030 let (request_sender, response_event_stream) =
1031 match hummock_meta_client.subscribe_compaction_event().await {
1032 Ok((request_sender, response_event_stream)) => {
1033 tracing::debug!("Succeeded subscribe_compaction_event.");
1034 (request_sender, response_event_stream)
1035 }
1036
1037 Err(e) => {
1038 tracing::warn!(
1039 error = %e.as_report(),
1040 "Subscribing to compaction tasks failed with error. Will retry.",
1041 );
1042 continue 'start_stream;
1043 }
1044 };
1045
1046 pin_mut!(response_event_stream);
1047
1048 let executor = compactor_context.compaction_executor.clone();
1049 let object_id_manager = object_id_manager.clone();
1050
1051 let mut event_loop_iteration_now = Instant::now();
1053 'consume_stream: loop {
1054 {
1055 compactor_context
1057 .compactor_metrics
1058 .compaction_event_loop_iteration_latency
1059 .observe(event_loop_iteration_now.elapsed().as_millis() as _);
1060 event_loop_iteration_now = Instant::now();
1061 }
1062
1063 let running_task_parallelism = running_task_parallelism.clone();
1064 let request_sender = request_sender.clone();
1065 let event: Option<Result<SubscribeCompactionEventResponse, _>> = tokio::select! {
1066 _ = periodic_event_interval.tick() => {
1067 let progress_list = get_task_progress(task_progress.clone());
1068
1069 if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
1070 event: Some(RequestEvent::HeartBeat(
1071 HeartBeat {
1072 progress: progress_list
1073 }
1074 )),
1075 create_at: SystemTime::now()
1076 .duration_since(std::time::UNIX_EPOCH)
1077 .expect("Clock may have gone backwards")
1078 .as_millis() as u64,
1079 }) {
1080 tracing::warn!(error = %e.as_report(), "Failed to report task progress");
1081 continue 'start_stream;
1083 }
1084
1085
1086 let mut pending_pull_task_count = 0;
1087 if pull_task_ack {
1088 pending_pull_task_count = (max_task_parallelism - running_task_parallelism.load(Ordering::SeqCst)).min(max_pull_task_count);
1090
1091 if pending_pull_task_count > 0 {
1092 if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
1093 event: Some(RequestEvent::PullTask(
1094 PullTask {
1095 pull_task_count: pending_pull_task_count,
1096 }
1097 )),
1098 create_at: SystemTime::now()
1099 .duration_since(std::time::UNIX_EPOCH)
1100 .expect("Clock may have gone backwards")
1101 .as_millis() as u64,
1102 }) {
1103 tracing::warn!(error = %e.as_report(), "Failed to pull task");
1104
1105 continue 'start_stream;
1107 } else {
1108 pull_task_ack = false;
1109 }
1110 }
1111 }
1112
1113 let running_count = running_task_parallelism.load(Ordering::SeqCst);
1114 let current_state = CompactionLogState {
1115 running_parallelism: running_count,
1116 pull_task_ack,
1117 pending_pull_task_count,
1118 };
1119
1120 if log_throttler.should_log(¤t_state) {
1122 tracing::info!(
1123 running_parallelism_count = %current_state.running_parallelism,
1124 pull_task_ack = %current_state.pull_task_ack,
1125 pending_pull_task_count = %current_state.pending_pull_task_count
1126 );
1127 log_throttler.update(current_state);
1128 }
1129
1130 continue;
1131 }
1132 event = response_event_stream.next() => {
1133 event
1134 }
1135
1136 _ = &mut shutdown_rx => {
1137 tracing::info!("Compactor is shutting down");
1138 return
1139 }
1140 };
1141
1142 fn send_report_task_event(
1143 compact_task: &CompactTask,
1144 table_stats: TableStatsMap,
1145 object_timestamps: HashMap<HummockSstableObjectId, u64>,
1146 request_sender: &mpsc::UnboundedSender<SubscribeCompactionEventRequest>,
1147 ) {
1148 if let Err(e) = request_sender.send(SubscribeCompactionEventRequest {
1149 event: Some(RequestEvent::ReportTask(ReportTask {
1150 task_id: compact_task.task_id,
1151 task_status: compact_task.task_status.into(),
1152 sorted_output_ssts: compact_task
1153 .sorted_output_ssts
1154 .iter()
1155 .map(|sst| sst.into())
1156 .collect(),
1157 table_stats_change: to_prost_table_stats_map(table_stats),
1158 object_timestamps,
1159 })),
1160 create_at: SystemTime::now()
1161 .duration_since(std::time::UNIX_EPOCH)
1162 .expect("Clock may have gone backwards")
1163 .as_millis() as u64,
1164 }) {
1165 let task_id = compact_task.task_id;
1166 tracing::warn!(error = %e.as_report(), "Failed to report task {task_id:?}");
1167 }
1168 }
1169
1170 match event {
1171 Some(Ok(SubscribeCompactionEventResponse { event, create_at })) => {
1172 let event = match event {
1173 Some(event) => event,
1174 None => continue 'consume_stream,
1175 };
1176 let shutdown = shutdown_map.clone();
1177 let context = compactor_context.clone();
1178 let consumed_latency_ms = SystemTime::now()
1179 .duration_since(std::time::UNIX_EPOCH)
1180 .expect("Clock may have gone backwards")
1181 .as_millis() as u64
1182 - create_at;
1183 context
1184 .compactor_metrics
1185 .compaction_event_consumed_latency
1186 .observe(consumed_latency_ms as _);
1187
1188 let object_id_manager = object_id_manager.clone();
1189 let compaction_catalog_manager_ref = compaction_catalog_manager_ref.clone();
1190
1191 match event {
1192 ResponseEvent::CompactTask(compact_task) => {
1193 let compact_task = CompactTask::from(compact_task);
1194 let parallelism =
1195 calculate_task_parallelism(&compact_task, &context);
1196
1197 assert_ne!(parallelism, 0, "splits cannot be empty");
1198
1199 if (max_task_parallelism
1200 - running_task_parallelism.load(Ordering::SeqCst))
1201 < parallelism as u32
1202 {
1203 tracing::warn!(
1204 "Not enough core parallelism to serve the task {} task_parallelism {} running_task_parallelism {} max_task_parallelism {}",
1205 compact_task.task_id,
1206 parallelism,
1207 max_task_parallelism,
1208 running_task_parallelism.load(Ordering::Relaxed),
1209 );
1210 let (compact_task, table_stats, object_timestamps) =
1211 compact_done(
1212 compact_task,
1213 context.clone(),
1214 vec![],
1215 TaskStatus::NoAvailCpuResourceCanceled,
1216 );
1217
1218 send_report_task_event(
1219 &compact_task,
1220 table_stats,
1221 object_timestamps,
1222 &request_sender,
1223 );
1224
1225 continue 'consume_stream;
1226 }
1227
1228 running_task_parallelism
1229 .fetch_add(parallelism as u32, Ordering::SeqCst);
1230 executor.spawn(async move {
1231 let (tx, rx) = tokio::sync::oneshot::channel();
1232 let task_id = compact_task.task_id;
1233 shutdown.lock().unwrap().insert(task_id, tx);
1234
1235 let (
1236 (compact_task, table_stats, object_timestamps),
1237 _memory_tracker,
1238 ) = compactor_runner::compact(
1239 context.clone(),
1240 compact_task,
1241 rx,
1242 object_id_manager.clone(),
1243 compaction_catalog_manager_ref.clone(),
1244 )
1245 .await;
1246
1247 shutdown.lock().unwrap().remove(&task_id);
1248 running_task_parallelism
1249 .fetch_sub(parallelism as u32, Ordering::SeqCst);
1250
1251 send_report_task_event(
1252 &compact_task,
1253 table_stats,
1254 object_timestamps,
1255 &request_sender,
1256 );
1257
1258 let enable_check_compaction_result =
1259 context.storage_opts.check_compaction_result;
1260 let need_check_task =
1261 !compact_task.sorted_output_ssts.is_empty()
1262 && compact_task.task_status == TaskStatus::Success;
1263
1264 if enable_check_compaction_result && need_check_task {
1265 let read_table_ids = compact_task
1266 .get_table_ids_from_input_ssts()
1267 .collect::<Vec<_>>();
1268 match compaction_catalog_manager_ref.acquire(read_table_ids).await {
1269 Ok(compaction_catalog_agent_ref) => {
1270 match check_compaction_result(&compact_task, context.clone(), compaction_catalog_agent_ref).await
1271 {
1272 Err(e) => {
1273 tracing::warn!(error = %e.as_report(), "Failed to check compaction task {}",compact_task.task_id);
1274 }
1275 Ok(true) => (),
1276 Ok(false) => {
1277 panic!("Failed to pass consistency check for result of compaction task:\n{:?}", compact_task_to_string(&compact_task));
1278 }
1279 }
1280 },
1281 Err(e) => {
1282 tracing::warn!(error = %e.as_report(), "failed to acquire compaction catalog agent");
1283 }
1284 }
1285 }
1286 });
1287 }
1288 #[expect(deprecated)]
1289 ResponseEvent::VacuumTask(_) => {
1290 unreachable!("unexpected vacuum task");
1291 }
1292 #[expect(deprecated)]
1293 ResponseEvent::FullScanTask(_) => {
1294 unreachable!("unexpected scan task");
1295 }
1296 #[expect(deprecated)]
1297 ResponseEvent::ValidationTask(validation_task) => {
1298 let validation_task = ValidationTask::from(validation_task);
1299 executor.spawn(async move {
1300 validate_ssts(validation_task, context.sstable_store.clone())
1301 .await;
1302 });
1303 }
1304 ResponseEvent::CancelCompactTask(cancel_compact_task) => match shutdown
1305 .lock()
1306 .unwrap()
1307 .remove(&cancel_compact_task.task_id)
1308 {
1309 Some(tx) => {
1310 if tx.send(()).is_err() {
1311 tracing::warn!(
1312 "Cancellation of compaction task failed. task_id: {}",
1313 cancel_compact_task.task_id
1314 );
1315 }
1316 }
1317 _ => {
1318 tracing::warn!(
1319 "Attempting to cancel non-existent compaction task. task_id: {}",
1320 cancel_compact_task.task_id
1321 );
1322 }
1323 },
1324
1325 ResponseEvent::PullTaskAck(_pull_task_ack) => {
1326 pull_task_ack = true;
1328 }
1329 }
1330 }
1331 Some(Err(e)) => {
1332 tracing::warn!("Failed to consume stream. {}", e.message());
1333 continue 'start_stream;
1334 }
1335 _ => {
1336 continue 'start_stream;
1338 }
1339 }
1340 }
1341 }
1342 });
1343
1344 (join_handle, shutdown_tx)
1345}
1346
1347#[must_use]
1350pub fn start_shared_compactor(
1351 grpc_proxy_client: GrpcCompactorProxyClient,
1352 mut receiver: mpsc::UnboundedReceiver<Request<DispatchCompactionTaskRequest>>,
1353 context: CompactorContext,
1354) -> (JoinHandle<()>, Sender<()>) {
1355 type CompactionShutdownMap = Arc<Mutex<HashMap<u64, Sender<()>>>>;
1356 let task_progress = context.task_progress_manager.clone();
1357 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
1358 let periodic_event_update_interval = Duration::from_millis(1000);
1359
1360 let join_handle = tokio::spawn(async move {
1361 let shutdown_map = CompactionShutdownMap::default();
1362
1363 let mut periodic_event_interval = tokio::time::interval(periodic_event_update_interval);
1364 let executor = context.compaction_executor.clone();
1365 let report_heartbeat_client = grpc_proxy_client.clone();
1366 'consume_stream: loop {
1367 let request: Option<Request<DispatchCompactionTaskRequest>> = tokio::select! {
1368 _ = periodic_event_interval.tick() => {
1369 let progress_list = get_task_progress(task_progress.clone());
1370 let report_compaction_task_request = ReportCompactionTaskRequest{
1371 event: Some(ReportCompactionTaskEvent::HeartBeat(
1372 SharedHeartBeat {
1373 progress: progress_list
1374 }
1375 )),
1376 };
1377 if let Err(e) = report_heartbeat_client.report_compaction_task(report_compaction_task_request).await{
1378 tracing::warn!(error = %e.as_report(), "Failed to report heartbeat");
1379 }
1380 continue
1381 }
1382
1383
1384 _ = &mut shutdown_rx => {
1385 tracing::info!("Compactor is shutting down");
1386 return
1387 }
1388
1389 request = receiver.recv() => {
1390 request
1391 }
1392
1393 };
1394 match request {
1395 Some(request) => {
1396 let context = context.clone();
1397 let shutdown = shutdown_map.clone();
1398
1399 let cloned_grpc_proxy_client = grpc_proxy_client.clone();
1400 executor.spawn(async move {
1401 let DispatchCompactionTaskRequest {
1402 tables,
1403 output_object_ids,
1404 task: dispatch_task,
1405 } = request.into_inner();
1406 let table_id_to_catalog = tables.into_iter().fold(HashMap::new(), |mut acc, table| {
1407 acc.insert(table.id, table);
1408 acc
1409 });
1410
1411 let mut output_object_ids_deque: VecDeque<_> = VecDeque::new();
1412 output_object_ids_deque.extend(output_object_ids.into_iter().map(Into::<HummockSstableObjectId>::into));
1413 let shared_compactor_object_id_manager =
1414 SharedComapctorObjectIdManager::new(output_object_ids_deque, cloned_grpc_proxy_client.clone(), context.storage_opts.sstable_id_remote_fetch_number);
1415 match dispatch_task.unwrap() {
1416 dispatch_compaction_task_request::Task::CompactTask(compact_task) => {
1417 let compact_task = CompactTask::from(&compact_task);
1418 let (tx, rx) = tokio::sync::oneshot::channel();
1419 let task_id = compact_task.task_id;
1420 shutdown.lock().unwrap().insert(task_id, tx);
1421
1422 let compaction_catalog_manager_ref =
1423 Arc::new(CompactionCatalogManager::new_preloaded(
1424 table_id_to_catalog,
1425 ));
1426 let ((compact_task, table_stats, object_timestamps), _memory_tracker)= compactor_runner::compact(
1427 context.clone(),
1428 compact_task,
1429 rx,
1430 shared_compactor_object_id_manager,
1431 compaction_catalog_manager_ref.clone(),
1432 )
1433 .await;
1434 shutdown.lock().unwrap().remove(&task_id);
1435 let report_compaction_task_request = ReportCompactionTaskRequest {
1436 event: Some(ReportCompactionTaskEvent::ReportTask(ReportSharedTask {
1437 compact_task: Some(PbCompactTask {
1439 task_id: compact_task.task_id,
1440 task_status: compact_task.task_status.into(),
1441 sorted_output_ssts: compact_task
1442 .sorted_output_ssts
1443 .iter()
1444 .map(|sst| sst.into())
1445 .collect(),
1446 ..Default::default()
1447 }),
1448 table_stats_change: to_prost_table_stats_map(table_stats),
1449 object_timestamps,
1450 })),
1451 };
1452
1453 match cloned_grpc_proxy_client
1454 .report_compaction_task(report_compaction_task_request)
1455 .await
1456 {
1457 Ok(_) => {
1458 let enable_check_compaction_result =
1460 context.storage_opts.check_compaction_result;
1461 let need_check_task =
1462 !compact_task.sorted_output_ssts.is_empty()
1463 && compact_task.task_status
1464 == TaskStatus::Success;
1465 if enable_check_compaction_result && need_check_task {
1466 let read_table_ids = compact_task
1467 .get_table_ids_from_input_ssts()
1468 .collect::<Vec<_>>();
1469 match compaction_catalog_manager_ref.acquire(read_table_ids).await {
1470 Ok(compaction_catalog_agent_ref) => {
1471 match check_compaction_result(&compact_task, context.clone(), compaction_catalog_agent_ref).await
1472 {
1473 Err(e) => {
1474 tracing::warn!(error = %e.as_report(), "Failed to check compaction task {}", task_id);
1475 }
1476 Ok(true) => (),
1477 Ok(false) => {
1478 panic!("Failed to pass consistency check for result of compaction task:\n{:?}", compact_task_to_string(&compact_task));
1479 }
1480 }
1481 }
1482 Err(e) => {
1483 tracing::warn!(error = %e.as_report(), "failed to acquire compaction catalog agent");
1484 }
1485 }
1486 }
1487 }
1488 Err(e) => tracing::warn!(error = %e.as_report(), "Failed to report task {task_id:?}"),
1489 }
1490
1491 }
1492 dispatch_compaction_task_request::Task::VacuumTask(_) => {
1493 unreachable!("unexpected vacuum task");
1494 }
1495 dispatch_compaction_task_request::Task::FullScanTask(_) => {
1496 unreachable!("unexpected scan task");
1497 }
1498 dispatch_compaction_task_request::Task::ValidationTask(validation_task) => {
1499 let validation_task = ValidationTask::from(validation_task);
1500 validate_ssts(validation_task, context.sstable_store.clone()).await;
1501 }
1502 dispatch_compaction_task_request::Task::CancelCompactTask(cancel_compact_task) => {
1503 match shutdown
1504 .lock()
1505 .unwrap()
1506 .remove(&cancel_compact_task.task_id)
1507 { Some(tx) => {
1508 if tx.send(()).is_err() {
1509 tracing::warn!(
1510 "Cancellation of compaction task failed. task_id: {}",
1511 cancel_compact_task.task_id
1512 );
1513 }
1514 } _ => {
1515 tracing::warn!(
1516 "Attempting to cancel non-existent compaction task. task_id: {}",
1517 cancel_compact_task.task_id
1518 );
1519 }}
1520 }
1521 }
1522 });
1523 }
1524 None => continue 'consume_stream,
1525 }
1526 }
1527 });
1528 (join_handle, shutdown_tx)
1529}
1530
1531fn get_task_progress(
1532 task_progress: Arc<
1533 parking_lot::lock_api::Mutex<parking_lot::RawMutex, HashMap<u64, Arc<TaskProgress>>>,
1534 >,
1535) -> Vec<CompactTaskProgress> {
1536 let mut progress_list = Vec::new();
1537 for (&task_id, progress) in &*task_progress.lock() {
1538 progress_list.push(progress.snapshot(task_id));
1539 }
1540 progress_list
1541}
1542
1543fn schedule_queued_tasks(
1545 task_queue: &mut IcebergTaskQueue,
1546 compactor_context: &CompactorContext,
1547 shutdown_map: &Arc<Mutex<HashMap<TaskKey, Sender<()>>>>,
1548 task_completion_tx: &tokio::sync::mpsc::UnboundedSender<IcebergPlanCompletion>,
1549) {
1550 while let Some(popped_task) = task_queue.pop() {
1551 let task_id = popped_task.meta.task_id;
1552 let plan_index = popped_task.meta.plan_index;
1553 let task_key = (task_id, plan_index);
1554
1555 let unique_ident = popped_task.runner.as_ref().map(|r| r.unique_ident());
1557
1558 let Some(runner) = popped_task.runner else {
1559 tracing::error!(
1560 iceberg_component = "compaction_worker",
1561 iceberg_operation = "schedule_plan",
1562 task_id = %task_id,
1563 plan_index = plan_index,
1564 "iceberg_compaction_plan_missing_runner",
1565 );
1566 task_queue.finish_running(task_key);
1567 continue;
1568 };
1569 let runner_sink_id = runner.sink_id;
1570 let runner_compaction_kind = runner.compaction_kind();
1571 let runner_table = runner.table_ident.to_string();
1572
1573 let executor = compactor_context.compaction_executor.clone();
1574 let shutdown_map_clone = shutdown_map.clone();
1575 let completion_tx_clone = task_completion_tx.clone();
1576 let (tx, rx) = tokio::sync::oneshot::channel();
1577
1578 {
1579 let mut shutdown_guard = shutdown_map.lock().unwrap();
1580 shutdown_guard.insert(task_key, tx);
1581 }
1582
1583 tracing::info!(
1584 iceberg_component = "compaction_worker",
1585 iceberg_operation = "schedule_plan",
1586 task_id = %task_id,
1587 sink_id = runner_sink_id,
1588 plan_index = plan_index,
1589 task_type = ?runner_compaction_kind,
1590 table = %runner_table,
1591 unique_ident = ?unique_ident,
1592 required_parallelism = popped_task.meta.required_parallelism,
1593 "iceberg_compaction_plan_started_from_queue",
1594 );
1595
1596 executor.spawn(async move {
1597 let _cleanup_guard = scopeguard::guard(shutdown_map_clone, move |shutdown_map| {
1598 let mut shutdown_guard = shutdown_map.lock().unwrap();
1599 shutdown_guard.remove(&task_key);
1600 });
1601
1602 let result = Box::pin(runner.compact(rx)).await;
1603
1604 let completion = match result {
1605 Ok((_stats, pk_index_result)) => IcebergPlanCompletion {
1606 task_key,
1607 error_message: None,
1608 pk_index_result,
1609 },
1610 Err(e) => {
1611 if is_cancelled_iceberg_compaction_error(&e) {
1612 tracing::info!(
1613 iceberg_component = "compaction_worker",
1614 iceberg_operation = "execute_plan",
1615 task_id = %task_key.0,
1616 sink_id = runner_sink_id,
1617 plan_index = task_key.1,
1618 task_type = ?runner_compaction_kind,
1619 table = %runner_table,
1620 "iceberg_compaction_plan_cancelled",
1621 );
1622 } else {
1623 tracing::warn!(
1624 iceberg_component = "compaction_worker",
1625 iceberg_operation = "execute_plan",
1626 error = %e.as_report(),
1627 task_id = %task_key.0,
1628 sink_id = runner_sink_id,
1629 plan_index = task_key.1,
1630 task_type = ?runner_compaction_kind,
1631 table = %runner_table,
1632 "iceberg_compaction_plan_failed",
1633 );
1634 }
1635 IcebergPlanCompletion {
1636 task_key,
1637 error_message: Some(e.to_report_string()),
1638 pk_index_result: None,
1639 }
1640 }
1641 };
1642
1643 if completion_tx_clone.send(completion).is_err() {
1644 tracing::warn!(
1645 iceberg_component = "compaction_worker",
1646 iceberg_operation = "notify_plan_completion",
1647 task_id = %task_key.0,
1648 sink_id = runner_sink_id,
1649 plan_index = task_key.1,
1650 task_type = ?runner_compaction_kind,
1651 table = %runner_table,
1652 "iceberg_compaction_plan_completion_send_failed",
1653 );
1654 }
1655 });
1656 }
1657}
1658
1659fn iceberg_compaction_max_plan_parallelism(
1662 worker_num: usize,
1663 task_parallelism_ratio: f32,
1664 max_task_parallelism: u32,
1665) -> u32 {
1666 ((worker_num as f32 * task_parallelism_ratio) as u32).min(max_task_parallelism)
1667}
1668
1669fn is_cancelled_iceberg_compaction_error(error: &crate::hummock::HummockError) -> bool {
1670 matches!(
1671 error.inner(),
1672 HummockErrorInner::CompactionExecutor(message) if message == "Plan cancelled"
1673 )
1674}
1675
1676fn cancel_iceberg_task(
1677 task_id: IcebergCompactionTaskId,
1678 task_queue: &mut IcebergTaskQueue,
1679 shutdown_map: &Arc<Mutex<HashMap<TaskKey, Sender<()>>>>,
1680 task_trackers: &mut HashMap<IcebergCompactionTaskId, IcebergTaskTracker>,
1681) {
1682 let cancelled_waiting = task_queue.cancel_waiting_task(task_id);
1687 let removed_tracker = task_trackers.remove(&task_id).is_some();
1688
1689 let cancelled_running = {
1690 let mut shutdown_guard = shutdown_map.lock().unwrap();
1691 let task_keys: Vec<_> = shutdown_guard
1692 .keys()
1693 .filter(|(running_task_id, _)| *running_task_id == task_id)
1694 .copied()
1695 .collect();
1696
1697 for task_key in &task_keys {
1698 if let Some(tx) = shutdown_guard.remove(task_key)
1699 && tx.send(()).is_err()
1700 {
1701 tracing::debug!(
1702 task_id = %task_key.0,
1703 plan_index = task_key.1,
1704 "Iceberg compaction plan shutdown receiver already closed during cancellation"
1705 );
1706 }
1707 }
1708
1709 task_keys.len()
1710 };
1711
1712 if cancelled_waiting == 0 && cancelled_running == 0 && !removed_tracker {
1713 tracing::warn!(
1714 task_id = %task_id,
1715 "Attempting to cancel non-existent iceberg compaction task"
1716 );
1717 } else {
1718 tracing::info!(
1719 task_id = %task_id,
1720 cancelled_waiting = cancelled_waiting,
1721 cancelled_running = cancelled_running,
1722 removed_tracker = removed_tracker,
1723 "Cancelled iceberg compaction task"
1724 );
1725 }
1726}
1727
1728fn handle_meta_task_pulling(
1731 pull_task_ack: &mut bool,
1732 task_queue: &IcebergTaskQueue,
1733 max_task_parallelism: u32,
1734 max_pull_task_count: u32,
1735 request_sender: &mpsc::UnboundedSender<SubscribeIcebergCompactionEventRequest>,
1736 log_throttler: &mut LogThrottler<IcebergCompactionLogState>,
1737) -> bool {
1738 let mut pending_pull_task_count = 0;
1739 if *pull_task_ack {
1740 let current_reserved_parallelism = task_queue
1745 .running_parallelism_sum()
1746 .saturating_add(task_queue.waiting_parallelism_sum());
1747 pending_pull_task_count = max_task_parallelism
1748 .saturating_sub(current_reserved_parallelism)
1749 .min(max_pull_task_count);
1750
1751 if pending_pull_task_count > 0 {
1752 if let Err(e) = request_sender.send(SubscribeIcebergCompactionEventRequest {
1753 event: Some(subscribe_iceberg_compaction_event_request::Event::PullTask(
1754 subscribe_iceberg_compaction_event_request::PullTask {
1755 pull_task_count: pending_pull_task_count,
1756 },
1757 )),
1758 create_at: SystemTime::now()
1759 .duration_since(std::time::UNIX_EPOCH)
1760 .expect("Clock may have gone backwards")
1761 .as_millis() as u64,
1762 }) {
1763 tracing::warn!(error = %e.as_report(), "Failed to pull task - will retry on stream restart");
1764 return true; } else {
1766 *pull_task_ack = false;
1767 }
1768 }
1769 }
1770
1771 let running_count = task_queue.running_parallelism_sum();
1772 let waiting_count = task_queue.waiting_parallelism_sum();
1773 let available_count = max_task_parallelism.saturating_sub(running_count);
1774 let current_state = IcebergCompactionLogState {
1775 running_parallelism: running_count,
1776 waiting_parallelism: waiting_count,
1777 available_parallelism: available_count,
1778 pull_task_ack: *pull_task_ack,
1779 pending_pull_task_count,
1780 };
1781
1782 if log_throttler.should_log(¤t_state) {
1784 tracing::info!(
1785 running_parallelism_count = %current_state.running_parallelism,
1786 waiting_parallelism_count = %current_state.waiting_parallelism,
1787 available_parallelism = %current_state.available_parallelism,
1788 pull_task_ack = %current_state.pull_task_ack,
1789 pending_pull_task_count = %current_state.pending_pull_task_count
1790 );
1791 log_throttler.update(current_state);
1792 }
1793
1794 false }
1796
1797#[cfg(test)]
1798mod tests {
1799 use super::*;
1800
1801 #[test]
1802 fn test_iceberg_compaction_max_plan_parallelism_fits_worker_capacity() {
1803 assert_eq!(iceberg_compaction_max_plan_parallelism(8, 4.0, 24), 24);
1805 assert_eq!(iceberg_compaction_max_plan_parallelism(8, 2.0, 24), 16);
1806 assert_eq!(iceberg_compaction_max_plan_parallelism(8, 1.0, 8), 8);
1807 }
1808
1809 #[test]
1810 fn test_cancel_iceberg_task_removes_waiting_plans_and_tracker() {
1811 let task_id = IcebergCompactionTaskId::new(42);
1812 let mut task_queue = IcebergTaskQueue::new(10, 30, 1024);
1813 assert_eq!(
1814 task_queue.push(
1815 iceberg_compaction::IcebergTaskMeta {
1816 task_id,
1817 plan_index: 0,
1818 required_parallelism: 3,
1819 memory_reservation_bytes: 64,
1820 },
1821 None,
1822 ),
1823 PushResult::Added
1824 );
1825 assert_eq!(
1826 task_queue.push(
1827 iceberg_compaction::IcebergTaskMeta {
1828 task_id,
1829 plan_index: 1,
1830 required_parallelism: 4,
1831 memory_reservation_bytes: 64,
1832 },
1833 None,
1834 ),
1835 PushResult::Added
1836 );
1837 assert_eq!(task_queue.waiting_parallelism_sum(), 7);
1838
1839 let shutdown_map = Arc::new(Mutex::new(HashMap::new()));
1840 let mut task_trackers =
1841 HashMap::from([(task_id, IcebergTaskTracker::new(10, 2, false, false))]);
1842
1843 cancel_iceberg_task(task_id, &mut task_queue, &shutdown_map, &mut task_trackers);
1844
1845 assert_eq!(task_queue.waiting_parallelism_sum(), 0);
1846 assert!(!task_trackers.contains_key(&task_id));
1847 }
1848
1849 #[test]
1850 fn test_cancel_iceberg_task_shuts_down_running_plans_and_tracker() {
1851 let task_id = IcebergCompactionTaskId::new(43);
1852 let task_key = (task_id, 0);
1853 let mut task_queue = IcebergTaskQueue::new(10, 30, 1024);
1854 let shutdown_map = Arc::new(Mutex::new(HashMap::new()));
1855 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
1856 shutdown_map.lock().unwrap().insert(task_key, shutdown_tx);
1857 let mut task_trackers =
1858 HashMap::from([(task_id, IcebergTaskTracker::new(10, 1, false, false))]);
1859
1860 cancel_iceberg_task(task_id, &mut task_queue, &shutdown_map, &mut task_trackers);
1861
1862 assert!(shutdown_rx.try_recv().is_ok());
1863 assert!(shutdown_map.lock().unwrap().is_empty());
1864 assert!(!task_trackers.contains_key(&task_id));
1865 }
1866
1867 #[test]
1868 fn test_log_state_equality() {
1869 let state1 = CompactionLogState {
1871 running_parallelism: 10,
1872 pull_task_ack: true,
1873 pending_pull_task_count: 2,
1874 };
1875 let state2 = CompactionLogState {
1876 running_parallelism: 10,
1877 pull_task_ack: true,
1878 pending_pull_task_count: 2,
1879 };
1880 let state3 = CompactionLogState {
1881 running_parallelism: 11,
1882 pull_task_ack: true,
1883 pending_pull_task_count: 2,
1884 };
1885 assert_eq!(state1, state2);
1886 assert_ne!(state1, state3);
1887
1888 let ice_state1 = IcebergCompactionLogState {
1890 running_parallelism: 10,
1891 waiting_parallelism: 5,
1892 available_parallelism: 15,
1893 pull_task_ack: true,
1894 pending_pull_task_count: 2,
1895 };
1896 let ice_state2 = IcebergCompactionLogState {
1897 running_parallelism: 10,
1898 waiting_parallelism: 6,
1899 available_parallelism: 15,
1900 pull_task_ack: true,
1901 pending_pull_task_count: 2,
1902 };
1903 assert_ne!(ice_state1, ice_state2);
1904 }
1905
1906 #[test]
1907 fn test_log_throttler_state_change_detection() {
1908 let mut throttler = LogThrottler::<CompactionLogState>::new(Duration::from_secs(60));
1909 let state1 = CompactionLogState {
1910 running_parallelism: 10,
1911 pull_task_ack: true,
1912 pending_pull_task_count: 2,
1913 };
1914 let state2 = CompactionLogState {
1915 running_parallelism: 11,
1916 pull_task_ack: true,
1917 pending_pull_task_count: 2,
1918 };
1919
1920 assert!(throttler.should_log(&state1));
1922 throttler.update(state1.clone());
1923
1924 assert!(!throttler.should_log(&state1));
1926
1927 assert!(throttler.should_log(&state2));
1929 throttler.update(state2.clone());
1930
1931 assert!(!throttler.should_log(&state2));
1933 }
1934
1935 #[test]
1936 fn test_log_throttler_heartbeat() {
1937 let mut throttler = LogThrottler::<CompactionLogState>::new(Duration::from_millis(10));
1938 let state = CompactionLogState {
1939 running_parallelism: 10,
1940 pull_task_ack: true,
1941 pending_pull_task_count: 2,
1942 };
1943
1944 assert!(throttler.should_log(&state));
1946 throttler.update(state.clone());
1947
1948 assert!(!throttler.should_log(&state));
1950
1951 std::thread::sleep(Duration::from_millis(15));
1953
1954 assert!(throttler.should_log(&state));
1956 }
1957}