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