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