1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::ops::DerefMut;
17use std::sync::{Arc, LazyLock};
18use std::time::Instant;
19
20use anyhow::Context;
21use compaction_event_loop::{
22 HummockCompactionEventDispatcher, HummockCompactionEventHandler, HummockCompactionEventLoop,
23 HummockCompactorDedicatedEventLoop,
24};
25use fail::fail_point;
26use itertools::Itertools;
27use parking_lot::Mutex;
28use rand::rng as thread_rng;
29use rand::seq::SliceRandom;
30use risingwave_common::catalog::TableId;
31use risingwave_common::config::meta::default::compaction_config;
32use risingwave_common::util::epoch::Epoch;
33use risingwave_hummock_sdk::compact_task::{CompactTask, CompactTaskAssignment, ReportTask};
34use risingwave_hummock_sdk::compaction_group::StateTableId;
35use risingwave_hummock_sdk::compaction_group::hummock_version_ext::safe_epoch_table_watermarks_impl;
36use risingwave_hummock_sdk::level::Levels;
37use risingwave_hummock_sdk::sstable_info::SstableInfo;
38use risingwave_hummock_sdk::table_stats::{
39 PbTableStatsMap, add_prost_table_stats_map, purge_prost_table_stats,
40};
41use risingwave_hummock_sdk::table_watermark::TableWatermarks;
42use risingwave_hummock_sdk::version::{GroupDelta, IntraLevelDelta};
43use risingwave_hummock_sdk::{
44 CompactionGroupId, HummockCompactionTaskId, HummockContextId, HummockSstableId,
45 HummockSstableObjectId, HummockVersionId, compact_task_to_string, statistics_compact_task,
46};
47use risingwave_meta_model::hummock_sequence::COMPACTION_TASK_ID;
48use risingwave_pb::hummock::compact_task::{TaskStatus, TaskType};
49use risingwave_pb::hummock::subscribe_compaction_event_response::Event as ResponseEvent;
50use risingwave_pb::hummock::{
51 CompactTaskAssignment as PbCompactTaskAssignment, CompactionConfig, PbCompactStatus,
52 SubscribeCompactionEventRequest, TableOption, compact_task,
53};
54use thiserror_ext::AsReport;
55use tokio::sync::mpsc::UnboundedReceiver;
56use tokio::sync::oneshot::{Receiver, Sender};
57use tokio::task::JoinHandle;
58use tonic::Streaming;
59use tracing::warn;
60
61use crate::hummock::compaction::in_progress_compaction::InProgressCompactionView;
62use crate::hummock::compaction::selector::level_selector::PickerInfo;
63use crate::hummock::compaction::selector::{
64 DynamicLevelSelector, DynamicLevelSelectorCore, LocalSelectorStatistic, ManualCompactionOption,
65 ManualCompactionSelector, SpaceReclaimCompactionSelector, TombstoneCompactionSelector,
66 TtlCompactionSelector, VnodeWatermarkCompactionSelector,
67};
68use crate::hummock::compaction::{
69 CompactStatus, CompactionDeveloperConfig, CompactionSelector,
70 CompactionTask as PickedCompactionTask,
71};
72use crate::hummock::error::{Error, Result};
73use crate::hummock::manager::CompactionTaskReportResult;
74use crate::hummock::manager::compaction::compact_task_builder::{
75 CompactTaskBuildContext, attach_compact_task_table_metadata, build_base_compact_task,
76};
77use crate::hummock::manager::transaction::{
78 HummockVersionStatsTransaction, HummockVersionTransaction,
79};
80use crate::hummock::manager::versioning::Versioning;
81use crate::hummock::metrics_utils::{
82 build_compact_task_level_type_metrics_label, trigger_compact_tasks_stat,
83 trigger_local_table_stat,
84};
85use crate::hummock::model::CompactionGroup;
86use crate::hummock::{HummockManager, commit_multi_var};
87use crate::manager::META_NODE_ID;
88use crate::model::BTreeMapTransaction;
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum ManualCompactionTriggerResult {
92 Submitted,
93 Retry,
94}
95
96mod compact_task_builder;
97pub mod compaction_event_loop;
98pub mod compaction_group_manager;
99pub mod compaction_group_schedule;
100
101static CANCEL_STATUS_SET: LazyLock<HashSet<TaskStatus>> = LazyLock::new(|| {
102 [
103 TaskStatus::ManualCanceled,
104 TaskStatus::SendFailCanceled,
105 TaskStatus::AssignFailCanceled,
106 TaskStatus::HeartbeatCanceled,
107 TaskStatus::InvalidGroupCanceled,
108 TaskStatus::NoAvailMemoryResourceCanceled,
109 TaskStatus::NoAvailCpuResourceCanceled,
110 TaskStatus::HeartbeatProgressCanceled,
111 ]
112 .into_iter()
113 .collect()
114});
115
116fn init_selectors() -> HashMap<compact_task::TaskType, Box<dyn CompactionSelector>> {
117 let mut compaction_selectors: HashMap<compact_task::TaskType, Box<dyn CompactionSelector>> =
118 HashMap::default();
119 compaction_selectors.insert(
120 compact_task::TaskType::Dynamic,
121 Box::<DynamicLevelSelector>::default(),
122 );
123 compaction_selectors.insert(
124 compact_task::TaskType::SpaceReclaim,
125 Box::<SpaceReclaimCompactionSelector>::default(),
126 );
127 compaction_selectors.insert(
128 compact_task::TaskType::Ttl,
129 Box::<TtlCompactionSelector>::default(),
130 );
131 compaction_selectors.insert(
132 compact_task::TaskType::Tombstone,
133 Box::<TombstoneCompactionSelector>::default(),
134 );
135 compaction_selectors.insert(
136 compact_task::TaskType::VnodeWatermark,
137 Box::<VnodeWatermarkCompactionSelector>::default(),
138 );
139 compaction_selectors
140}
141
142enum BuiltCompactTask {
143 MetaFinished(CompactTask),
144 PendingAssignment(CompactTask),
145}
146
147impl HummockVersionTransaction<'_> {
148 fn apply_compact_task(&mut self, compact_task: &CompactTask) {
149 let mut version_delta = self.new_delta();
150 let trivial_move = compact_task.is_trivial_move_task();
151 version_delta.trivial_move = trivial_move;
152
153 let group_deltas = &mut version_delta
154 .group_deltas
155 .entry(compact_task.compaction_group_id)
156 .or_default()
157 .group_deltas;
158 let mut removed_table_ids_map: BTreeMap<u32, HashSet<HummockSstableId>> =
159 BTreeMap::default();
160
161 for level in &compact_task.input_ssts {
162 let level_idx = level.level_idx;
163
164 removed_table_ids_map
165 .entry(level_idx)
166 .or_default()
167 .extend(level.table_infos.iter().map(|sst| sst.sst_id));
168 }
169
170 for (level_idx, removed_table_ids) in removed_table_ids_map {
171 let group_delta = GroupDelta::IntraLevel(IntraLevelDelta::new(
172 level_idx,
173 0, removed_table_ids,
175 vec![], 0, compact_task.compaction_group_version_id,
178 ));
179
180 group_deltas.push(group_delta);
181 }
182
183 let group_delta = GroupDelta::IntraLevel(IntraLevelDelta::new(
184 compact_task.target_level,
185 compact_task.target_sub_level_id,
186 HashSet::new(), compact_task.sorted_output_ssts.clone(),
188 compact_task.split_weight_by_vnode,
189 compact_task.compaction_group_version_id,
190 ));
191
192 group_deltas.push(group_delta);
193 version_delta.pre_apply();
194 }
195}
196
197#[derive(Default)]
198pub struct Compaction {
199 pub compact_task_assignment: BTreeMap<HummockCompactionTaskId, CompactTaskAssignment>,
201 pub compaction_statuses: BTreeMap<CompactionGroupId, CompactStatus>,
203
204 pub _deterministic_mode: bool,
205}
206
207impl HummockManager {
208 pub async fn get_assigned_compact_task_num(&self) -> u64 {
209 self.compaction.read().await.compact_task_assignment.len() as u64
210 }
211
212 pub async fn list_compaction_status(
213 &self,
214 ) -> (Vec<PbCompactStatus>, Vec<PbCompactTaskAssignment>) {
215 let (compaction_statuses, compact_task_assignments) = {
216 let compaction = self.compaction.read().await;
217 (
218 compaction
219 .compaction_statuses
220 .values()
221 .map_into()
222 .collect_vec(),
223 compaction
224 .compact_task_assignment
225 .values()
226 .cloned()
227 .collect_vec(),
228 )
229 };
230
231 (
232 compaction_statuses,
233 compact_task_assignments
234 .into_iter()
235 .map(PbCompactTaskAssignment::from)
236 .collect(),
237 )
238 }
239
240 pub async fn get_compaction_scores(
241 &self,
242 compaction_group_id: CompactionGroupId,
243 ) -> Vec<PickerInfo> {
244 let (status, levels, group) = {
245 let compaction = self.compaction.read().await;
246 let versioning = self.versioning.read().await;
247 let config_manager = self.compaction_group_manager.read().await;
248 match (
249 compaction.compaction_statuses.get(&compaction_group_id),
250 versioning.current_version.levels.get(&compaction_group_id),
251 config_manager.try_get_compaction_group_config(compaction_group_id),
252 ) {
253 (Some(cs), Some(v), Some(cf)) => (cs.to_owned(), v.to_owned(), cf),
254 _ => {
255 return vec![];
256 }
257 }
258 };
259 let dynamic_level_core = DynamicLevelSelectorCore::new(
260 group.compaction_config,
261 Arc::new(CompactionDeveloperConfig::default()),
262 );
263 let ctx = dynamic_level_core.get_priority_levels(&levels, &status.level_handlers);
264 ctx.score_levels
265 }
266}
267
268impl HummockManager {
269 pub fn compaction_event_loop(
270 hummock_manager: Arc<Self>,
271 compactor_streams_change_rx: UnboundedReceiver<(
272 HummockContextId,
273 Streaming<SubscribeCompactionEventRequest>,
274 )>,
275 ) -> Vec<(JoinHandle<()>, Sender<()>)> {
276 let mut join_handle_vec = Vec::default();
277
278 let hummock_compaction_event_handler =
279 HummockCompactionEventHandler::new(hummock_manager.clone());
280
281 let dedicated_event_loop = HummockCompactorDedicatedEventLoop::new(
282 hummock_manager.clone(),
283 hummock_compaction_event_handler.clone(),
284 );
285
286 let (dedicated_event_loop_join_handle, event_tx, shutdown_tx) = dedicated_event_loop.run();
287 join_handle_vec.push((dedicated_event_loop_join_handle, shutdown_tx));
288
289 let hummock_compaction_event_dispatcher = HummockCompactionEventDispatcher::new(
290 hummock_manager.env.opts.clone(),
291 hummock_compaction_event_handler,
292 Some(event_tx),
293 );
294
295 let event_loop = HummockCompactionEventLoop::new(
296 hummock_compaction_event_dispatcher,
297 hummock_manager.metrics.clone(),
298 compactor_streams_change_rx,
299 );
300
301 let (event_loop_join_handle, event_loop_shutdown_tx) = event_loop.run();
302 join_handle_vec.push((event_loop_join_handle, event_loop_shutdown_tx));
303
304 join_handle_vec
305 }
306
307 pub fn add_compactor_stream(
308 &self,
309 context_id: HummockContextId,
310 req_stream: Streaming<SubscribeCompactionEventRequest>,
311 ) {
312 self.compactor_streams_change_tx
313 .send((context_id, req_stream))
314 .unwrap();
315 }
316}
317
318impl HummockManager {
319 async fn next_compaction_task_id_with_prefetch(&self, refill_capacity: u32) -> Result<u64> {
322 self.prefetched_compaction_task_ids
323 .next(refill_capacity, |count| async move {
324 self.env
325 .hummock_seq
326 .next_interval(COMPACTION_TASK_ID, count)
327 .await
328 })
329 .await
330 }
331
332 pub async fn get_compact_tasks_impl(
333 &self,
334 compaction_groups: Vec<CompactionGroupId>,
335 max_select_count: usize,
336 selector: &mut dyn CompactionSelector,
337 ) -> Result<(Vec<CompactTask>, Vec<CompactionGroupId>)> {
338 let deterministic_mode = self.env.opts.compaction_deterministic_test;
339
340 let mut compaction_guard = self
341 .compaction
342 .write_with_process_name("get_compact_tasks_impl")
343 .await;
344 let mut versioning_guard = self
345 .versioning
346 .write_with_process_name("get_compact_tasks_impl")
347 .await;
348 let compaction: &mut Compaction = &mut compaction_guard;
349 let versioning: &mut Versioning = &mut versioning_guard;
350
351 let start_time = Instant::now();
352 let mut compaction_statuses = BTreeMapTransaction::new(&mut compaction.compaction_statuses);
353
354 let mut compact_task_assignment =
355 BTreeMapTransaction::new(&mut compaction.compact_task_assignment);
356
357 let mut version = HummockVersionTransaction::new(
358 &mut versioning.current_version,
359 &mut versioning.hummock_version_deltas,
360 &mut versioning.table_change_log,
361 self.env.notification_manager(),
362 None,
363 &self.metrics,
364 &self.env.opts,
365 &self.version_stat_tx,
366 );
367 let mut version_stats = HummockVersionStatsTransaction::new(
369 &mut versioning.version_stats,
370 self.env.notification_manager(),
371 );
372
373 if deterministic_mode {
374 version.disable_apply_to_txn();
375 }
376 let all_versioned_table_schemas = if self.env.opts.enable_dropped_column_reclaim {
377 self.metadata_manager
378 .catalog_controller
379 .get_versioned_table_schemas()
380 .await
381 .map_err(|e| Error::Internal(e.into()))?
382 } else {
383 HashMap::default()
384 };
385 let mut unschedule_groups = vec![];
386 let mut trivial_tasks = vec![];
387 let mut pick_tasks = vec![];
388 let developer_config = Arc::new(CompactionDeveloperConfig::new_from_meta_opts(
389 &self.env.opts,
390 ));
391 'outside: for compaction_group_id in compaction_groups {
396 if pick_tasks.len() >= max_select_count {
397 break;
398 }
399
400 if !version
401 .latest_version()
402 .levels
403 .contains_key(&compaction_group_id)
404 {
405 continue;
406 }
407
408 let group_config = {
412 let config_manager = self.compaction_group_manager.read().await;
413
414 match config_manager.try_get_compaction_group_config(compaction_group_id) {
415 Some(config) => config,
416 None => continue,
417 }
418 };
419
420 let task_id = self
422 .next_compaction_task_id_with_prefetch(
423 self.env.opts.compaction_task_id_refill_capacity,
424 )
425 .await?;
426
427 if !compaction_statuses.contains_key(&compaction_group_id) {
428 compaction_statuses.insert(
430 compaction_group_id,
431 CompactStatus::new(
432 compaction_group_id,
433 group_config.compaction_config.max_level,
434 ),
435 );
436 }
437 let mut compact_status = compaction_statuses.get_mut(compaction_group_id).unwrap();
438
439 let mut stats = LocalSelectorStatistic::default();
440 let member_table_ids: Vec<_> = version
441 .latest_version()
442 .state_table_info
443 .compaction_group_member_table_ids(compaction_group_id)
444 .iter()
445 .copied()
446 .collect();
447
448 let mut table_id_to_option: HashMap<TableId, _> = HashMap::default();
449
450 {
451 let guard = self.table_id_to_table_option.read();
452 for table_id in &member_table_ids {
453 if let Some(opts) = guard.get(table_id) {
454 table_id_to_option.insert(*table_id, *opts);
455 }
456 }
457 }
458
459 let in_progress_compactions = InProgressCompactionView::for_group(
460 compact_task_assignment.tree_ref().values(),
461 compaction_group_id,
462 );
463
464 while let Some(picked_task) = compact_status.get_compact_task(
465 version
466 .latest_version()
467 .get_compaction_group_levels(compaction_group_id),
468 version
469 .latest_version()
470 .state_table_info
471 .compaction_group_member_table_ids(compaction_group_id),
472 task_id as HummockCompactionTaskId,
473 &group_config,
474 &mut stats,
475 selector,
476 &table_id_to_option,
477 developer_config.clone(),
478 &version.latest_version().table_watermarks,
479 &version.latest_version().state_table_info,
480 &in_progress_compactions,
481 ) {
482 let compaction_group_levels = version
483 .latest_version()
484 .get_compaction_group_levels(compaction_group_id);
485 let target_level_id = picked_task.input.target_level as u32;
486 let is_target_level_last = compaction_group_levels.is_last_level(target_level_id);
487 let table_options = table_id_to_option
488 .iter()
489 .map(|(table_id, table_option)| (*table_id, TableOption::from(table_option)))
490 .collect();
491 let built_compact_task = self.build_ready_compact_task(
492 picked_task,
493 CompactTaskBuildContext {
494 task_id,
495 compaction_group_id: group_config.group_id,
496 compaction_group_version_id: compaction_group_levels
497 .compaction_group_version_id,
498 existing_table_ids: member_table_ids.clone(),
499 table_options,
500 is_target_level_last,
501 compaction_config: group_config.compaction_config.clone(),
502 current_epoch_time: Epoch::now().0,
503 },
504 &version.latest_version().table_watermarks,
505 &all_versioned_table_schemas,
506 );
507
508 match built_compact_task {
509 BuiltCompactTask::MetaFinished(compact_task) => {
510 let label = compact_task.task_label();
511 tracing::debug!(
512 "{} for compaction group {}: input: {:?}, cost time: {:?}",
513 label,
514 compact_task.compaction_group_id,
515 compact_task.input_ssts,
516 start_time.elapsed()
517 );
518 compact_status.report_compact_task(&compact_task);
519 update_table_stats_for_vnode_watermark_trivial_reclaim(
520 &mut version_stats.table_stats,
521 &compact_task,
522 );
523 self.metrics
524 .compact_frequency
525 .with_label_values(&[
526 label,
527 &compact_task.compaction_group_id.to_string(),
528 selector.task_type().as_str_name(),
529 "SUCCESS",
530 ])
531 .inc();
532
533 version.apply_compact_task(&compact_task);
534 trivial_tasks.push(compact_task);
535 if trivial_tasks.len() >= self.env.opts.max_trivial_move_task_count_per_loop
536 {
537 break 'outside;
538 }
539 }
540 BuiltCompactTask::PendingAssignment(compact_task) => {
541 compact_task_assignment.insert(
542 compact_task.task_id,
543 CompactTaskAssignment {
544 compact_task: compact_task.clone(),
545 context_id: META_NODE_ID, },
547 );
548
549 pick_tasks.push(compact_task);
550 break;
551 }
552 }
553
554 stats.report_to_metrics(compaction_group_id, self.metrics.as_ref());
555 stats = LocalSelectorStatistic::default();
556 }
557 if pick_tasks
558 .last()
559 .map(|task| task.compaction_group_id != compaction_group_id)
560 .unwrap_or(true)
561 {
562 unschedule_groups.push(compaction_group_id);
563 }
564 stats.report_to_metrics(compaction_group_id, self.metrics.as_ref());
565 }
566
567 if !trivial_tasks.is_empty() {
568 commit_multi_var!(
569 self.meta_store_ref(),
570 compaction_statuses,
571 compact_task_assignment,
572 version,
573 version_stats
574 )?;
575 self.metrics
576 .compact_task_batch_count
577 .with_label_values(&["batch_trivial_move"])
578 .observe(trivial_tasks.len() as f64);
579
580 for trivial_task in &trivial_tasks {
581 self.metrics
582 .compact_task_trivial_move_sst_count
583 .with_label_values(&[&trivial_task.compaction_group_id.to_string()])
584 .observe(trivial_task.input_ssts[0].table_infos.len() as _);
585 }
586
587 drop(versioning_guard);
588 } else {
589 drop(versioning_guard);
592 commit_multi_var!(
593 self.meta_store_ref(),
594 compaction_statuses,
595 compact_task_assignment
596 )?;
597 }
598 drop(compaction_guard);
599 if !pick_tasks.is_empty() {
600 self.metrics
601 .compact_task_batch_count
602 .with_label_values(&["batch_get_compact_task"])
603 .observe(pick_tasks.len() as f64);
604 }
605
606 for compact_task in &mut pick_tasks {
607 let compaction_group_id = compact_task.compaction_group_id;
608
609 self.compactor_manager
611 .initiate_task_heartbeat(compact_task.clone());
612
613 compact_task.task_status = TaskStatus::Pending;
615 let compact_task_statistics = statistics_compact_task(compact_task);
616
617 let level_type_label = build_compact_task_level_type_metrics_label(
618 compact_task.input_ssts[0].level_idx as usize,
619 compact_task.input_ssts.last().unwrap().level_idx as usize,
620 );
621
622 let level_count = compact_task.input_ssts.len();
623 if compact_task.input_ssts[0].level_idx == 0 {
624 self.metrics
625 .l0_compact_level_count
626 .with_label_values(&[&compaction_group_id.to_string(), &level_type_label])
627 .observe(level_count as _);
628 }
629
630 self.metrics
631 .compact_task_size
632 .with_label_values(&[&compaction_group_id.to_string(), &level_type_label])
633 .observe(compact_task_statistics.total_file_size as _);
634
635 self.metrics
636 .compact_task_size
637 .with_label_values(&[
638 &compaction_group_id.to_string(),
639 &format!("{} uncompressed", level_type_label),
640 ])
641 .observe(compact_task_statistics.total_uncompressed_file_size as _);
642
643 self.metrics
644 .compact_task_file_count
645 .with_label_values(&[&compaction_group_id.to_string(), &level_type_label])
646 .observe(compact_task_statistics.total_file_count as _);
647
648 tracing::trace!(
649 "For compaction group {}: pick up {} {} sub_level in level {} to compact to target {}. cost time: {:?} compact_task_statistics {:?}",
650 compaction_group_id,
651 level_count,
652 compact_task.input_ssts[0].level_type.as_str_name(),
653 compact_task.input_ssts[0].level_idx,
654 compact_task.target_level,
655 start_time.elapsed(),
656 compact_task_statistics
657 );
658 }
659
660 #[cfg(test)]
661 {
662 self.check_state_consistency().await;
663 }
664 pick_tasks.extend(trivial_tasks);
665 Ok((pick_tasks, unschedule_groups))
666 }
667
668 pub async fn cancel_compact_task(&self, task_id: u64, task_status: TaskStatus) -> Result<bool> {
670 fail_point!("fp_cancel_compact_task", |_| Err(Error::MetaStore(
671 anyhow::anyhow!("failpoint metastore err")
672 )));
673 let ret = self
674 .cancel_compact_task_impl(vec![task_id], task_status)
675 .await?;
676 Ok(ret[0])
677 }
678
679 pub async fn cancel_compact_tasks(
680 &self,
681 tasks: Vec<u64>,
682 task_status: TaskStatus,
683 ) -> Result<Vec<bool>> {
684 self.cancel_compact_task_impl(tasks, task_status).await
685 }
686
687 async fn cancel_compact_task_impl(
688 &self,
689 task_ids: Vec<u64>,
690 task_status: TaskStatus,
691 ) -> Result<Vec<bool>> {
692 assert!(CANCEL_STATUS_SET.contains(&task_status));
693 let tasks = task_ids
694 .into_iter()
695 .map(|task_id| ReportTask {
696 task_id,
697 task_status,
698 sorted_output_ssts: vec![],
699 table_stats_change: HashMap::default(),
700 object_timestamps: HashMap::default(),
701 })
702 .collect_vec();
703 let rets = self.report_compact_tasks(tasks).await?;
704 #[cfg(test)]
705 {
706 self.check_state_consistency().await;
707 }
708 Ok(rets)
709 }
710
711 async fn get_compact_tasks(
712 &self,
713 mut compaction_groups: Vec<CompactionGroupId>,
714 max_select_count: usize,
715 selector: &mut dyn CompactionSelector,
716 ) -> Result<(Vec<CompactTask>, Vec<CompactionGroupId>)> {
717 fail_point!("fp_get_compact_task", |_| Err(Error::MetaStore(
718 anyhow::anyhow!("failpoint metastore error")
719 )));
720 compaction_groups.shuffle(&mut thread_rng());
721 let (mut tasks, groups) = self
722 .get_compact_tasks_impl(compaction_groups, max_select_count, selector)
723 .await?;
724 tasks.retain(|task| {
725 if task.task_status == TaskStatus::Success {
726 debug_assert!(task.is_trivial_reclaim() || task.is_trivial_move_task());
727 false
728 } else {
729 true
730 }
731 });
732 Ok((tasks, groups))
733 }
734
735 pub async fn get_compact_task(
736 &self,
737 compaction_group_id: CompactionGroupId,
738 selector: &mut dyn CompactionSelector,
739 ) -> Result<Option<CompactTask>> {
740 fail_point!("fp_get_compact_task", |_| Err(Error::MetaStore(
741 anyhow::anyhow!("failpoint metastore error")
742 )));
743
744 let (normal_tasks, _) = self
745 .get_compact_tasks_impl(vec![compaction_group_id], 1, selector)
746 .await?;
747 for task in normal_tasks {
748 if task.task_status != TaskStatus::Success {
749 return Ok(Some(task));
750 }
751 debug_assert!(task.is_trivial_reclaim() || task.is_trivial_move_task());
752 }
753 Ok(None)
754 }
755
756 pub async fn manual_get_compact_task(
757 &self,
758 compaction_group_id: CompactionGroupId,
759 manual_compaction_option: ManualCompactionOption,
760 ) -> Result<Option<CompactTask>> {
761 let (task, _) = self
762 .manual_get_compact_task_with_info(compaction_group_id, manual_compaction_option)
763 .await?;
764 Ok(task)
765 }
766
767 pub async fn manual_get_compact_task_with_info(
768 &self,
769 compaction_group_id: CompactionGroupId,
770 manual_compaction_option: ManualCompactionOption,
771 ) -> Result<(Option<CompactTask>, bool)> {
772 let mut selector = ManualCompactionSelector::new(manual_compaction_option);
773 let task = self
774 .get_compact_task(compaction_group_id, &mut selector)
775 .await?;
776 if let Some(err) = selector.validation_error() {
777 return Err(Error::InvalidManualCompactionOption(err.to_owned()));
778 }
779 Ok((task, selector.blocked_by_pending()))
780 }
781
782 pub async fn report_compact_task(
783 &self,
784 task_id: u64,
785 task_status: TaskStatus,
786 sorted_output_ssts: Vec<SstableInfo>,
787 table_stats_change: Option<PbTableStatsMap>,
788 object_timestamps: HashMap<HummockSstableObjectId, u64>,
789 ) -> Result<bool> {
790 let rets = self
791 .report_compact_tasks(vec![ReportTask {
792 task_id,
793 task_status,
794 sorted_output_ssts,
795 table_stats_change: table_stats_change.unwrap_or_default(),
796 object_timestamps,
797 }])
798 .await?;
799 Ok(rets[0])
800 }
801
802 pub async fn report_compact_tasks(&self, report_tasks: Vec<ReportTask>) -> Result<Vec<bool>> {
803 let compaction_guard = self
804 .compaction
805 .write_with_process_name("report_compact_tasks")
806 .await;
807 let versioning_guard = self
808 .versioning
809 .write_with_process_name("report_compact_tasks")
810 .await;
811
812 self.report_compact_tasks_impl(report_tasks, compaction_guard, versioning_guard)
813 .await
814 }
815
816 pub async fn report_compact_tasks_impl(
824 &self,
825 report_tasks: Vec<ReportTask>,
826 mut compaction_guard: impl DerefMut<Target = Compaction>,
827 mut versioning_guard: impl DerefMut<Target = Versioning>,
828 ) -> Result<Vec<bool>> {
829 let deterministic_mode = self.env.opts.compaction_deterministic_test;
830 let compaction: &mut Compaction = &mut compaction_guard;
831 let start_time = Instant::now();
832 let original_keys = compaction.compaction_statuses.keys().cloned().collect_vec();
833 let mut compact_statuses = BTreeMapTransaction::new(&mut compaction.compaction_statuses);
834 let mut rets = vec![false; report_tasks.len()];
835 let mut compact_task_assignment =
836 BTreeMapTransaction::new(&mut compaction.compact_task_assignment);
837 let versioning: &mut Versioning = &mut versioning_guard;
839
840 for group_id in original_keys {
842 if !versioning.current_version.levels.contains_key(&group_id) {
843 compact_statuses.remove(group_id);
844 }
845 }
846 let mut tasks = vec![];
847
848 let mut version = HummockVersionTransaction::new(
849 &mut versioning.current_version,
850 &mut versioning.hummock_version_deltas,
851 &mut versioning.table_change_log,
852 self.env.notification_manager(),
853 None,
854 &self.metrics,
855 &self.env.opts,
856 &self.version_stat_tx,
857 );
858
859 if deterministic_mode {
860 version.disable_apply_to_txn();
861 }
862
863 let mut version_stats = HummockVersionStatsTransaction::new(
864 &mut versioning.version_stats,
865 self.env.notification_manager(),
866 );
867 let mut success_count = 0;
868 let mut report_results = Vec::with_capacity(rets.len());
869 for (idx, task) in report_tasks.into_iter().enumerate() {
870 rets[idx] = true;
871 let task_id = task.task_id;
872 let mut task_status = task.task_status;
873 let mut compact_task = match compact_task_assignment.remove(task.task_id) {
874 Some(compact_task_assignment) => compact_task_assignment.compact_task,
875 None => {
876 tracing::warn!("{}", format!("compact task {} not found", task.task_id));
877 rets[idx] = false;
878 report_results.push(CompactionTaskReportResult {
879 task_id,
880 task_status,
881 reported: false,
882 });
883 continue;
884 }
885 };
886
887 {
888 compact_task.task_status = task.task_status;
890 compact_task.sorted_output_ssts = task.sorted_output_ssts;
891 }
892
893 match compact_statuses.get_mut(compact_task.compaction_group_id) {
894 Some(mut compact_status) => {
895 compact_status.report_compact_task(&compact_task);
896 }
897 None => {
898 compact_task.task_status = TaskStatus::InvalidGroupCanceled;
904 }
905 }
906
907 let is_success = if let TaskStatus::Success = compact_task.task_status {
908 match self
909 .report_compaction_sanity_check(&task.object_timestamps)
910 .await
911 {
912 Err(e) => {
913 warn!(
914 "failed to commit compaction task {} {}",
915 compact_task.task_id,
916 e.as_report()
917 );
918 compact_task.task_status = TaskStatus::RetentionTimeRejected;
919 false
920 }
921 _ => {
922 let group = version
923 .latest_version()
924 .levels
925 .get(&compact_task.compaction_group_id)
926 .unwrap();
927 let is_expired = compact_task.is_expired(group.compaction_group_version_id);
928 if is_expired {
929 compact_task.task_status = TaskStatus::InputOutdatedCanceled;
930 warn!(
931 "The task may be expired because of group split, task:\n {:?}",
932 compact_task_to_string(&compact_task)
933 );
934 }
935 !is_expired
936 }
937 }
938 } else {
939 false
940 };
941 if is_success {
942 success_count += 1;
943 version.apply_compact_task(&compact_task);
944 if purge_prost_table_stats(
945 &mut version_stats.table_stats,
946 version.latest_version(),
947 &HashSet::default(),
948 ) {
949 self.metrics.version_stats.reset();
950 versioning.local_metrics.clear();
951 }
952 add_prost_table_stats_map(&mut version_stats.table_stats, &task.table_stats_change);
953 trigger_local_table_stat(
954 &self.metrics,
955 &mut versioning.local_metrics,
956 &version_stats,
957 &task.table_stats_change,
958 );
959 }
960 task_status = compact_task.task_status;
961 report_results.push(CompactionTaskReportResult {
962 task_id,
963 task_status,
964 reported: rets[idx],
965 });
966 tasks.push(compact_task);
967 }
968 if success_count > 0 {
969 commit_multi_var!(
970 self.meta_store_ref(),
971 compact_statuses,
972 compact_task_assignment,
973 version,
974 version_stats
975 )?;
976
977 self.metrics
978 .compact_task_batch_count
979 .with_label_values(&["batch_report_task"])
980 .observe(success_count as f64);
981 } else {
982 commit_multi_var!(
984 self.meta_store_ref(),
985 compact_statuses,
986 compact_task_assignment
987 )?;
988 }
989
990 self.notify_compaction_task_report_waiters(report_results);
991
992 let mut success_groups = vec![];
993 for compact_task in &tasks {
994 self.compactor_manager
995 .remove_task_heartbeat(compact_task.task_id);
996 tracing::trace!(
997 "Reported compaction task. {}. cost time: {:?}",
998 compact_task_to_string(compact_task),
999 start_time.elapsed(),
1000 );
1001
1002 if !deterministic_mode
1003 && (matches!(compact_task.task_type, compact_task::TaskType::Dynamic)
1004 || matches!(compact_task.task_type, compact_task::TaskType::Emergency))
1005 {
1006 self.try_send_compaction_request(
1008 compact_task.compaction_group_id,
1009 compact_task::TaskType::Dynamic,
1010 );
1011 }
1012
1013 if compact_task.task_status == TaskStatus::Success {
1014 success_groups.push(compact_task.compaction_group_id);
1015 }
1016 }
1017
1018 trigger_compact_tasks_stat(
1019 &self.metrics,
1020 &tasks,
1021 &compaction.compaction_statuses,
1022 &versioning_guard.current_version,
1023 );
1024 drop(versioning_guard);
1025 if !success_groups.is_empty() {
1026 self.try_update_write_limits(&success_groups).await;
1027 }
1028 Ok(rets)
1029 }
1030
1031 pub async fn trigger_compaction_deterministic(
1034 &self,
1035 _base_version_id: HummockVersionId,
1036 compaction_groups: Vec<CompactionGroupId>,
1037 ) -> Result<()> {
1038 self.on_current_version(|old_version| {
1039 tracing::info!(
1040 "Trigger compaction for version {}, groups {:?}",
1041 old_version.id,
1042 compaction_groups
1043 );
1044 })
1045 .await;
1046
1047 if compaction_groups.is_empty() {
1048 return Ok(());
1049 }
1050 for compaction_group in compaction_groups {
1051 self.try_send_compaction_request(compaction_group, compact_task::TaskType::Dynamic);
1052 }
1053 Ok(())
1054 }
1055
1056 pub async fn trigger_manual_compaction(
1057 &self,
1058 compaction_group: CompactionGroupId,
1059 manual_compaction_option: ManualCompactionOption,
1060 ) -> Result<ManualCompactionTriggerResult> {
1061 let start_time = Instant::now();
1062 let exclusive = manual_compaction_option.exclusive;
1063
1064 let compactor = match self.compactor_manager.next_compactor() {
1066 Some(compactor) => compactor,
1067 None => {
1068 tracing::warn!("trigger_manual_compaction No compactor is available.");
1069 return Err(anyhow::anyhow!(
1070 "trigger_manual_compaction No compactor is available. compaction_group {}",
1071 compaction_group
1072 )
1073 .into());
1074 }
1075 };
1076
1077 let compact_task = self
1079 .manual_get_compact_task_with_info(compaction_group, manual_compaction_option)
1080 .await;
1081 let (compact_task, blocked_by_pending) = match compact_task {
1082 Ok((compact_task, blocked_by_pending)) => (compact_task, blocked_by_pending),
1083 Err(err) => {
1084 tracing::warn!(error = %err.as_report(), "Failed to get compaction task");
1085 if matches!(err, Error::InvalidManualCompactionOption(_)) {
1086 return Err(err);
1087 }
1088
1089 return Err(anyhow::anyhow!(err)
1090 .context(format!(
1091 "Failed to get compaction task for compaction_group {}",
1092 compaction_group,
1093 ))
1094 .into());
1095 }
1096 };
1097 let compact_task = match compact_task {
1098 Some(compact_task) => compact_task,
1099 None => {
1100 if exclusive && blocked_by_pending {
1101 return Ok(ManualCompactionTriggerResult::Retry);
1102 }
1103 return Err(anyhow::anyhow!(
1105 "trigger_manual_compaction No compaction_task is available. compaction_group {}",
1106 compaction_group
1107 )
1108 .into());
1109 }
1110 };
1111
1112 let task_id = compact_task.task_id;
1114 let compact_task_string = compact_task_to_string(&compact_task);
1115 tracing::info!(
1116 compact_task_string,
1117 duration = ?start_time.elapsed(),
1118 "Triggered manual compaction task."
1119 );
1120
1121 let report_rx = self.register_compaction_task_report_waiter(task_id);
1122 if let Err(err) = compactor
1123 .send_event(ResponseEvent::CompactTask(compact_task.into()))
1124 .with_context(|| {
1125 format!(
1126 "Failed to trigger compaction task for compaction_group {}",
1127 compaction_group,
1128 )
1129 })
1130 {
1131 self.remove_compaction_task_report_waiter(task_id);
1132 return Err(err.into());
1133 }
1134
1135 let report_result = match report_rx.await {
1136 Ok(result) => result,
1137 Err(_) => {
1138 self.remove_compaction_task_report_waiter(task_id);
1139 return Err(anyhow::anyhow!(
1140 "trigger_manual_compaction wait report failed. compaction_group {}",
1141 compaction_group
1142 )
1143 .into());
1144 }
1145 };
1146 if !report_result.reported {
1147 return Err(anyhow::anyhow!(
1148 "trigger_manual_compaction report not accepted. task_id {}",
1149 report_result.task_id
1150 )
1151 .into());
1152 }
1153
1154 if report_result.task_status == TaskStatus::NoAvailCpuResourceCanceled
1155 || report_result.task_status == TaskStatus::NoAvailMemoryResourceCanceled
1156 {
1157 return Ok(ManualCompactionTriggerResult::Retry);
1158 }
1159
1160 tracing::info!(
1161 ?report_result,
1162 duration = ?start_time.elapsed(),
1163 "Completed manual compaction task."
1164 );
1165
1166 Ok(ManualCompactionTriggerResult::Submitted)
1167 }
1168
1169 pub fn try_send_compaction_request(
1171 &self,
1172 compaction_group: CompactionGroupId,
1173 task_type: compact_task::TaskType,
1174 ) -> bool {
1175 self.compaction_state.try_sched_compaction(
1176 compaction_group,
1177 task_type,
1178 ScheduleTrigger::NewData,
1179 )
1180 }
1181
1182 fn apply_split_weight_by_vnode_partition(
1185 &self,
1186 compact_task: &mut CompactTask,
1187 compaction_config: &CompactionConfig,
1188 compact_table_ids: &[TableId],
1189 ) {
1190 if compaction_config.split_weight_by_vnode > 0 {
1191 for table_id in compact_table_ids {
1192 compact_task
1193 .table_vnode_partition
1194 .insert(*table_id, compact_task.split_weight_by_vnode);
1195 }
1196
1197 return;
1198 }
1199
1200 let mut table_size_info: HashMap<TableId, u64> = HashMap::default();
1202 for input_ssts in &compact_task.input_ssts {
1203 for sst in &input_ssts.table_infos {
1204 for table_id in &sst.table_ids {
1205 *table_size_info.entry(*table_id).or_default() +=
1206 sst.sst_size / (sst.table_ids.len() as u64);
1207 }
1208 }
1209 }
1210
1211 let hybrid_vnode_count = self.env.opts.hybrid_partition_node_count;
1212 let default_partition_count = self.env.opts.partition_vnode_count;
1213 let compact_task_table_size_partition_threshold_low = self
1214 .env
1215 .opts
1216 .compact_task_table_size_partition_threshold_low;
1217 let compact_task_table_size_partition_threshold_high = self
1218 .env
1219 .opts
1220 .compact_task_table_size_partition_threshold_high;
1221
1222 let table_write_throughput_statistic_manager =
1224 self.table_write_throughput_statistic_manager.read();
1225 let timestamp = chrono::Utc::now().timestamp();
1226
1227 for (table_id, compact_table_size) in table_size_info {
1228 let write_throughput = table_write_throughput_statistic_manager
1229 .get_table_throughput_descending(table_id, timestamp)
1230 .peekable()
1231 .peek()
1232 .map(|item| item.throughput)
1233 .unwrap_or(0);
1234
1235 if compact_table_size > compact_task_table_size_partition_threshold_high
1236 && default_partition_count > 0
1237 {
1238 compact_task
1239 .table_vnode_partition
1240 .insert(table_id, default_partition_count);
1241 } else if (compact_table_size > compact_task_table_size_partition_threshold_low
1242 || (write_throughput > self.env.opts.table_high_write_throughput_threshold
1243 && compact_table_size > compaction_config.target_file_size_base))
1244 && hybrid_vnode_count > 0
1245 {
1246 compact_task
1247 .table_vnode_partition
1248 .insert(table_id, hybrid_vnode_count);
1249 } else if compact_table_size > compaction_config.target_file_size_base {
1250 compact_task.table_vnode_partition.insert(table_id, 1);
1251 }
1252 }
1253
1254 compact_task
1255 .table_vnode_partition
1256 .retain(|table_id, _| compact_table_ids.contains(table_id));
1257 }
1258
1259 pub(crate) fn calculate_vnode_partition(
1260 &self,
1261 compact_task: &mut CompactTask,
1262 compaction_config: &CompactionConfig,
1263 compact_table_ids: &[TableId],
1264 ) {
1265 if compact_task.target_level > compact_task.base_level {
1270 return;
1271 }
1272
1273 self.apply_split_weight_by_vnode_partition(
1275 compact_task,
1276 compaction_config,
1277 compact_table_ids,
1278 );
1279 }
1280
1281 fn build_ready_compact_task(
1282 &self,
1283 picked_task: PickedCompactionTask,
1284 context: CompactTaskBuildContext,
1285 table_watermarks: &HashMap<TableId, Arc<TableWatermarks>>,
1286 all_versioned_table_schemas: &HashMap<TableId, Vec<i32>>,
1287 ) -> BuiltCompactTask {
1288 let compaction_config = context.compaction_config.clone();
1289 let (mut compact_task, compact_table_ids) = build_base_compact_task(picked_task, context);
1290
1291 if compact_task.is_trivial_reclaim() {
1292 compact_task.task_status = TaskStatus::Success;
1293 compact_task.sorted_output_ssts.clear();
1294 return BuiltCompactTask::MetaFinished(compact_task);
1295 }
1296
1297 if compact_task.is_trivial_move_task() {
1298 compact_task.task_status = TaskStatus::Success;
1299 compact_task.sorted_output_ssts = compact_task.input_ssts[0]
1300 .read_sstable_infos()
1301 .cloned()
1302 .collect();
1303 return BuiltCompactTask::MetaFinished(compact_task);
1304 }
1305
1306 self.prepare_compact_task_for_assignment(
1307 &mut compact_task,
1308 compaction_config.as_ref(),
1309 &compact_table_ids,
1310 safe_epoch_table_watermarks_impl(table_watermarks, &compact_table_ids),
1311 all_versioned_table_schemas,
1312 );
1313
1314 BuiltCompactTask::PendingAssignment(compact_task)
1315 }
1316
1317 fn prepare_compact_task_for_assignment(
1318 &self,
1319 compact_task: &mut CompactTask,
1320 compaction_config: &CompactionConfig,
1321 compact_table_ids: &[TableId],
1322 table_watermarks: BTreeMap<TableId, TableWatermarks>,
1323 all_versioned_table_schemas: &HashMap<TableId, Vec<i32>>,
1324 ) {
1325 self.calculate_vnode_partition(compact_task, compaction_config, compact_table_ids);
1326 attach_compact_task_table_metadata(
1327 compact_task,
1328 compact_table_ids,
1329 table_watermarks,
1330 all_versioned_table_schemas,
1331 );
1332 }
1333
1334 pub fn compactor_manager_ref(&self) -> crate::hummock::CompactorManagerRef {
1335 self.compactor_manager.clone()
1336 }
1337
1338 fn register_compaction_task_report_waiter(
1339 &self,
1340 task_id: HummockCompactionTaskId,
1341 ) -> Receiver<CompactionTaskReportResult> {
1342 let (tx, rx) = tokio::sync::oneshot::channel();
1343 self.compaction_task_report_notifiers
1344 .lock()
1345 .register(task_id, tx);
1346 rx
1347 }
1348
1349 fn remove_compaction_task_report_waiter(&self, task_id: HummockCompactionTaskId) {
1350 self.compaction_task_report_notifiers.lock().remove(task_id);
1351 }
1352
1353 fn notify_compaction_task_report_waiters(&self, results: Vec<CompactionTaskReportResult>) {
1354 let mut guard = self.compaction_task_report_notifiers.lock();
1355 for result in results {
1356 guard.notify(result);
1357 }
1358 }
1359}
1360
1361#[cfg(any(test, feature = "test"))]
1362impl HummockManager {
1363 pub async fn compaction_task_from_assignment_for_test(
1364 &self,
1365 task_id: u64,
1366 ) -> Option<CompactTaskAssignment> {
1367 let compaction_guard = self.compaction.read().await;
1368 let assignment_ref = &compaction_guard.compact_task_assignment;
1369 assignment_ref.get(&task_id).cloned()
1370 }
1371
1372 pub async fn report_compact_task_for_test(
1373 &self,
1374 task_id: u64,
1375 compact_task: Option<CompactTask>,
1376 task_status: TaskStatus,
1377 sorted_output_ssts: Vec<SstableInfo>,
1378 table_stats_change: Option<PbTableStatsMap>,
1379 ) -> Result<()> {
1380 if let Some(task) = compact_task {
1381 let mut guard = self.compaction.write().await;
1382 guard.compact_task_assignment.insert(
1383 task_id,
1384 CompactTaskAssignment {
1385 compact_task: task,
1386 context_id: 0.into(),
1387 },
1388 );
1389 }
1390
1391 self.report_compact_tasks(vec![ReportTask {
1394 task_id,
1395 task_status,
1396 sorted_output_ssts,
1397 table_stats_change: table_stats_change.unwrap_or_default(),
1398 object_timestamps: HashMap::default(),
1399 }])
1400 .await?;
1401 Ok(())
1402 }
1403}
1404
1405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1407pub enum ScheduleTrigger {
1408 NewData,
1410 Periodic,
1412}
1413
1414pub struct CompactionScheduleSnapshot {
1419 scheduled: HashSet<(CompactionGroupId, compact_task::TaskType)>,
1420 snapshot_time: Instant,
1421}
1422
1423impl CompactionScheduleSnapshot {
1424 const TASK_TYPE_PRIORITY: &[TaskType] = &[
1426 TaskType::Dynamic,
1427 TaskType::SpaceReclaim,
1428 TaskType::Ttl,
1429 TaskType::Tombstone,
1430 TaskType::VnodeWatermark,
1431 ];
1432
1433 pub fn snapshot_time(&self) -> Instant {
1434 self.snapshot_time
1435 }
1436
1437 pub fn pick_compaction_groups_and_type(&self) -> Option<(Vec<CompactionGroupId>, TaskType)> {
1442 let group_ids = self.group_ids_shuffled();
1443 let mut normal_groups = vec![];
1444 for cg_id in group_ids {
1445 if let Some(pick_type) = self.pick_type(cg_id) {
1446 if pick_type == TaskType::Dynamic {
1447 normal_groups.push(cg_id);
1448 } else if normal_groups.is_empty() {
1449 return Some((vec![cg_id], pick_type));
1450 }
1451 }
1452 }
1453 if normal_groups.is_empty() {
1454 None
1455 } else {
1456 Some((normal_groups, TaskType::Dynamic))
1457 }
1458 }
1459
1460 fn group_ids_shuffled(&self) -> Vec<CompactionGroupId> {
1461 let mut group_ids: Vec<_> = self.scheduled.iter().map(|(g, _)| *g).unique().collect();
1462 group_ids.shuffle(&mut thread_rng());
1463 group_ids
1464 }
1465
1466 fn pick_type(&self, group: CompactionGroupId) -> Option<TaskType> {
1467 Self::TASK_TYPE_PRIORITY
1468 .iter()
1469 .find(|t| self.scheduled.contains(&(group, **t)))
1470 .copied()
1471 }
1472}
1473
1474#[derive(Debug, Default)]
1479pub struct CompactionState {
1480 inner: Mutex<CompactionStateInner>,
1481}
1482
1483#[derive(Debug, Default)]
1484struct CompactionStateInner {
1485 scheduled: HashSet<(CompactionGroupId, compact_task::TaskType)>,
1486 dynamic_cooldown: HashSet<CompactionGroupId>,
1488 last_new_data_time: HashMap<CompactionGroupId, Instant>,
1490}
1491
1492impl CompactionState {
1493 pub fn new() -> Self {
1494 Self {
1495 inner: Default::default(),
1496 }
1497 }
1498
1499 pub fn try_sched_compaction(
1503 &self,
1504 compaction_group: CompactionGroupId,
1505 task_type: TaskType,
1506 trigger: ScheduleTrigger,
1507 ) -> bool {
1508 let mut guard = self.inner.lock();
1509 if task_type == TaskType::Dynamic {
1510 match trigger {
1511 ScheduleTrigger::NewData => {
1512 guard.dynamic_cooldown.remove(&compaction_group);
1513 guard
1514 .last_new_data_time
1515 .insert(compaction_group, Instant::now());
1516 }
1517 ScheduleTrigger::Periodic => {
1518 if guard.dynamic_cooldown.contains(&compaction_group) {
1519 return false;
1520 }
1521 }
1522 }
1523 }
1524 guard.scheduled.insert((compaction_group, task_type))
1525 }
1526
1527 pub fn unschedule(
1530 &self,
1531 compaction_group: CompactionGroupId,
1532 task_type: compact_task::TaskType,
1533 snapshot_time: Instant,
1534 ) {
1535 let mut guard = self.inner.lock();
1536 guard.scheduled.remove(&(compaction_group, task_type));
1537 if task_type == TaskType::Dynamic {
1538 let has_new_data = guard
1539 .last_new_data_time
1540 .get(&compaction_group)
1541 .is_some_and(|t| *t > snapshot_time);
1542 if !has_new_data {
1543 guard.dynamic_cooldown.insert(compaction_group);
1544 }
1545 }
1546 }
1547
1548 pub fn snapshot(&self) -> CompactionScheduleSnapshot {
1550 let guard = self.inner.lock();
1551 let snapshot_time = Instant::now();
1553 CompactionScheduleSnapshot {
1554 scheduled: guard.scheduled.clone(),
1555 snapshot_time,
1556 }
1557 }
1558
1559 pub fn remove_compaction_group(&self, compaction_group: CompactionGroupId) {
1561 let mut guard = self.inner.lock();
1562 guard
1563 .scheduled
1564 .retain(|(group, _)| *group != compaction_group);
1565 guard.dynamic_cooldown.remove(&compaction_group);
1566 guard.last_new_data_time.remove(&compaction_group);
1567 }
1568}
1569
1570impl Compaction {
1571 pub fn get_compact_task_assignments_by_group_id(
1572 &self,
1573 compaction_group_id: CompactionGroupId,
1574 ) -> Vec<CompactTaskAssignment> {
1575 self.compact_task_assignment
1576 .values()
1577 .filter_map(|assignment| {
1578 if assignment.compact_task.compaction_group_id == compaction_group_id {
1579 Some(assignment.clone())
1580 } else {
1581 None
1582 }
1583 })
1584 .collect()
1585 }
1586}
1587
1588#[derive(Clone, Default)]
1589pub struct CompactionGroupStatistic {
1590 pub group_id: CompactionGroupId,
1591 pub group_size: u64,
1592 pub table_statistic: BTreeMap<StateTableId, u64>,
1593 pub compaction_group_config: CompactionGroup,
1594}
1595
1596fn update_table_stats_for_vnode_watermark_trivial_reclaim(
1598 table_stats: &mut PbTableStatsMap,
1599 task: &CompactTask,
1600) {
1601 if task.task_type != TaskType::VnodeWatermark {
1602 return;
1603 }
1604 let mut deleted_table_keys: HashMap<TableId, u64> = HashMap::default();
1605 for s in task.input_ssts.iter().flat_map(|l| l.table_infos.iter()) {
1606 assert_eq!(s.table_ids.len(), 1);
1607 let e = deleted_table_keys.entry(s.table_ids[0]).or_insert(0);
1608 *e += s.total_key_count;
1609 }
1610 for (table_id, delete_count) in deleted_table_keys {
1611 let Some(stats) = table_stats.get_mut(&table_id) else {
1612 continue;
1613 };
1614 if stats.total_key_count == 0 {
1615 continue;
1616 }
1617 let new_total_key_count = stats.total_key_count.saturating_sub(delete_count as i64);
1618 let ratio = new_total_key_count as f64 / stats.total_key_count as f64;
1619 stats.total_key_count = new_total_key_count;
1621 stats.total_key_size = (stats.total_key_size as f64 * ratio).ceil() as i64;
1623 stats.total_value_size = (stats.total_value_size as f64 * ratio).ceil() as i64;
1624 }
1625}
1626
1627#[derive(Debug, Clone)]
1628pub enum GroupState {
1629 Normal,
1631
1632 Emergency(String), WriteStop(String), }
1638
1639impl GroupState {
1640 pub fn is_write_stop(&self) -> bool {
1641 matches!(self, Self::WriteStop(_))
1642 }
1643
1644 pub fn is_emergency(&self) -> bool {
1645 matches!(self, Self::Emergency(_))
1646 }
1647
1648 pub fn reason(&self) -> Option<&str> {
1649 match self {
1650 Self::Emergency(reason) | Self::WriteStop(reason) => Some(reason),
1651 _ => None,
1652 }
1653 }
1654}
1655
1656#[derive(Clone, Default)]
1657pub struct GroupStateValidator;
1658
1659impl GroupStateValidator {
1660 pub fn write_stop_sub_level_count(
1661 level_count: usize,
1662 compaction_config: &CompactionConfig,
1663 ) -> bool {
1664 let threshold = compaction_config.level0_stop_write_threshold_sub_level_number as usize;
1665 level_count > threshold
1666 }
1667
1668 pub fn write_stop_l0_size(l0_size: u64, compaction_config: &CompactionConfig) -> bool {
1669 l0_size
1670 > compaction_config
1671 .level0_stop_write_threshold_max_size
1672 .unwrap_or(compaction_config::level0_stop_write_threshold_max_size())
1673 }
1674
1675 pub fn write_stop_l0_file_count(
1676 l0_file_count: usize,
1677 compaction_config: &CompactionConfig,
1678 ) -> bool {
1679 l0_file_count
1680 > compaction_config
1681 .level0_stop_write_threshold_max_sst_count
1682 .unwrap_or(compaction_config::level0_stop_write_threshold_max_sst_count())
1683 as usize
1684 }
1685
1686 pub fn emergency_l0_file_count(
1687 l0_file_count: usize,
1688 compaction_config: &CompactionConfig,
1689 ) -> bool {
1690 l0_file_count
1691 > compaction_config
1692 .emergency_level0_sst_file_count
1693 .unwrap_or(compaction_config::emergency_level0_sst_file_count())
1694 as usize
1695 }
1696
1697 pub fn emergency_l0_partition_count(
1698 last_l0_sub_level_partition_count: usize,
1699 compaction_config: &CompactionConfig,
1700 ) -> bool {
1701 last_l0_sub_level_partition_count
1702 > compaction_config
1703 .emergency_level0_sub_level_partition
1704 .unwrap_or(compaction_config::emergency_level0_sub_level_partition())
1705 as usize
1706 }
1707
1708 pub fn check_single_group_write_stop(
1709 levels: &Levels,
1710 compaction_config: &CompactionConfig,
1711 ) -> GroupState {
1712 if Self::write_stop_sub_level_count(levels.l0.sub_levels.len(), compaction_config) {
1713 return GroupState::WriteStop(format!(
1714 "WriteStop(l0_level_count: {}, threshold: {}) too many L0 sub levels",
1715 levels.l0.sub_levels.len(),
1716 compaction_config.level0_stop_write_threshold_sub_level_number
1717 ));
1718 }
1719
1720 if Self::write_stop_l0_file_count(
1721 levels
1722 .l0
1723 .sub_levels
1724 .iter()
1725 .map(|l| l.table_infos.len())
1726 .sum(),
1727 compaction_config,
1728 ) {
1729 return GroupState::WriteStop(format!(
1730 "WriteStop(l0_sst_count: {}, threshold: {}) too many L0 sst files",
1731 levels
1732 .l0
1733 .sub_levels
1734 .iter()
1735 .map(|l| l.table_infos.len())
1736 .sum::<usize>(),
1737 compaction_config
1738 .level0_stop_write_threshold_max_sst_count
1739 .unwrap_or(compaction_config::level0_stop_write_threshold_max_sst_count())
1740 ));
1741 }
1742
1743 if Self::write_stop_l0_size(levels.l0.total_file_size, compaction_config) {
1744 return GroupState::WriteStop(format!(
1745 "WriteStop(l0_size: {}, threshold: {}) too large L0 size",
1746 levels.l0.total_file_size,
1747 compaction_config
1748 .level0_stop_write_threshold_max_size
1749 .unwrap_or(compaction_config::level0_stop_write_threshold_max_size())
1750 ));
1751 }
1752
1753 GroupState::Normal
1754 }
1755
1756 pub fn check_single_group_emergency(
1757 levels: &Levels,
1758 compaction_config: &CompactionConfig,
1759 ) -> GroupState {
1760 if Self::emergency_l0_file_count(
1761 levels
1762 .l0
1763 .sub_levels
1764 .iter()
1765 .map(|l| l.table_infos.len())
1766 .sum(),
1767 compaction_config,
1768 ) {
1769 return GroupState::Emergency(format!(
1770 "Emergency(l0_sst_count: {}, threshold: {}) too many L0 sst files",
1771 levels
1772 .l0
1773 .sub_levels
1774 .iter()
1775 .map(|l| l.table_infos.len())
1776 .sum::<usize>(),
1777 compaction_config
1778 .emergency_level0_sst_file_count
1779 .unwrap_or(compaction_config::emergency_level0_sst_file_count())
1780 ));
1781 }
1782
1783 if Self::emergency_l0_partition_count(
1784 levels
1785 .l0
1786 .sub_levels
1787 .first()
1788 .map(|l| l.table_infos.len())
1789 .unwrap_or(0),
1790 compaction_config,
1791 ) {
1792 return GroupState::Emergency(format!(
1793 "Emergency(l0_partition_count: {}, threshold: {}) too many L0 partitions",
1794 levels
1795 .l0
1796 .sub_levels
1797 .first()
1798 .map(|l| l.table_infos.len())
1799 .unwrap_or(0),
1800 compaction_config
1801 .emergency_level0_sub_level_partition
1802 .unwrap_or(compaction_config::emergency_level0_sub_level_partition())
1803 ));
1804 }
1805
1806 GroupState::Normal
1807 }
1808
1809 pub fn group_state(levels: &Levels, compaction_config: &CompactionConfig) -> GroupState {
1810 let state = Self::check_single_group_write_stop(levels, compaction_config);
1811 if state.is_write_stop() {
1812 return state;
1813 }
1814
1815 Self::check_single_group_emergency(levels, compaction_config)
1816 }
1817}
1818
1819#[cfg(test)]
1820mod prefetched_task_id_tests {
1821 use crate::manager::MetaOpts;
1822
1823 #[test]
1824 fn test_compaction_task_id_refill_capacity_default() {
1825 assert_eq!(MetaOpts::test(false).compaction_task_id_refill_capacity, 64);
1826 }
1827}
1828
1829#[cfg(test)]
1830mod compaction_state_tests {
1831 use risingwave_pb::hummock::compact_task::TaskType;
1832
1833 use super::*;
1834
1835 #[test]
1836 fn test_basic_schedule_and_unschedule() {
1837 let state = CompactionState::new();
1838 let group_id: CompactionGroupId = 1.into();
1839
1840 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1842 assert!(!state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1844 assert!(state.try_sched_compaction(group_id, TaskType::Ttl, ScheduleTrigger::Periodic));
1846
1847 let snapshot = state.snapshot();
1849 assert!(snapshot.scheduled.contains(&(group_id, TaskType::Dynamic)));
1850 assert!(snapshot.scheduled.contains(&(group_id, TaskType::Ttl)));
1851
1852 state.unschedule(group_id, TaskType::Dynamic, snapshot.snapshot_time());
1854 let snapshot2 = state.snapshot();
1855 assert!(!snapshot2.scheduled.contains(&(group_id, TaskType::Dynamic)));
1856 assert!(snapshot2.scheduled.contains(&(group_id, TaskType::Ttl)));
1857 }
1858
1859 #[test]
1860 fn test_cooldown_blocks_periodic_trigger() {
1861 let state = CompactionState::new();
1862 let group_id: CompactionGroupId = 1.into();
1863
1864 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1866 let snapshot = state.snapshot();
1867 state.unschedule(group_id, TaskType::Dynamic, snapshot.snapshot_time());
1868
1869 assert!(state.inner.lock().dynamic_cooldown.contains(&group_id));
1871
1872 assert!(!state.try_sched_compaction(
1874 group_id,
1875 TaskType::Dynamic,
1876 ScheduleTrigger::Periodic
1877 ));
1878 }
1879
1880 #[test]
1881 fn test_new_data_clears_cooldown() {
1882 let state = CompactionState::new();
1883 let group_id: CompactionGroupId = 1.into();
1884
1885 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1887 let snapshot = state.snapshot();
1888 state.unschedule(group_id, TaskType::Dynamic, snapshot.snapshot_time());
1889 assert!(state.inner.lock().dynamic_cooldown.contains(&group_id));
1890
1891 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1893 assert!(!state.inner.lock().dynamic_cooldown.contains(&group_id));
1894 }
1895
1896 #[test]
1897 fn test_cooldown_only_affects_dynamic_type() {
1898 let state = CompactionState::new();
1899 let group_id: CompactionGroupId = 1.into();
1900
1901 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1903 let snapshot = state.snapshot();
1904 state.unschedule(group_id, TaskType::Dynamic, snapshot.snapshot_time());
1905
1906 let group_id_2: CompactionGroupId = 2.into();
1908 assert!(state.try_sched_compaction(group_id_2, TaskType::Ttl, ScheduleTrigger::Periodic));
1909 let snapshot2 = state.snapshot();
1910 state.unschedule(group_id_2, TaskType::Ttl, snapshot2.snapshot_time());
1911 assert!(!state.inner.lock().dynamic_cooldown.contains(&group_id_2));
1912
1913 assert!(state.try_sched_compaction(group_id, TaskType::Ttl, ScheduleTrigger::Periodic));
1915 assert!(state.try_sched_compaction(
1916 group_id,
1917 TaskType::SpaceReclaim,
1918 ScheduleTrigger::Periodic
1919 ));
1920 }
1921
1922 #[test]
1923 fn test_race_condition_new_data_after_snapshot() {
1924 let state = CompactionState::new();
1925 let group_id: CompactionGroupId = 1.into();
1926
1927 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1928 let snapshot = state.snapshot();
1929
1930 {
1932 let mut guard = state.inner.lock();
1933 guard.last_new_data_time.insert(group_id, Instant::now());
1934 }
1935
1936 state.unschedule(group_id, TaskType::Dynamic, snapshot.snapshot_time());
1938 assert!(
1939 !state.inner.lock().dynamic_cooldown.contains(&group_id),
1940 "Should skip cooldown when new data arrived after snapshot"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_remove_compaction_group_cleans_all_state() {
1946 let state = CompactionState::new();
1947 let group_id: CompactionGroupId = 1.into();
1948
1949 assert!(state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData));
1951 assert!(state.try_sched_compaction(group_id, TaskType::Ttl, ScheduleTrigger::Periodic));
1952 state.inner.lock().dynamic_cooldown.insert(group_id);
1953
1954 state.remove_compaction_group(group_id);
1956
1957 let guard = state.inner.lock();
1959 assert!(!guard.scheduled.contains(&(group_id, TaskType::Dynamic)));
1960 assert!(!guard.scheduled.contains(&(group_id, TaskType::Ttl)));
1961 assert!(!guard.dynamic_cooldown.contains(&group_id));
1962 assert!(!guard.last_new_data_time.contains_key(&group_id));
1963 }
1964
1965 #[test]
1966 fn test_snapshot_pick_type_priority() {
1967 let state = CompactionState::new();
1968 let group_id: CompactionGroupId = 1.into();
1969
1970 assert_eq!(state.snapshot().pick_type(group_id), None);
1972
1973 state.try_sched_compaction(
1975 group_id,
1976 TaskType::VnodeWatermark,
1977 ScheduleTrigger::Periodic,
1978 );
1979 assert_eq!(
1980 state.snapshot().pick_type(group_id),
1981 Some(TaskType::VnodeWatermark)
1982 );
1983
1984 state.try_sched_compaction(group_id, TaskType::Tombstone, ScheduleTrigger::Periodic);
1985 assert_eq!(
1986 state.snapshot().pick_type(group_id),
1987 Some(TaskType::Tombstone)
1988 );
1989
1990 state.try_sched_compaction(group_id, TaskType::Ttl, ScheduleTrigger::Periodic);
1991 assert_eq!(state.snapshot().pick_type(group_id), Some(TaskType::Ttl));
1992
1993 state.try_sched_compaction(group_id, TaskType::SpaceReclaim, ScheduleTrigger::Periodic);
1994 assert_eq!(
1995 state.snapshot().pick_type(group_id),
1996 Some(TaskType::SpaceReclaim)
1997 );
1998
1999 state.try_sched_compaction(group_id, TaskType::Dynamic, ScheduleTrigger::NewData);
2000 assert_eq!(
2001 state.snapshot().pick_type(group_id),
2002 Some(TaskType::Dynamic)
2003 );
2004 }
2005
2006 #[test]
2007 fn test_multiple_groups_independent_cooldown() {
2008 let state = CompactionState::new();
2009 let g1: CompactionGroupId = 1.into();
2010 let g2: CompactionGroupId = 2.into();
2011
2012 state.try_sched_compaction(g1, TaskType::Dynamic, ScheduleTrigger::NewData);
2013 state.try_sched_compaction(g2, TaskType::Dynamic, ScheduleTrigger::NewData);
2014 let snapshot = state.snapshot();
2015
2016 state.unschedule(g1, TaskType::Dynamic, snapshot.snapshot_time());
2018
2019 let guard = state.inner.lock();
2020 assert!(guard.dynamic_cooldown.contains(&g1));
2021 assert!(!guard.dynamic_cooldown.contains(&g2));
2022 }
2023
2024 #[test]
2025 fn test_pick_compaction_groups_empty() {
2026 let state = CompactionState::new();
2027 let snapshot = state.snapshot();
2028 assert!(snapshot.pick_compaction_groups_and_type().is_none());
2030 }
2031
2032 #[test]
2033 fn test_pick_compaction_groups_mixed_types() {
2034 let state = CompactionState::new();
2035 let g1: CompactionGroupId = 1.into();
2036 let g2: CompactionGroupId = 2.into();
2037 let g3: CompactionGroupId = 3.into();
2038
2039 state.try_sched_compaction(g1, TaskType::Dynamic, ScheduleTrigger::NewData);
2041 state.try_sched_compaction(g2, TaskType::Ttl, ScheduleTrigger::Periodic);
2042 state.try_sched_compaction(g3, TaskType::Dynamic, ScheduleTrigger::NewData);
2043
2044 let snapshot = state.snapshot();
2045 let (groups, task_type) = snapshot.pick_compaction_groups_and_type().unwrap();
2046
2047 if task_type == TaskType::Dynamic {
2052 assert!(groups.contains(&g1));
2053 assert!(groups.contains(&g3));
2054 assert!(!groups.contains(&g2)); } else {
2056 assert_eq!(task_type, TaskType::Ttl);
2057 assert_eq!(groups, vec![g2]);
2058 }
2059 }
2060
2061 #[test]
2062 fn test_pick_compaction_groups_all_dynamic() {
2063 let state = CompactionState::new();
2064 let g1: CompactionGroupId = 1.into();
2065 let g2: CompactionGroupId = 2.into();
2066
2067 state.try_sched_compaction(g1, TaskType::Dynamic, ScheduleTrigger::NewData);
2068 state.try_sched_compaction(g2, TaskType::Dynamic, ScheduleTrigger::NewData);
2069
2070 let snapshot = state.snapshot();
2071 let (groups, task_type) = snapshot.pick_compaction_groups_and_type().unwrap();
2072 assert_eq!(task_type, TaskType::Dynamic);
2073 assert!(groups.contains(&g1));
2074 assert!(groups.contains(&g2));
2075 }
2076
2077 #[test]
2078 fn test_pick_compaction_groups_single_non_dynamic() {
2079 let state = CompactionState::new();
2080 let g1: CompactionGroupId = 1.into();
2081
2082 state.try_sched_compaction(g1, TaskType::SpaceReclaim, ScheduleTrigger::Periodic);
2083
2084 let snapshot = state.snapshot();
2085 let (groups, task_type) = snapshot.pick_compaction_groups_and_type().unwrap();
2086 assert_eq!(task_type, TaskType::SpaceReclaim);
2087 assert_eq!(groups, vec![g1]);
2088 }
2089}