1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
16use std::ops::{Deref, DerefMut};
17use std::sync::Arc;
18
19use bytes::Bytes;
20use itertools::Itertools;
21use risingwave_common::catalog::TableId;
22use risingwave_common::hash::VirtualNode;
23use risingwave_common::monitor::MonitoredRwLock;
24use risingwave_hummock_sdk::compact_task::{ReportTask, is_compaction_task_expired};
25use risingwave_hummock_sdk::compaction_group::{
26 StateTableId, StaticCompactionGroupId, group_split,
27};
28use risingwave_hummock_sdk::version::{GroupDelta, GroupDeltas, HummockVersion};
29use risingwave_hummock_sdk::{CompactionGroupId, can_concat};
30use risingwave_pb::hummock::compact_task::TaskStatus;
31use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::mutable_config::MutableConfig;
32use risingwave_pb::hummock::{
33 CompatibilityVersion, PbGroupConstruct, PbGroupMerge, PbStateTableInfoDelta,
34};
35use thiserror_ext::AsReport;
36
37use super::compaction_group_manager::CompactionGroupManager;
38use super::{CompactionGroupStatistic, GroupStateValidator};
39use crate::hummock::error::{Error, Result};
40use crate::hummock::manager::transaction::HummockVersionTransaction;
41use crate::hummock::manager::versioning::Versioning;
42use crate::hummock::manager::{HummockManager, commit_multi_var};
43use crate::hummock::metrics_utils::remove_compaction_group_metrics;
44use crate::hummock::sequence::{next_compaction_group_id, next_sstable_id};
45use crate::hummock::table_write_throughput_statistic::{
46 TableWriteThroughputStatistic, TableWriteThroughputStatisticManager,
47};
48use crate::manager::MetaOpts;
49
50#[derive(Debug, PartialEq, Eq)]
51struct NormalizePlan {
52 parent_group_id: CompactionGroupId,
53 parent_table_ids: Vec<StateTableId>,
54 boundary_table_id: StateTableId,
55}
56
57impl NormalizePlan {
58 fn split_key(&self) -> Bytes {
59 group_split::build_split_full_key(self.boundary_table_id, VirtualNode::ZERO)
60 .encode()
61 .into()
62 }
63
64 fn split_table_ids(&self) -> (Vec<StateTableId>, Vec<StateTableId>) {
65 let split_full_key =
66 group_split::build_split_full_key(self.boundary_table_id, VirtualNode::ZERO);
67 let (table_ids_left, table_ids_right) =
68 group_split::split_table_ids_with_table_id_and_vnode(
69 &self.parent_table_ids,
70 split_full_key.user_key.table_id,
71 split_full_key.user_key.get_vnode_id(),
72 );
73 assert!(!table_ids_left.is_empty() && !table_ids_right.is_empty());
74 (table_ids_left, table_ids_right)
75 }
76}
77
78fn gen_normalize_plan(
79 left: &CompactionGroupStatistic,
80 right: &CompactionGroupStatistic,
81) -> Option<NormalizePlan> {
82 let left_table_ids = left.table_statistic.keys().copied().collect_vec();
83
84 if left_table_ids.len() <= 1 {
85 return None;
86 }
87
88 let left_max = *left_table_ids.last().unwrap();
89 let right_min = *right.table_statistic.keys().next().unwrap();
90 if left_max < right_min {
91 return None;
92 }
93
94 let boundary_index = left_table_ids.partition_point(|&table_id| table_id < right_min);
95 if boundary_index == 0 || boundary_index >= left_table_ids.len() {
96 return None;
97 }
98 let boundary_table_id = left_table_ids[boundary_index];
99
100 Some(NormalizePlan {
101 parent_group_id: left.group_id,
102 parent_table_ids: left_table_ids,
103 boundary_table_id,
104 })
105}
106
107fn build_normalize_plan_from_group_statistics(
108 groups: &[CompactionGroupStatistic],
109) -> Option<NormalizePlan> {
110 let mut groups = groups
113 .iter()
114 .filter(|group| !group.table_statistic.is_empty())
115 .collect_vec();
116 groups.sort_by_key(|group| *group.table_statistic.keys().next().unwrap());
117
118 groups
119 .split(|group| {
120 group
121 .compaction_group_config
122 .compaction_config
123 .disable_auto_group_scheduling
124 .unwrap_or(false)
125 })
126 .find_map(|segment| {
127 segment
128 .windows(2)
129 .find_map(|pair| gen_normalize_plan(pair[0], pair[1]))
130 })
131}
132
133fn collect_normalize_group_statistics(
134 version: &HummockVersion,
135 compaction_group_manager: &CompactionGroupManager,
136) -> Result<Vec<CompactionGroupStatistic>> {
137 let mut groups = vec![];
138 for group_id in version.levels.keys() {
139 let table_ids = version
140 .state_table_info
141 .compaction_group_member_table_ids(*group_id)
142 .iter()
143 .copied()
144 .collect_vec();
145 if table_ids.is_empty() {
146 continue;
147 }
148
149 let group_config = compaction_group_manager
150 .try_get_compaction_group_config(*group_id)
151 .ok_or_else(|| {
152 Error::CompactionGroup(format!(
153 "group {} config not found during normalize",
154 group_id
155 ))
156 })?;
157 groups.push(CompactionGroupStatistic {
158 group_id: *group_id,
159 group_size: 0,
160 table_statistic: table_ids
161 .into_iter()
162 .map(|table_id| (table_id, 0))
163 .collect(),
164 compaction_group_config: group_config,
165 });
166 }
167 Ok(groups)
168}
169
170impl HummockManager {
171 pub async fn merge_compaction_group(
172 &self,
173 group_1: CompactionGroupId,
174 group_2: CompactionGroupId,
175 ) -> Result<()> {
176 self.merge_compaction_group_impl(group_1, group_2, None)
177 .await
178 }
179
180 pub async fn merge_compaction_group_for_test(
181 &self,
182 group_1: CompactionGroupId,
183 group_2: CompactionGroupId,
184 created_tables: HashSet<TableId>,
185 ) -> Result<()> {
186 self.merge_compaction_group_impl(group_1, group_2, Some(created_tables))
187 .await
188 }
189
190 pub async fn merge_compaction_group_impl(
191 &self,
192 group_1: CompactionGroupId,
193 group_2: CompactionGroupId,
194 created_tables: Option<HashSet<TableId>>,
195 ) -> Result<()> {
196 let compaction_guard = self
197 .compaction
198 .write_with_process_name("merge_compaction_group_impl")
199 .await;
200 let mut versioning_guard = self
201 .versioning
202 .write_with_process_name("merge_compaction_group_impl")
203 .await;
204 let versioning = versioning_guard.deref_mut();
205 if !versioning.current_version.levels.contains_key(&group_1) {
207 return Err(Error::CompactionGroup(format!("invalid group {}", group_1)));
208 }
209
210 if !versioning.current_version.levels.contains_key(&group_2) {
211 return Err(Error::CompactionGroup(format!("invalid group {}", group_2)));
212 }
213
214 let state_table_info = versioning.current_version.state_table_info.clone();
215 let mut member_table_ids_1 = state_table_info
216 .compaction_group_member_table_ids(group_1)
217 .iter()
218 .cloned()
219 .collect_vec();
220
221 if member_table_ids_1.is_empty() {
222 return Err(Error::CompactionGroup(format!(
223 "group_1 {} is empty",
224 group_1
225 )));
226 }
227
228 let mut member_table_ids_2 = state_table_info
229 .compaction_group_member_table_ids(group_2)
230 .iter()
231 .cloned()
232 .collect_vec();
233
234 if member_table_ids_2.is_empty() {
235 return Err(Error::CompactionGroup(format!(
236 "group_2 {} is empty",
237 group_2
238 )));
239 }
240
241 debug_assert!(!member_table_ids_1.is_empty());
242 debug_assert!(!member_table_ids_2.is_empty());
243 assert!(member_table_ids_1.is_sorted());
244 assert!(member_table_ids_2.is_sorted());
245
246 let created_tables = if let Some(created_tables) = created_tables {
247 #[expect(clippy::assertions_on_constants)]
249 {
250 assert!(cfg!(debug_assertions));
251 }
252 created_tables
253 } else {
254 match self.metadata_manager.get_created_table_ids().await {
255 Ok(created_tables) => HashSet::from_iter(created_tables),
256 Err(err) => {
257 tracing::warn!(error = %err.as_report(), "failed to fetch created table ids");
258 return Err(Error::CompactionGroup(format!(
259 "merge group_1 {} group_2 {} failed to fetch created table ids",
260 group_1, group_2
261 )));
262 }
263 }
264 };
265
266 fn contains_creating_table(
267 table_ids: &Vec<TableId>,
268 created_tables: &HashSet<TableId>,
269 ) -> bool {
270 table_ids
271 .iter()
272 .any(|table_id| !created_tables.contains(table_id))
273 }
274
275 if contains_creating_table(&member_table_ids_1, &created_tables)
277 || contains_creating_table(&member_table_ids_2, &created_tables)
278 {
279 return Err(Error::CompactionGroup(format!(
280 "Cannot merge creating group {} next_group {} member_table_ids_1 {:?} member_table_ids_2 {:?}",
281 group_1, group_2, member_table_ids_1, member_table_ids_2
282 )));
283 }
284
285 let (left_group_id, right_group_id) =
287 if member_table_ids_1.first().unwrap() < member_table_ids_2.first().unwrap() {
288 (group_1, group_2)
289 } else {
290 std::mem::swap(&mut member_table_ids_1, &mut member_table_ids_2);
291 (group_2, group_1)
292 };
293
294 if member_table_ids_1.last().unwrap() >= member_table_ids_2.first().unwrap() {
299 return Err(Error::CompactionGroup(format!(
300 "invalid merge group_1 {} group_2 {}: table id ranges overlap",
301 left_group_id, right_group_id
302 )));
303 }
304
305 let combined_member_table_ids = member_table_ids_1
306 .iter()
307 .chain(member_table_ids_2.iter())
308 .collect_vec();
309 assert!(combined_member_table_ids.is_sorted());
310
311 let mut sst_id_set = HashSet::new();
313 for sst_id in versioning
314 .current_version
315 .get_sst_ids_by_group_id(left_group_id)
316 .chain(
317 versioning
318 .current_version
319 .get_sst_ids_by_group_id(right_group_id),
320 )
321 {
322 if !sst_id_set.insert(sst_id) {
323 return Err(Error::CompactionGroup(format!(
324 "invalid merge group_1 {} group_2 {} duplicated sst_id {}",
325 left_group_id, right_group_id, sst_id
326 )));
327 }
328 }
329
330 {
332 let left_levels = versioning
333 .current_version
334 .get_compaction_group_levels(group_1);
335
336 let right_levels = versioning
337 .current_version
338 .get_compaction_group_levels(group_2);
339
340 let max_level = std::cmp::max(left_levels.levels.len(), right_levels.levels.len());
343 for level_idx in 1..=max_level {
344 let left_level = left_levels.get_level(level_idx);
345 let right_level = right_levels.get_level(level_idx);
346 if left_level.table_infos.is_empty() || right_level.table_infos.is_empty() {
347 continue;
348 }
349
350 let left_last_sst = left_level.table_infos.last().unwrap().clone();
351 let right_first_sst = right_level.table_infos.first().unwrap().clone();
352 let left_sst_id = left_last_sst.sst_id;
353 let right_sst_id = right_first_sst.sst_id;
354 let left_obj_id = left_last_sst.object_id;
355 let right_obj_id = right_first_sst.object_id;
356
357 if !can_concat(&[left_last_sst, right_first_sst]) {
359 return Err(Error::CompactionGroup(format!(
360 "invalid merge group_1 {} group_2 {} level_idx {} left_last_sst_id {} right_first_sst_id {} left_obj_id {} right_obj_id {}",
361 left_group_id,
362 right_group_id,
363 level_idx,
364 left_sst_id,
365 right_sst_id,
366 left_obj_id,
367 right_obj_id
368 )));
369 }
370 }
371 }
372
373 let mut version = HummockVersionTransaction::new(
374 &mut versioning.current_version,
375 &mut versioning.hummock_version_deltas,
376 &mut versioning.table_change_log,
377 self.env.notification_manager(),
378 None,
379 &self.metrics,
380 &self.env.opts,
381 &self.version_stat_tx,
382 );
383 let mut new_version_delta = version.new_delta();
384
385 let target_compaction_group_id = {
386 new_version_delta.group_deltas.insert(
388 left_group_id,
389 GroupDeltas {
390 group_deltas: vec![GroupDelta::GroupMerge(PbGroupMerge {
391 left_group_id,
392 right_group_id,
393 })],
394 },
395 );
396 left_group_id
397 };
398
399 new_version_delta.with_latest_version(|version, new_version_delta| {
402 for &table_id in combined_member_table_ids {
403 let info = version
404 .state_table_info
405 .info()
406 .get(&table_id)
407 .expect("have check exist previously");
408 assert!(
409 new_version_delta
410 .state_table_info_delta
411 .insert(
412 table_id,
413 PbStateTableInfoDelta {
414 committed_epoch: info.committed_epoch,
415 compaction_group_id: target_compaction_group_id,
416 }
417 )
418 .is_none()
419 );
420 }
421 });
422
423 {
424 let mut compaction_group_manager = self
425 .compaction_group_manager
426 .write_with_process_name("merge_compaction_group_impl")
427 .await;
428 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
429
430 {
432 let right_group_max_level = new_version_delta
433 .latest_version()
434 .get_compaction_group_levels(right_group_id)
435 .levels
436 .len();
437
438 remove_compaction_group_metrics(
439 &self.metrics,
440 right_group_id,
441 right_group_max_level,
442 );
443 }
444
445 self.compaction_state
447 .remove_compaction_group(right_group_id);
448
449 {
451 if let Err(err) = compaction_groups_txn.update_compaction_config(
452 &[left_group_id],
453 &[MutableConfig::SplitWeightByVnode(0)], ) {
455 tracing::error!(
456 error = %err.as_report(),
457 "failed to update compaction config for group-{}",
458 left_group_id
459 );
460 }
461 }
462
463 new_version_delta.pre_apply();
464
465 compaction_groups_txn.remove(right_group_id);
467 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
468 }
469
470 versioning.mark_next_time_travel_version_snapshot();
472
473 let mut canceled_tasks = vec![];
475 let compact_task_assignments =
478 compaction_guard.get_compact_task_assignments_by_group_id(right_group_id);
479 compact_task_assignments
480 .into_iter()
481 .for_each(|task_assignment| {
482 let task = &task_assignment.compact_task;
483 assert_eq!(task.compaction_group_id, right_group_id);
484 canceled_tasks.push(ReportTask {
485 task_id: task.task_id,
486 task_status: TaskStatus::ManualCanceled,
487 table_stats_change: HashMap::default(),
488 sorted_output_ssts: vec![],
489 object_timestamps: HashMap::default(),
490 });
491 });
492
493 if !canceled_tasks.is_empty() {
494 self.report_compact_tasks_impl(canceled_tasks, compaction_guard, versioning_guard)
495 .await?;
496 } else {
497 drop(versioning_guard);
498 drop(compaction_guard);
499 }
500
501 self.try_update_write_limits(&[left_group_id, right_group_id])
502 .await;
503
504 self.metrics
505 .merge_compaction_group_count
506 .with_label_values(&[&left_group_id.to_string()])
507 .inc();
508
509 Ok(())
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use std::collections::BTreeMap;
516
517 use risingwave_hummock_sdk::CompactionGroupId;
518 use risingwave_pb::hummock::CompactionConfig;
519
520 use super::{
521 CompactionGroupStatistic, NormalizePlan, build_normalize_plan_from_group_statistics,
522 gen_normalize_plan,
523 };
524 use crate::hummock::model::CompactionGroup;
525
526 fn group(
527 group_id: CompactionGroupId,
528 table_ids: &[u32],
529 disable_auto_group_scheduling: bool,
530 ) -> CompactionGroupStatistic {
531 let config = CompactionConfig {
532 disable_auto_group_scheduling: Some(disable_auto_group_scheduling),
533 ..Default::default()
534 };
535 CompactionGroupStatistic {
536 group_id,
537 group_size: 0,
538 table_statistic: table_ids
539 .iter()
540 .copied()
541 .map(|table_id| (table_id.into(), 0_u64))
542 .collect::<BTreeMap<_, _>>(),
543 compaction_group_config: CompactionGroup::new(group_id, config),
544 }
545 }
546
547 #[test]
548 fn test_gen_normalize_plan_returns_none_for_single_table_group() {
549 let left = group(1.into(), &[10], false);
550 let right = group(2.into(), &[5, 20], false);
551
552 assert_eq!(None, gen_normalize_plan(&left, &right));
553 }
554
555 #[test]
556 fn test_gen_normalize_plan_returns_none_for_non_overlapping_groups() {
557 let left = group(1.into(), &[1, 2, 3], false);
558 let right = group(2.into(), &[4, 5, 6], false);
559
560 assert_eq!(None, gen_normalize_plan(&left, &right));
561 }
562
563 #[test]
564 fn test_gen_normalize_plan_returns_none_when_boundary_cannot_split_parent() {
565 let left = group(1.into(), &[5, 6, 7], false);
566 let right = group(2.into(), &[4, 8], false);
567
568 assert_eq!(None, gen_normalize_plan(&left, &right));
569 }
570
571 #[test]
572 fn test_gen_normalize_plan_generates_expected_boundary() {
573 let left = group(1.into(), &[1, 4, 7], false);
574 let right = group(2.into(), &[2, 5, 8], false);
575
576 assert_eq!(
577 Some(NormalizePlan {
578 parent_group_id: 1.into(),
579 parent_table_ids: vec![1.into(), 4.into(), 7.into()],
580 boundary_table_id: 4.into(),
581 }),
582 gen_normalize_plan(&left, &right)
583 );
584 }
585
586 #[test]
587 fn test_build_normalize_plan_skips_disabled_boundary_and_continues_later_segment() {
588 let groups = vec![
589 group(1.into(), &[1, 4, 7], false),
590 group(2.into(), &[2, 5, 8], true),
591 group(3.into(), &[10, 13, 16], false),
592 group(4.into(), &[11, 14, 17], false),
593 ];
594
595 assert_eq!(
596 Some(NormalizePlan {
597 parent_group_id: 3.into(),
598 parent_table_ids: vec![10.into(), 13.into(), 16.into()],
599 boundary_table_id: 13.into(),
600 }),
601 build_normalize_plan_from_group_statistics(&groups)
602 );
603 }
604}
605
606impl HummockManager {
607 async fn split_compaction_group_impl(
619 &self,
620 parent_group_id: CompactionGroupId,
621 split_table_ids: &[StateTableId],
622 table_id_to_split: StateTableId,
623 vnode_to_split: VirtualNode,
624 partition_vnode_count: Option<u32>,
625 ) -> Result<Vec<(CompactionGroupId, Vec<StateTableId>)>> {
626 let mut result = vec![];
627 let compaction_guard = self
628 .compaction
629 .write_with_process_name("split_compaction_group_impl")
630 .await;
631 let mut versioning_guard = self
632 .versioning
633 .write_with_process_name("split_compaction_group_impl")
634 .await;
635 let versioning = versioning_guard.deref_mut();
636 if !versioning
638 .current_version
639 .levels
640 .contains_key(&parent_group_id)
641 {
642 return Err(Error::CompactionGroup(format!(
643 "invalid group {}",
644 parent_group_id
645 )));
646 }
647
648 let member_table_ids = versioning
649 .current_version
650 .state_table_info
651 .compaction_group_member_table_ids(parent_group_id)
652 .iter()
653 .copied()
654 .collect::<BTreeSet<_>>();
655
656 if !member_table_ids.contains(&table_id_to_split) {
657 return Err(Error::CompactionGroup(format!(
658 "table {} doesn't in group {}",
659 table_id_to_split, parent_group_id
660 )));
661 }
662
663 let split_full_key = group_split::build_split_full_key(table_id_to_split, vnode_to_split);
664
665 let table_ids = member_table_ids.into_iter().collect_vec();
667 if table_ids == split_table_ids {
668 return Err(Error::CompactionGroup(format!(
669 "invalid split attempt for group {}: all member tables are moved",
670 parent_group_id
671 )));
672 }
673 let (table_ids_left, table_ids_right) =
675 group_split::split_table_ids_with_table_id_and_vnode(
676 &table_ids,
677 split_full_key.user_key.table_id,
678 split_full_key.user_key.get_vnode_id(),
679 );
680 if table_ids_left.is_empty() || table_ids_right.is_empty() {
681 if !table_ids_left.is_empty() {
683 result.push((parent_group_id, table_ids_left));
684 }
685
686 if !table_ids_right.is_empty() {
687 result.push((parent_group_id, table_ids_right));
688 }
689 return Ok(result);
690 }
691
692 result.push((parent_group_id, table_ids_left));
693
694 let split_key: Bytes = split_full_key.encode().into();
695
696 let mut version = HummockVersionTransaction::new(
697 &mut versioning.current_version,
698 &mut versioning.hummock_version_deltas,
699 &mut versioning.table_change_log,
700 self.env.notification_manager(),
701 None,
702 &self.metrics,
703 &self.env.opts,
704 &self.version_stat_tx,
705 );
706 let mut new_version_delta = version.new_delta();
707
708 let split_sst_count = new_version_delta
709 .latest_version()
710 .count_new_ssts_in_group_split(parent_group_id, split_key.clone());
711
712 let new_sst_start_id = next_sstable_id(&self.env, split_sst_count).await?;
713 let (new_compaction_group_id, config) = {
714 let new_compaction_group_id = next_compaction_group_id(&self.env).await?;
716 let config = self
718 .compaction_group_manager
719 .read_with_process_name("split_compaction_group_impl")
720 .await
721 .try_get_compaction_group_config(parent_group_id)
722 .ok_or_else(|| {
723 Error::CompactionGroup(format!(
724 "parent group {} config not found",
725 parent_group_id
726 ))
727 })?
728 .compaction_config()
729 .as_ref()
730 .clone();
731
732 #[expect(deprecated)]
733 new_version_delta.group_deltas.insert(
735 new_compaction_group_id,
736 GroupDeltas {
737 group_deltas: vec![GroupDelta::GroupConstruct(Box::new(PbGroupConstruct {
738 group_config: Some(config.clone()),
739 group_id: new_compaction_group_id,
740 parent_group_id,
741 new_sst_start_id,
742 table_ids: vec![],
743 version: CompatibilityVersion::LATEST as _, split_key: Some(split_key.into()),
745 }))],
746 },
747 );
748 (new_compaction_group_id, config)
749 };
750
751 new_version_delta.with_latest_version(|version, new_version_delta| {
752 for &table_id in &table_ids_right {
753 let info = version
754 .state_table_info
755 .info()
756 .get(&table_id)
757 .expect("have check exist previously");
758 assert!(
759 new_version_delta
760 .state_table_info_delta
761 .insert(
762 table_id,
763 PbStateTableInfoDelta {
764 committed_epoch: info.committed_epoch,
765 compaction_group_id: new_compaction_group_id,
766 }
767 )
768 .is_none()
769 );
770 }
771 });
772
773 result.push((new_compaction_group_id, table_ids_right));
774
775 {
776 let mut compaction_group_manager = self
777 .compaction_group_manager
778 .write_with_process_name("split_compaction_group_impl")
779 .await;
780 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
781 compaction_groups_txn
782 .create_compaction_groups(new_compaction_group_id, Arc::new(config));
783
784 for (cg_id, table_ids) in &result {
788 if let Some(partition_vnode_count) = partition_vnode_count
790 && table_ids.len() == 1
791 && table_ids == split_table_ids
792 && let Err(err) = compaction_groups_txn.update_compaction_config(
793 &[*cg_id],
794 &[MutableConfig::SplitWeightByVnode(partition_vnode_count)],
795 )
796 {
797 tracing::error!(
798 error = %err.as_report(),
799 "failed to update compaction config for group-{}",
800 cg_id
801 );
802 }
803 }
804
805 new_version_delta.pre_apply();
806 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
807 }
808 versioning.mark_next_time_travel_version_snapshot();
810
811 let mut canceled_tasks = vec![];
814 let compact_task_assignments =
815 compaction_guard.get_compact_task_assignments_by_group_id(parent_group_id);
816 let levels = versioning
817 .current_version
818 .get_compaction_group_levels(parent_group_id);
819 compact_task_assignments
820 .into_iter()
821 .for_each(|task_assignment| {
822 let task = &task_assignment.compact_task;
823 let is_expired = is_compaction_task_expired(
824 task.compaction_group_version_id,
825 levels.compaction_group_version_id,
826 );
827 if is_expired {
828 canceled_tasks.push(ReportTask {
829 task_id: task.task_id,
830 task_status: TaskStatus::ManualCanceled,
831 table_stats_change: HashMap::default(),
832 sorted_output_ssts: vec![],
833 object_timestamps: HashMap::default(),
834 });
835 }
836 });
837
838 if !canceled_tasks.is_empty() {
839 self.report_compact_tasks_impl(canceled_tasks, compaction_guard, versioning_guard)
840 .await?;
841 } else {
842 drop(versioning_guard);
843 drop(compaction_guard);
844 }
845
846 let affected_group_ids = result.iter().map(|(cg_id, _)| *cg_id).collect_vec();
847 self.try_update_write_limits(&affected_group_ids).await;
848
849 self.metrics
850 .split_compaction_group_count
851 .with_label_values(&[&parent_group_id.to_string()])
852 .inc();
853
854 Ok(result)
855 }
856
857 pub async fn move_state_tables_to_dedicated_compaction_group(
860 &self,
861 parent_group_id: CompactionGroupId,
862 table_ids: &[StateTableId],
863 partition_vnode_count: Option<u32>,
864 ) -> Result<(
865 CompactionGroupId,
866 BTreeMap<CompactionGroupId, Vec<StateTableId>>,
867 )> {
868 if table_ids.is_empty() {
869 return Err(Error::CompactionGroup(
870 "table_ids must not be empty".to_owned(),
871 ));
872 }
873
874 if !table_ids.is_sorted() {
875 return Err(Error::CompactionGroup(
876 "table_ids must be sorted".to_owned(),
877 ));
878 }
879
880 let parent_table_ids = {
881 let versioning_guard = self
882 .versioning
883 .read_with_process_name("move_state_tables_to_dedicated_compaction_group")
884 .await;
885 versioning_guard
886 .current_version
887 .state_table_info
888 .compaction_group_member_table_ids(parent_group_id)
889 .iter()
890 .copied()
891 .collect_vec()
892 };
893
894 if parent_table_ids == table_ids {
895 return Err(Error::CompactionGroup(format!(
896 "invalid split attempt for group {}: all member tables are moved",
897 parent_group_id
898 )));
899 }
900
901 fn check_table_ids_valid(cg_id_to_table_ids: &BTreeMap<CompactionGroupId, Vec<TableId>>) {
902 {
904 cg_id_to_table_ids
905 .iter()
906 .for_each(|(_cg_id, table_ids)| assert!(table_ids.is_sorted()));
907 }
908
909 {
911 let mut table_table_ids_vec = cg_id_to_table_ids.values().cloned().collect_vec();
912 table_table_ids_vec.sort_by(|a, b| a[0].cmp(&b[0]));
913 assert!(table_table_ids_vec.concat().is_sorted());
914 }
915
916 {
918 let mut all_table_ids = HashSet::new();
919 for table_ids in cg_id_to_table_ids.values() {
920 for table_id in table_ids {
921 assert!(all_table_ids.insert(*table_id));
922 }
923 }
924 }
925 }
926
927 let mut cg_id_to_table_ids: BTreeMap<CompactionGroupId, Vec<TableId>> = BTreeMap::new();
937 let table_id_to_split = *table_ids.first().unwrap();
938 let mut target_compaction_group_id: CompactionGroupId = 0.into();
939 let result_vec = self
940 .split_compaction_group_impl(
941 parent_group_id,
942 table_ids,
943 table_id_to_split,
944 VirtualNode::ZERO,
945 partition_vnode_count,
946 )
947 .await?;
948 assert!(result_vec.len() <= 2);
949
950 let mut finish_move = false;
951 for (cg_id, table_ids_after_split) in result_vec {
952 if table_ids_after_split.contains(&table_id_to_split) {
953 target_compaction_group_id = cg_id;
954 }
955
956 if table_ids_after_split == table_ids {
957 finish_move = true;
958 }
959
960 cg_id_to_table_ids.insert(cg_id, table_ids_after_split);
961 }
962 check_table_ids_valid(&cg_id_to_table_ids);
963
964 if finish_move {
965 return Ok((target_compaction_group_id, cg_id_to_table_ids));
966 }
967
968 let table_id_to_split = *table_ids.last().unwrap();
971 let result_vec = self
972 .split_compaction_group_impl(
973 target_compaction_group_id,
974 table_ids,
975 table_id_to_split,
976 VirtualNode::MAX_REPRESENTABLE,
977 partition_vnode_count,
978 )
979 .await?;
980 assert!(result_vec.len() <= 2);
981 for (cg_id, table_ids_after_split) in result_vec {
982 if table_ids_after_split.contains(&table_id_to_split) {
983 target_compaction_group_id = cg_id;
984 }
985 cg_id_to_table_ids.insert(cg_id, table_ids_after_split);
986 }
987 check_table_ids_valid(&cg_id_to_table_ids);
988
989 Ok((target_compaction_group_id, cg_id_to_table_ids))
990 }
991}
992
993impl HummockManager {
994 async fn build_normalize_plan(&self) -> Option<NormalizePlan> {
995 let groups = self.calculate_compaction_group_statistic().await;
996 build_normalize_plan_from_group_statistics(&groups)
997 }
998
999 async fn apply_normalize_plan(&self, plan: &NormalizePlan) -> Result<bool> {
1000 let (table_ids_right, boundary_table_id, new_compaction_group_id) = {
1001 let mut versioning_guard = self
1002 .versioning
1003 .write_with_process_name("apply_normalize_plan")
1004 .await;
1005 let versioning = versioning_guard.deref_mut();
1006 let mut compaction_group_manager = self
1007 .compaction_group_manager
1008 .write_with_process_name("apply_normalize_plan")
1009 .await;
1010
1011 let groups = collect_normalize_group_statistics(
1012 &versioning.current_version,
1013 &compaction_group_manager,
1014 )?;
1015 let Some(current_plan) = build_normalize_plan_from_group_statistics(&groups) else {
1016 return Ok(false);
1017 };
1018
1019 if ¤t_plan != plan {
1020 return Ok(false);
1021 }
1022
1023 let (_table_ids_left, table_ids_right) = plan.split_table_ids();
1024
1025 let config = compaction_group_manager
1026 .try_get_compaction_group_config(plan.parent_group_id)
1027 .ok_or_else(|| {
1028 Error::CompactionGroup(format!(
1029 "parent group {} config not found",
1030 plan.parent_group_id
1031 ))
1032 })?
1033 .compaction_config()
1034 .as_ref()
1035 .clone();
1036
1037 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
1038 let mut version = HummockVersionTransaction::new(
1039 &mut versioning.current_version,
1040 &mut versioning.hummock_version_deltas,
1041 &mut versioning.table_change_log,
1042 self.env.notification_manager(),
1043 None,
1044 &self.metrics,
1045 &self.env.opts,
1046 &self.version_stat_tx,
1047 );
1048 let mut new_version_delta = version.new_delta();
1049 let split_key = plan.split_key();
1050 let split_sst_count = new_version_delta
1051 .latest_version()
1052 .count_new_ssts_in_group_split(plan.parent_group_id, split_key.clone());
1053 let new_sst_start_id = next_sstable_id(&self.env, split_sst_count).await?;
1054 let new_compaction_group_id = next_compaction_group_id(&self.env).await?;
1055
1056 #[expect(deprecated)]
1057 new_version_delta.group_deltas.insert(
1058 new_compaction_group_id,
1059 GroupDeltas {
1060 group_deltas: vec![GroupDelta::GroupConstruct(Box::new(PbGroupConstruct {
1061 group_config: Some(config.clone()),
1062 group_id: new_compaction_group_id,
1063 parent_group_id: plan.parent_group_id,
1064 new_sst_start_id,
1065 table_ids: vec![],
1066 version: CompatibilityVersion::LATEST as _,
1067 split_key: Some(split_key.into()),
1068 }))],
1069 },
1070 );
1071
1072 new_version_delta.with_latest_version(|version, new_version_delta| {
1073 for &table_id in &table_ids_right {
1074 let info = version
1075 .state_table_info
1076 .info()
1077 .get(&table_id)
1078 .expect("table should exist before normalize split");
1079 assert!(
1080 new_version_delta
1081 .state_table_info_delta
1082 .insert(
1083 table_id,
1084 PbStateTableInfoDelta {
1085 committed_epoch: info.committed_epoch,
1086 compaction_group_id: new_compaction_group_id,
1087 }
1088 )
1089 .is_none()
1090 );
1091 }
1092 });
1093 new_version_delta.pre_apply();
1094 compaction_groups_txn
1095 .create_compaction_groups(new_compaction_group_id, Arc::new(config));
1096
1097 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
1098 versioning.mark_next_time_travel_version_snapshot();
1099
1100 (
1101 table_ids_right,
1102 plan.boundary_table_id,
1103 new_compaction_group_id,
1104 )
1105 };
1106
1107 self.cancel_expired_normalize_split_tasks(plan.parent_group_id)
1108 .await?;
1109 self.try_update_write_limits(&[plan.parent_group_id, new_compaction_group_id])
1110 .await;
1111 self.metrics
1112 .split_compaction_group_count
1113 .with_label_values(&[&plan.parent_group_id.to_string()])
1114 .inc();
1115 tracing::info!(
1116 "normalize split success: parent_group={} boundary_table_id={} moved_tables={:?} new_group_id={}",
1117 plan.parent_group_id,
1118 boundary_table_id,
1119 table_ids_right,
1120 new_compaction_group_id
1121 );
1122
1123 Ok(true)
1124 }
1125
1126 async fn cancel_expired_normalize_split_tasks(
1127 &self,
1128 parent_group_id: CompactionGroupId,
1129 ) -> Result<()> {
1130 let mut canceled_tasks = vec![];
1131 let compaction_guard = self
1132 .compaction
1133 .write_with_process_name("cancel_expired_normalize_split_tasks")
1134 .await;
1135 let mut versioning_guard = self
1136 .versioning
1137 .write_with_process_name("cancel_expired_normalize_split_tasks")
1138 .await;
1139 let versioning = versioning_guard.deref_mut();
1140 let compact_task_assignments =
1141 compaction_guard.get_compact_task_assignments_by_group_id(parent_group_id);
1142 let levels = versioning
1143 .current_version
1144 .get_compaction_group_levels(parent_group_id);
1145 compact_task_assignments
1146 .into_iter()
1147 .for_each(|task_assignment| {
1148 let task = &task_assignment.compact_task;
1149 if is_compaction_task_expired(
1150 task.compaction_group_version_id,
1151 levels.compaction_group_version_id,
1152 ) {
1153 canceled_tasks.push(ReportTask {
1154 task_id: task.task_id,
1155 task_status: TaskStatus::ManualCanceled,
1156 table_stats_change: HashMap::default(),
1157 sorted_output_ssts: vec![],
1158 object_timestamps: HashMap::default(),
1159 });
1160 }
1161 });
1162 canceled_tasks.sort_by_key(|task| task.task_id);
1163 canceled_tasks.dedup_by_key(|task| task.task_id);
1164
1165 if !canceled_tasks.is_empty() {
1166 self.report_compact_tasks_impl(canceled_tasks, compaction_guard, versioning_guard)
1167 .await?;
1168 }
1169
1170 Ok(())
1171 }
1172
1173 pub async fn normalize_overlapping_compaction_groups(&self) -> Result<usize> {
1180 self.normalize_overlapping_compaction_groups_with_limit(usize::MAX)
1181 .await
1182 }
1183
1184 pub async fn normalize_overlapping_compaction_groups_with_limit(
1185 &self,
1186 max_splits: usize,
1187 ) -> Result<usize> {
1188 let mut split_count = 0usize;
1189 while split_count < max_splits {
1190 let Some(plan) = self.build_normalize_plan().await else {
1191 break;
1192 };
1193
1194 if !self.apply_normalize_plan(&plan).await? {
1195 tracing::debug!(
1196 parent_group_id = %plan.parent_group_id,
1197 boundary_table_id = %plan.boundary_table_id,
1198 "normalize plan became stale before apply"
1199 );
1200 break;
1201 }
1202 split_count += 1;
1203 }
1204
1205 Ok(split_count)
1206 }
1207
1208 pub async fn try_split_compaction_group(
1210 &self,
1211 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
1212 group: CompactionGroupStatistic,
1213 ) {
1214 if group
1215 .compaction_group_config
1216 .compaction_config
1217 .disable_auto_group_scheduling
1218 .unwrap_or(false)
1219 {
1220 return;
1221 }
1222 for (table_id, table_size) in &group.table_statistic {
1224 self.try_move_high_throughput_table_to_dedicated_cg(
1225 table_write_throughput_statistic_manager,
1226 *table_id,
1227 table_size,
1228 group.group_id,
1229 )
1230 .await;
1231 }
1232
1233 self.try_split_huge_compaction_group(group).await;
1235 }
1236
1237 pub async fn try_move_high_throughput_table_to_dedicated_cg(
1239 &self,
1240 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
1241 table_id: TableId,
1242 _table_size: &u64,
1243 parent_group_id: CompactionGroupId,
1244 ) {
1245 let mut table_throughput = table_write_throughput_statistic_manager
1246 .get_table_throughput_descending(
1247 table_id,
1248 self.env.opts.table_stat_throuput_window_seconds_for_split as i64,
1249 )
1250 .peekable();
1251
1252 if table_throughput.peek().is_none() {
1253 return;
1254 }
1255
1256 let is_high_write_throughput = GroupMergeValidator::is_table_high_write_throughput(
1257 table_throughput,
1258 self.env.opts.table_high_write_throughput_threshold,
1259 self.env
1260 .opts
1261 .table_stat_high_write_throughput_ratio_for_split,
1262 );
1263
1264 if !is_high_write_throughput {
1266 return;
1267 }
1268
1269 let ret = self
1270 .move_state_tables_to_dedicated_compaction_group(
1271 parent_group_id,
1272 &[table_id],
1273 Some(self.env.opts.partition_vnode_count),
1274 )
1275 .await;
1276 match ret {
1277 Ok(split_result) => {
1278 tracing::info!(
1279 "split state table [{}] from group-{} success table_vnode_partition_count {:?} split result {:?}",
1280 table_id,
1281 parent_group_id,
1282 self.env.opts.partition_vnode_count,
1283 split_result
1284 );
1285 }
1286 Err(e) => {
1287 tracing::info!(
1288 error = %e.as_report(),
1289 "failed to split state table [{}] from group-{}",
1290 table_id,
1291 parent_group_id,
1292 )
1293 }
1294 }
1295 }
1296
1297 pub async fn try_split_huge_compaction_group(&self, group: CompactionGroupStatistic) {
1298 let group_max_size = (group.compaction_group_config.max_estimated_group_size() as f64
1299 * self.env.opts.split_group_size_ratio) as u64;
1300 let is_huge_hybrid_group =
1301 group.group_size > group_max_size && group.table_statistic.len() > 1; if is_huge_hybrid_group {
1303 let mut accumulated_size = 0;
1304 let mut table_ids = Vec::default();
1305 for (table_id, table_size) in &group.table_statistic {
1306 accumulated_size += table_size;
1307 table_ids.push(*table_id);
1308 assert!(table_ids.is_sorted());
1311 let remaining_size = group.group_size.saturating_sub(accumulated_size);
1312 if accumulated_size > group_max_size / 2
1313 && remaining_size > 0
1314 && table_ids.len() < group.table_statistic.len()
1315 {
1316 let ret = self
1317 .move_state_tables_to_dedicated_compaction_group(
1318 group.group_id,
1319 &table_ids,
1320 None,
1321 )
1322 .await;
1323 match ret {
1324 Ok(split_result) => {
1325 tracing::info!(
1326 "split_huge_compaction_group success {:?}",
1327 split_result
1328 );
1329 self.metrics
1330 .split_compaction_group_count
1331 .with_label_values(&[&group.group_id.to_string()])
1332 .inc();
1333 return;
1334 }
1335 Err(e) => {
1336 tracing::error!(
1337 error = %e.as_report(),
1338 "failed to split_huge_compaction_group table {:?} from group-{}",
1339 table_ids,
1340 group.group_id
1341 );
1342
1343 return;
1344 }
1345 }
1346 }
1347 }
1348 }
1349 }
1350
1351 pub async fn try_merge_compaction_group(
1352 &self,
1353 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
1354 group: &CompactionGroupStatistic,
1355 next_group: &CompactionGroupStatistic,
1356 created_tables: &HashSet<TableId>,
1357 ) -> Result<()> {
1358 GroupMergeValidator::validate_group_merge(
1359 group,
1360 next_group,
1361 created_tables,
1362 table_write_throughput_statistic_manager,
1363 &self.env.opts,
1364 &self.versioning,
1365 )
1366 .await?;
1367
1368 let result = self
1369 .merge_compaction_group(group.group_id, next_group.group_id)
1370 .await;
1371
1372 match &result {
1373 Ok(()) => {
1374 tracing::info!(
1375 "merge group-{} to group-{}",
1376 next_group.group_id,
1377 group.group_id,
1378 );
1379
1380 self.metrics
1381 .merge_compaction_group_count
1382 .with_label_values(&[&group.group_id.to_string()])
1383 .inc();
1384 }
1385 Err(e) => {
1386 tracing::info!(
1387 error = %e.as_report(),
1388 "failed to merge group-{} group-{}",
1389 next_group.group_id,
1390 group.group_id,
1391 );
1392 }
1393 }
1394
1395 result
1396 }
1397}
1398
1399#[derive(Debug, Default)]
1400struct GroupMergeValidator {}
1401
1402impl GroupMergeValidator {
1403 fn is_merge_compatible_by_semantics(
1406 group: &CompactionGroupStatistic,
1407 next_group: &CompactionGroupStatistic,
1408 ) -> bool {
1409 let (mut left, mut right) = (
1410 group
1411 .compaction_group_config
1412 .compaction_config
1413 .as_ref()
1414 .clone(),
1415 next_group
1416 .compaction_group_config
1417 .compaction_config
1418 .as_ref()
1419 .clone(),
1420 );
1421 left.split_weight_by_vnode = 0;
1422 right.split_weight_by_vnode = 0;
1423 left == right
1424 }
1425
1426 pub fn is_table_high_write_throughput(
1428 table_throughput: impl Iterator<Item = &TableWriteThroughputStatistic>,
1429 threshold: u64,
1430 high_write_throughput_ratio: f64,
1431 ) -> bool {
1432 let mut sample_size = 0;
1433 let mut high_write_throughput_count = 0;
1434 for statistic in table_throughput {
1435 sample_size += 1;
1436 if statistic.throughput > threshold {
1437 high_write_throughput_count += 1;
1438 }
1439 }
1440
1441 high_write_throughput_count as f64 > sample_size as f64 * high_write_throughput_ratio
1442 }
1443
1444 pub fn is_table_low_write_throughput(
1445 table_throughput: impl Iterator<Item = &TableWriteThroughputStatistic>,
1446 threshold: u64,
1447 low_write_throughput_ratio: f64,
1448 ) -> bool {
1449 let mut sample_size = 0;
1450 let mut low_write_throughput_count = 0;
1451 for statistic in table_throughput {
1452 sample_size += 1;
1453 if statistic.throughput <= threshold {
1454 low_write_throughput_count += 1;
1455 }
1456 }
1457
1458 low_write_throughput_count as f64 > sample_size as f64 * low_write_throughput_ratio
1459 }
1460
1461 fn check_is_low_write_throughput_compaction_group(
1462 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
1463 group: &CompactionGroupStatistic,
1464 opts: &Arc<MetaOpts>,
1465 ) -> bool {
1466 let mut table_with_statistic = Vec::with_capacity(group.table_statistic.len());
1467 for table_id in group.table_statistic.keys() {
1468 let mut table_throughput = table_write_throughput_statistic_manager
1469 .get_table_throughput_descending(
1470 *table_id,
1471 opts.table_stat_throuput_window_seconds_for_merge as i64,
1472 )
1473 .peekable();
1474 if table_throughput.peek().is_none() {
1475 continue;
1476 }
1477
1478 table_with_statistic.push(table_throughput);
1479 }
1480
1481 if table_with_statistic.is_empty() {
1483 return true;
1484 }
1485
1486 table_with_statistic.into_iter().all(|table_throughput| {
1488 Self::is_table_low_write_throughput(
1489 table_throughput,
1490 opts.table_low_write_throughput_threshold,
1491 opts.table_stat_low_write_throughput_ratio_for_merge,
1492 )
1493 })
1494 }
1495
1496 fn check_is_creating_compaction_group(
1497 group: &CompactionGroupStatistic,
1498 created_tables: &HashSet<TableId>,
1499 ) -> bool {
1500 group
1501 .table_statistic
1502 .keys()
1503 .any(|table_id| !created_tables.contains(table_id))
1504 }
1505
1506 async fn validate_group_merge(
1507 group: &CompactionGroupStatistic,
1508 next_group: &CompactionGroupStatistic,
1509 created_tables: &HashSet<TableId>,
1510 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
1511 opts: &Arc<MetaOpts>,
1512 versioning: &MonitoredRwLock<Versioning>,
1513 ) -> Result<()> {
1514 if (group.group_id == StaticCompactionGroupId::StateDefault
1516 && next_group.group_id == StaticCompactionGroupId::MaterializedView)
1517 || (group.group_id == StaticCompactionGroupId::MaterializedView
1518 && next_group.group_id == StaticCompactionGroupId::StateDefault)
1519 {
1520 return Err(Error::CompactionGroup(format!(
1521 "group-{} and group-{} are both StaticCompactionGroupId",
1522 group.group_id, next_group.group_id
1523 )));
1524 }
1525
1526 if group.table_statistic.is_empty() || next_group.table_statistic.is_empty() {
1527 return Err(Error::CompactionGroup(format!(
1528 "group-{} or group-{} is empty",
1529 group.group_id, next_group.group_id
1530 )));
1531 }
1532
1533 {
1538 let mut table_ids_1: Vec<TableId> = group.table_statistic.keys().cloned().collect_vec();
1539 let mut table_ids_2: Vec<TableId> =
1540 next_group.table_statistic.keys().cloned().collect_vec();
1541 table_ids_1.sort();
1542 table_ids_2.sort();
1543 if table_ids_1.first().unwrap() > table_ids_2.first().unwrap() {
1544 std::mem::swap(&mut table_ids_1, &mut table_ids_2);
1545 }
1546 if table_ids_1.last().unwrap() >= table_ids_2.first().unwrap() {
1547 return Err(Error::CompactionGroup(format!(
1548 "group-{} and group-{} have overlapping table id ranges, not mergeable",
1549 group.group_id, next_group.group_id
1550 )));
1551 }
1552 }
1553
1554 if group
1555 .compaction_group_config
1556 .compaction_config
1557 .disable_auto_group_scheduling
1558 .unwrap_or(false)
1559 || next_group
1560 .compaction_group_config
1561 .compaction_config
1562 .disable_auto_group_scheduling
1563 .unwrap_or(false)
1564 {
1565 return Err(Error::CompactionGroup(format!(
1566 "group-{} or group-{} disable_auto_group_scheduling",
1567 group.group_id, next_group.group_id
1568 )));
1569 }
1570
1571 if !Self::is_merge_compatible_by_semantics(group, next_group) {
1574 let left_config = group.compaction_group_config.compaction_config.as_ref();
1575 let right_config = next_group
1576 .compaction_group_config
1577 .compaction_config
1578 .as_ref();
1579
1580 tracing::warn!(
1581 group_id = %group.group_id,
1582 next_group_id = %next_group.group_id,
1583 left_config = ?left_config,
1584 right_config = ?right_config,
1585 "compaction config semantic mismatch detected while merging compaction groups"
1586 );
1587
1588 return Err(Error::CompactionGroup(format!(
1589 "Cannot merge group {} and next_group {} with different compaction config (split_weight_by_vnode is excluded from comparison). left_config: {:?}, right_config: {:?}",
1590 group.group_id, next_group.group_id, left_config, right_config
1591 )));
1592 }
1593
1594 if Self::check_is_creating_compaction_group(group, created_tables) {
1596 return Err(Error::CompactionGroup(format!(
1597 "Cannot merge creating group {} next_group {}",
1598 group.group_id, next_group.group_id
1599 )));
1600 }
1601
1602 if !Self::check_is_low_write_throughput_compaction_group(
1604 table_write_throughput_statistic_manager,
1605 group,
1606 opts,
1607 ) {
1608 return Err(Error::CompactionGroup(format!(
1609 "Cannot merge high throughput group {} next_group {}",
1610 group.group_id, next_group.group_id
1611 )));
1612 }
1613
1614 let size_limit = (group.compaction_group_config.max_estimated_group_size() as f64
1615 * opts.split_group_size_ratio) as u64;
1616
1617 if (group.group_size + next_group.group_size) > size_limit {
1618 return Err(Error::CompactionGroup(format!(
1619 "Cannot merge huge group {} group_size {} next_group {} next_group_size {} size_limit {}",
1620 group.group_id,
1621 group.group_size,
1622 next_group.group_id,
1623 next_group.group_size,
1624 size_limit
1625 )));
1626 }
1627
1628 if Self::check_is_creating_compaction_group(next_group, created_tables) {
1629 return Err(Error::CompactionGroup(format!(
1630 "Cannot merge creating group {} next group {}",
1631 group.group_id, next_group.group_id
1632 )));
1633 }
1634
1635 if !Self::check_is_low_write_throughput_compaction_group(
1636 table_write_throughput_statistic_manager,
1637 next_group,
1638 opts,
1639 ) {
1640 return Err(Error::CompactionGroup(format!(
1641 "Cannot merge high throughput group {} next group {}",
1642 group.group_id, next_group.group_id
1643 )));
1644 }
1645
1646 {
1647 let versioning_guard = versioning
1649 .read_with_process_name("validate_group_merge")
1650 .await;
1651 let levels = &versioning_guard.current_version.levels;
1652 if !levels.contains_key(&group.group_id) {
1653 return Err(Error::CompactionGroup(format!(
1654 "cannot merge compaction group {} because it does not exist",
1655 group.group_id
1656 )));
1657 }
1658
1659 if !levels.contains_key(&next_group.group_id) {
1660 return Err(Error::CompactionGroup(format!(
1661 "cannot merge next compaction group {} because it does not exist",
1662 next_group.group_id
1663 )));
1664 }
1665
1666 let group_levels = versioning_guard
1667 .current_version
1668 .get_compaction_group_levels(group.group_id);
1669
1670 let next_group_levels = versioning_guard
1671 .current_version
1672 .get_compaction_group_levels(next_group.group_id);
1673
1674 let group_state = GroupStateValidator::group_state(
1675 group_levels,
1676 group.compaction_group_config.compaction_config().deref(),
1677 );
1678
1679 if group_state.is_write_stop() || group_state.is_emergency() {
1680 return Err(Error::CompactionGroup(format!(
1681 "Cannot merge write limit group {} next group {}",
1682 group.group_id, next_group.group_id
1683 )));
1684 }
1685
1686 let next_group_state = GroupStateValidator::group_state(
1687 next_group_levels,
1688 next_group
1689 .compaction_group_config
1690 .compaction_config()
1691 .deref(),
1692 );
1693
1694 if next_group_state.is_write_stop() || next_group_state.is_emergency() {
1695 return Err(Error::CompactionGroup(format!(
1696 "Cannot merge write limit next group {} group {}",
1697 next_group.group_id, group.group_id
1698 )));
1699 }
1700
1701 let l0_sub_level_count_after_merge =
1703 group_levels.l0.sub_levels.len() + next_group_levels.l0.sub_levels.len();
1704 if GroupStateValidator::write_stop_sub_level_count(
1705 (l0_sub_level_count_after_merge as f64
1706 * opts.compaction_group_merge_dimension_threshold) as usize,
1707 group.compaction_group_config.compaction_config().deref(),
1708 ) {
1709 return Err(Error::CompactionGroup(format!(
1710 "Cannot merge write limit group {} next group {}, will trigger write stop after merge",
1711 group.group_id, next_group.group_id
1712 )));
1713 }
1714
1715 let l0_file_count_after_merge = group_levels
1716 .l0
1717 .sub_levels
1718 .iter()
1719 .chain(next_group_levels.l0.sub_levels.iter())
1720 .map(|level| level.table_infos.len())
1721 .sum::<usize>();
1722 if GroupStateValidator::write_stop_l0_file_count(
1723 (l0_file_count_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
1724 as usize,
1725 group.compaction_group_config.compaction_config().deref(),
1726 ) {
1727 return Err(Error::CompactionGroup(format!(
1728 "Cannot merge write limit next group {} group {}, will trigger write stop after merge",
1729 next_group.group_id, group.group_id
1730 )));
1731 }
1732
1733 let l0_size_after_merge =
1734 group_levels.l0.total_file_size + next_group_levels.l0.total_file_size;
1735
1736 if GroupStateValidator::write_stop_l0_size(
1737 (l0_size_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
1738 as u64,
1739 group.compaction_group_config.compaction_config().deref(),
1740 ) {
1741 return Err(Error::CompactionGroup(format!(
1742 "Cannot merge write limit next group {} group {}, will trigger write stop after merge",
1743 next_group.group_id, group.group_id
1744 )));
1745 }
1746
1747 if GroupStateValidator::emergency_l0_file_count(
1749 (l0_file_count_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
1750 as usize,
1751 group.compaction_group_config.compaction_config().deref(),
1752 ) {
1753 return Err(Error::CompactionGroup(format!(
1754 "Cannot merge emergency group {} next group {}, will trigger emergency after merge",
1755 group.group_id, next_group.group_id
1756 )));
1757 }
1758 }
1759
1760 Ok(())
1761 }
1762}