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