1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
19use std::ops::DerefMut;
20use std::sync::Arc;
21
22use bytes::Bytes;
23use itertools::Itertools;
24use risingwave_common::catalog::TableId;
25use risingwave_common::hash::VirtualNode;
26use risingwave_hummock_sdk::compact_task::{ReportTask, is_compaction_task_expired};
27use risingwave_hummock_sdk::compaction_group::{StateTableId, group_split};
28use risingwave_hummock_sdk::version::{GroupDelta, GroupDeltas};
29use risingwave_hummock_sdk::{CompactionGroupId, can_concat};
30use risingwave_pb::hummock::compact_task::{TaskStatus, TaskType};
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::{CompactionGroupStatistic, merge_policy};
38use crate::hummock::error::{Error, Result};
39use crate::hummock::manager::transaction::HummockVersionTransaction;
40use crate::hummock::manager::{HummockManager, commit_multi_var};
41use crate::hummock::metrics_utils::remove_compaction_group_metrics;
42use crate::hummock::sequence::{next_compaction_group_id, next_sstable_id};
43
44impl HummockManager {
45 pub async fn merge_compaction_group(
46 &self,
47 group_1: CompactionGroupId,
48 group_2: CompactionGroupId,
49 ) -> Result<()> {
50 self.merge_compaction_group_impl(group_1, group_2, None, false)
51 .await
52 .map(|_| ())
53 }
54
55 pub async fn merge_compaction_group_for_test(
56 &self,
57 group_1: CompactionGroupId,
58 group_2: CompactionGroupId,
59 created_tables: HashSet<TableId>,
60 ) -> Result<()> {
61 self.merge_compaction_group_impl(group_1, group_2, Some(&created_tables), false)
62 .await
63 .map(|_| ())
64 }
65
66 pub async fn merge_compaction_group_impl(
67 &self,
68 group_1: CompactionGroupId,
69 group_2: CompactionGroupId,
70 created_tables: Option<&HashSet<TableId>>,
71 validate_policy: bool,
72 ) -> Result<CompactionGroupStatistic> {
73 let fetched_created_tables;
76 let created_tables = if let Some(created_tables) = created_tables {
77 created_tables
80 } else {
81 fetched_created_tables = match self.metadata_manager.get_created_table_ids().await {
82 Ok(created_tables) => HashSet::from_iter(created_tables),
83 Err(err) => {
84 tracing::warn!(target: super::TRACE_TARGET, error = %err.as_report(), "failed to fetch created table ids");
85 return Err(Error::CompactionGroup(format!(
86 "merge group_1 {} group_2 {} failed to fetch created table ids",
87 group_1, group_2
88 )));
89 }
90 };
91 &fetched_created_tables
92 };
93 let compaction_guard = self
94 .compaction
95 .write_with_process_name("merge_compaction_group_impl")
96 .await;
97 let mut versioning_guard = self
98 .versioning
99 .write_with_process_name("merge_compaction_group_impl")
100 .await;
101 let versioning = versioning_guard.deref_mut();
102 if !versioning.current_version.levels.contains_key(&group_1) {
104 return Err(Error::CompactionGroup(format!("invalid group {}", group_1)));
105 }
106
107 if !versioning.current_version.levels.contains_key(&group_2) {
108 return Err(Error::CompactionGroup(format!("invalid group {}", group_2)));
109 }
110
111 let state_table_info = &versioning.current_version.state_table_info;
112 let mut member_table_ids_1 = state_table_info
113 .compaction_group_member_table_ids(group_1)
114 .iter()
115 .cloned()
116 .collect_vec();
117
118 if member_table_ids_1.is_empty() {
119 return Err(Error::CompactionGroup(format!(
120 "group_1 {} is empty",
121 group_1
122 )));
123 }
124
125 let mut member_table_ids_2 = state_table_info
126 .compaction_group_member_table_ids(group_2)
127 .iter()
128 .cloned()
129 .collect_vec();
130
131 if member_table_ids_2.is_empty() {
132 return Err(Error::CompactionGroup(format!(
133 "group_2 {} is empty",
134 group_2
135 )));
136 }
137
138 debug_assert!(!member_table_ids_1.is_empty());
139 debug_assert!(!member_table_ids_2.is_empty());
140 assert!(member_table_ids_1.is_sorted());
141 assert!(member_table_ids_2.is_sorted());
142
143 if member_table_ids_1
145 .iter()
146 .chain(&member_table_ids_2)
147 .any(|table_id| !created_tables.contains(table_id))
148 {
149 return Err(Error::CompactionGroup(format!(
150 "Cannot merge creating group {} next_group {} member_table_ids_1 {:?} member_table_ids_2 {:?}",
151 group_1, group_2, member_table_ids_1, member_table_ids_2
152 )));
153 }
154
155 let (left_group_id, right_group_id) =
157 if member_table_ids_1.first().unwrap() < member_table_ids_2.first().unwrap() {
158 (group_1, group_2)
159 } else {
160 std::mem::swap(&mut member_table_ids_1, &mut member_table_ids_2);
161 (group_2, group_1)
162 };
163
164 if member_table_ids_1.last().unwrap() >= member_table_ids_2.first().unwrap() {
169 return Err(Error::CompactionGroup(format!(
170 "invalid merge group_1 {} group_2 {}: table id ranges overlap",
171 left_group_id, right_group_id
172 )));
173 }
174
175 let combined_member_table_ids = member_table_ids_1
176 .iter()
177 .chain(member_table_ids_2.iter())
178 .collect_vec();
179 assert!(combined_member_table_ids.is_sorted());
180
181 let survivor_tables = combined_member_table_ids
184 .iter()
185 .map(|&&table_id| {
186 let size = versioning
187 .version_stats
188 .table_stats
189 .get(&table_id)
190 .map(|stats| (stats.total_key_size + stats.total_value_size).max(0) as u64)
191 .unwrap_or(0);
192 (table_id, size)
193 })
194 .collect_vec();
195
196 let mut compaction_group_manager = self
197 .compaction_group_manager
198 .write_with_process_name("merge_compaction_group_impl")
199 .await;
200 if validate_policy {
201 let group = compaction_group_manager
202 .try_get_compaction_group_config(group_1)
203 .ok_or_else(|| Error::CompactionGroup(format!("invalid group {group_1}")))?;
204 let next_group = compaction_group_manager
205 .try_get_compaction_group_config(group_2)
206 .ok_or_else(|| Error::CompactionGroup(format!("invalid group {group_2}")))?;
207 let current_size = survivor_tables.iter().map(|(_, size)| size).sum();
208 merge_policy::validate_group_config(&group, &next_group, current_size, &self.env.opts)?;
211 merge_policy::validate_group_levels(
212 &group,
213 &next_group,
214 &self.env.opts,
215 &versioning.current_version,
216 )?;
217 if !merge_policy::check_is_low_write_throughput(
220 &self.table_write_throughput_statistic_manager.read(),
221 combined_member_table_ids.iter().map(|&&table_id| table_id),
222 &self.env.opts,
223 ) {
224 return Err(Error::CompactionGroup(format!(
225 "Cannot merge groups {group_1} and {group_2}: current throughput is not observed cold"
226 )));
227 }
228 }
229
230 let mut sst_id_set = HashSet::new();
232 for sst_id in versioning
233 .current_version
234 .get_sst_ids_by_group_id(left_group_id)
235 .chain(
236 versioning
237 .current_version
238 .get_sst_ids_by_group_id(right_group_id),
239 )
240 {
241 if !sst_id_set.insert(sst_id) {
242 return Err(Error::CompactionGroup(format!(
243 "invalid merge group_1 {} group_2 {} duplicated sst_id {}",
244 left_group_id, right_group_id, sst_id
245 )));
246 }
247 }
248
249 {
251 let left_levels = versioning
252 .current_version
253 .get_compaction_group_levels(left_group_id);
254
255 let right_levels = versioning
256 .current_version
257 .get_compaction_group_levels(right_group_id);
258
259 let max_level = std::cmp::max(left_levels.levels.len(), right_levels.levels.len());
262 for level_idx in 1..=max_level {
263 let left_level = left_levels.get_level(level_idx);
264 let right_level = right_levels.get_level(level_idx);
265 if left_level.table_infos.is_empty() || right_level.table_infos.is_empty() {
266 continue;
267 }
268
269 let left_last_sst = left_level.table_infos.last().unwrap().clone();
270 let right_first_sst = right_level.table_infos.first().unwrap().clone();
271 let left_sst_id = left_last_sst.sst_id;
272 let right_sst_id = right_first_sst.sst_id;
273 let left_obj_id = left_last_sst.object_id;
274 let right_obj_id = right_first_sst.object_id;
275
276 if !can_concat(&[left_last_sst, right_first_sst]) {
278 return Err(Error::CompactionGroup(format!(
279 "invalid merge group_1 {} group_2 {} level_idx {} left_last_sst_id {} right_first_sst_id {} left_obj_id {} right_obj_id {}",
280 left_group_id,
281 right_group_id,
282 level_idx,
283 left_sst_id,
284 right_sst_id,
285 left_obj_id,
286 right_obj_id
287 )));
288 }
289 }
290 }
291
292 let mut version = HummockVersionTransaction::new(
293 &mut versioning.current_version,
294 &mut versioning.hummock_version_deltas,
295 &mut versioning.table_change_log,
296 &self.gc_manager.change_log_retention,
297 self.env.notification_manager(),
298 None,
299 &self.metrics,
300 &self.env.opts,
301 &self.version_stat_tx,
302 );
303 let mut new_version_delta = version.new_delta();
304
305 let target_compaction_group_id = {
306 new_version_delta.group_deltas.insert(
308 left_group_id,
309 GroupDeltas {
310 group_deltas: vec![GroupDelta::GroupMerge(PbGroupMerge {
311 left_group_id,
312 right_group_id,
313 })],
314 },
315 );
316 left_group_id
317 };
318
319 new_version_delta.with_latest_version(|version, new_version_delta| {
322 for &table_id in combined_member_table_ids {
323 let info = version
324 .state_table_info
325 .info()
326 .get(&table_id)
327 .expect("have check exist previously");
328 assert!(
329 new_version_delta
330 .state_table_info_delta
331 .insert(
332 table_id,
333 PbStateTableInfoDelta {
334 committed_epoch: info.committed_epoch,
335 compaction_group_id: target_compaction_group_id,
336 }
337 )
338 .is_none()
339 );
340 }
341 });
342
343 let survivor_config = {
344 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
345
346 {
348 let right_group_max_level = new_version_delta
349 .latest_version()
350 .get_compaction_group_levels(right_group_id)
351 .levels
352 .len();
353
354 remove_compaction_group_metrics(
355 &self.metrics,
356 right_group_id,
357 right_group_max_level,
358 );
359 }
360
361 {
363 if let Err(err) = compaction_groups_txn.update_compaction_config(
364 &[left_group_id],
365 &[MutableConfig::SplitWeightByVnode(0)], ) {
367 tracing::error!(target: super::TRACE_TARGET,
368 error = %err.as_report(),
369 "failed to update compaction config for group-{}",
370 left_group_id
371 );
372 }
373 }
374
375 new_version_delta.pre_apply();
376
377 compaction_groups_txn.remove(right_group_id);
379 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
380 compaction_group_manager
381 .try_get_compaction_group_config(left_group_id)
382 .expect("merged group config should exist")
383 };
384
385 drop(compaction_group_manager);
386
387 self.compaction_state
389 .remove_compaction_group(right_group_id);
390 if !self.env.opts.compaction_deterministic_test {
391 self.try_send_compaction_request(left_group_id, TaskType::Dynamic);
392 }
393
394 versioning.mark_next_time_travel_version_snapshot();
396
397 let mut canceled_tasks = vec![];
399 let compact_task_assignments =
402 compaction_guard.get_compact_task_assignments_by_group_id(right_group_id);
403 compact_task_assignments
404 .into_iter()
405 .for_each(|task_assignment| {
406 let task = &task_assignment.compact_task;
407 assert_eq!(task.compaction_group_id, right_group_id);
408 canceled_tasks.push(ReportTask {
409 task_id: task.task_id,
410 task_status: TaskStatus::ManualCanceled,
411 table_stats_change: HashMap::default(),
412 sorted_output_ssts: vec![],
413 object_timestamps: HashMap::default(),
414 });
415 });
416
417 if !canceled_tasks.is_empty() {
418 self.report_compact_tasks_impl(canceled_tasks, compaction_guard, versioning_guard)
419 .await?;
420 } else {
421 drop(versioning_guard);
422 drop(compaction_guard);
423 }
424
425 self.try_update_write_limits(&[left_group_id, right_group_id])
426 .await;
427
428 self.metrics
430 .merge_compaction_group_count
431 .with_label_values(&[&left_group_id.to_string()])
432 .inc();
433
434 Ok(CompactionGroupStatistic {
437 group_id: left_group_id,
438 group_size: survivor_tables.iter().map(|(_, size)| size).sum(),
439 table_statistic: survivor_tables.into_iter().collect(),
440 compaction_group_config: survivor_config,
441 })
442 }
443}
444
445impl HummockManager {
446 async fn split_compaction_group_impl(
458 &self,
459 parent_group_id: CompactionGroupId,
460 split_table_ids: &[StateTableId],
461 table_id_to_split: StateTableId,
462 vnode_to_split: VirtualNode,
463 partition_vnode_count: Option<u32>,
464 validate_policy: bool,
465 ) -> Result<Vec<(CompactionGroupId, Vec<StateTableId>)>> {
466 let mut result = vec![];
467 let compaction_guard = self
468 .compaction
469 .write_with_process_name("split_compaction_group_impl")
470 .await;
471 let mut versioning_guard = self
472 .versioning
473 .write_with_process_name("split_compaction_group_impl")
474 .await;
475 let versioning = versioning_guard.deref_mut();
476 if !versioning
478 .current_version
479 .levels
480 .contains_key(&parent_group_id)
481 {
482 return Err(Error::CompactionGroup(format!(
483 "invalid group {}",
484 parent_group_id
485 )));
486 }
487
488 let member_table_ids = versioning
489 .current_version
490 .state_table_info
491 .compaction_group_member_table_ids(parent_group_id)
492 .iter()
493 .copied()
494 .collect::<BTreeSet<_>>();
495
496 if !member_table_ids.contains(&table_id_to_split) {
497 return Err(Error::CompactionGroup(format!(
498 "table {} doesn't in group {}",
499 table_id_to_split, parent_group_id
500 )));
501 }
502
503 let split_full_key = group_split::build_split_full_key(table_id_to_split, vnode_to_split);
504
505 let table_ids = member_table_ids.into_iter().collect_vec();
507 if table_ids == split_table_ids {
508 return Err(Error::CompactionGroup(format!(
509 "invalid split attempt for group {}: all member tables are moved",
510 parent_group_id
511 )));
512 }
513 let (table_ids_left, table_ids_right) =
515 group_split::split_table_ids_with_table_id_and_vnode(
516 &table_ids,
517 split_full_key.user_key.table_id,
518 split_full_key.user_key.get_vnode_id(),
519 );
520 if table_ids_left.is_empty() || table_ids_right.is_empty() {
521 if !table_ids_left.is_empty() {
523 result.push((parent_group_id, table_ids_left));
524 }
525
526 if !table_ids_right.is_empty() {
527 result.push((parent_group_id, table_ids_right));
528 }
529 return Ok(result);
530 }
531
532 result.push((parent_group_id, table_ids_left));
533
534 let split_key: Bytes = split_full_key.encode().into();
535
536 let mut version = HummockVersionTransaction::new(
537 &mut versioning.current_version,
538 &mut versioning.hummock_version_deltas,
539 &mut versioning.table_change_log,
540 &self.gc_manager.change_log_retention,
541 self.env.notification_manager(),
542 None,
543 &self.metrics,
544 &self.env.opts,
545 &self.version_stat_tx,
546 );
547 let mut new_version_delta = version.new_delta();
548
549 let split_sst_count = new_version_delta
550 .latest_version()
551 .count_new_ssts_in_group_split(parent_group_id, split_key.clone());
552
553 let new_sst_start_id = next_sstable_id(&self.env, split_sst_count).await?;
554 let (new_compaction_group_id, config) = {
555 let new_compaction_group_id = next_compaction_group_id(&self.env).await?;
557 let config = self
559 .compaction_group_manager
560 .read_with_process_name("split_compaction_group_impl")
561 .await
562 .try_get_compaction_group_config(parent_group_id)
563 .ok_or_else(|| {
564 Error::CompactionGroup(format!(
565 "parent group {} config not found",
566 parent_group_id
567 ))
568 })?
569 .compaction_config()
570 .as_ref()
571 .clone();
572
573 #[expect(deprecated)]
574 new_version_delta.group_deltas.insert(
576 new_compaction_group_id,
577 GroupDeltas {
578 group_deltas: vec![GroupDelta::GroupConstruct(Box::new(PbGroupConstruct {
579 group_config: Some(config.clone()),
580 group_id: new_compaction_group_id,
581 parent_group_id,
582 new_sst_start_id,
583 table_ids: vec![],
584 version: CompatibilityVersion::LATEST as _, split_key: Some(split_key.into()),
586 }))],
587 },
588 );
589 (new_compaction_group_id, config)
590 };
591
592 new_version_delta.with_latest_version(|version, new_version_delta| {
593 for &table_id in &table_ids_right {
594 let info = version
595 .state_table_info
596 .info()
597 .get(&table_id)
598 .expect("have check exist previously");
599 assert!(
600 new_version_delta
601 .state_table_info_delta
602 .insert(
603 table_id,
604 PbStateTableInfoDelta {
605 committed_epoch: info.committed_epoch,
606 compaction_group_id: new_compaction_group_id,
607 }
608 )
609 .is_none()
610 );
611 }
612 });
613
614 result.push((new_compaction_group_id, table_ids_right));
615
616 {
617 let mut compaction_group_manager = self
618 .compaction_group_manager
619 .write_with_process_name("split_compaction_group_impl")
620 .await;
621 if validate_policy
624 && compaction_group_manager
625 .try_get_compaction_group_config(parent_group_id)
626 .expect("current parent config should exist")
627 .compaction_config
628 .disable_auto_group_scheduling
629 .unwrap_or(false)
630 {
631 return Err(Error::CompactionGroup(format!(
632 "group {parent_group_id} disables automatic splitting"
633 )));
634 }
635 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
636 compaction_groups_txn
637 .create_compaction_groups(new_compaction_group_id, Arc::new(config));
638
639 for (cg_id, table_ids) in &result {
643 if let Some(partition_vnode_count) = partition_vnode_count
645 && table_ids.len() == 1
646 && table_ids == split_table_ids
647 && let Err(err) = compaction_groups_txn.update_compaction_config(
648 &[*cg_id],
649 &[MutableConfig::SplitWeightByVnode(partition_vnode_count)],
650 )
651 {
652 tracing::error!(target: super::TRACE_TARGET,
653 error = %err.as_report(),
654 "failed to update compaction config for group-{}",
655 cg_id
656 );
657 }
658 }
659
660 new_version_delta.pre_apply();
661 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
662 }
663 if !self.env.opts.compaction_deterministic_test {
664 for (group_id, _) in &result {
665 self.try_send_compaction_request(*group_id, TaskType::Dynamic);
666 }
667 }
668 versioning.mark_next_time_travel_version_snapshot();
670
671 let mut canceled_tasks = vec![];
674 let compact_task_assignments =
675 compaction_guard.get_compact_task_assignments_by_group_id(parent_group_id);
676 let levels = versioning
677 .current_version
678 .get_compaction_group_levels(parent_group_id);
679 compact_task_assignments
680 .into_iter()
681 .for_each(|task_assignment| {
682 let task = &task_assignment.compact_task;
683 let is_expired = is_compaction_task_expired(
684 task.compaction_group_version_id,
685 levels.compaction_group_version_id,
686 );
687 if is_expired {
688 canceled_tasks.push(ReportTask {
689 task_id: task.task_id,
690 task_status: TaskStatus::ManualCanceled,
691 table_stats_change: HashMap::default(),
692 sorted_output_ssts: vec![],
693 object_timestamps: HashMap::default(),
694 });
695 }
696 });
697
698 if !canceled_tasks.is_empty() {
699 self.report_compact_tasks_impl(canceled_tasks, compaction_guard, versioning_guard)
700 .await?;
701 } else {
702 drop(versioning_guard);
703 drop(compaction_guard);
704 }
705
706 let affected_group_ids = result.iter().map(|(cg_id, _)| *cg_id).collect_vec();
707 self.try_update_write_limits(&affected_group_ids).await;
708
709 self.metrics
710 .split_compaction_group_count
711 .with_label_values(&[&parent_group_id.to_string()])
712 .inc();
713
714 Ok(result)
715 }
716
717 pub async fn move_state_tables_to_dedicated_compaction_group(
720 &self,
721 parent_group_id: CompactionGroupId,
722 table_ids: &[StateTableId],
723 partition_vnode_count: Option<u32>,
724 ) -> Result<(
725 CompactionGroupId,
726 BTreeMap<CompactionGroupId, Vec<StateTableId>>,
727 )> {
728 self.move_state_tables_to_dedicated_compaction_group_impl(
729 parent_group_id,
730 table_ids,
731 partition_vnode_count,
732 false,
733 )
734 .await
735 }
736
737 pub(super) async fn move_state_tables_to_dedicated_compaction_group_impl(
738 &self,
739 parent_group_id: CompactionGroupId,
740 table_ids: &[StateTableId],
741 partition_vnode_count: Option<u32>,
742 validate_policy: bool,
743 ) -> Result<(
744 CompactionGroupId,
745 BTreeMap<CompactionGroupId, Vec<StateTableId>>,
746 )> {
747 if table_ids.is_empty() {
748 return Err(Error::CompactionGroup(
749 "table_ids must not be empty".to_owned(),
750 ));
751 }
752
753 if !table_ids.is_sorted() {
754 return Err(Error::CompactionGroup(
755 "table_ids must be sorted".to_owned(),
756 ));
757 }
758
759 let parent_table_ids = {
760 let versioning_guard = self
761 .versioning
762 .read_with_process_name("move_state_tables_to_dedicated_compaction_group")
763 .await;
764 versioning_guard
765 .current_version
766 .state_table_info
767 .compaction_group_member_table_ids(parent_group_id)
768 .iter()
769 .copied()
770 .collect_vec()
771 };
772
773 if parent_table_ids == table_ids {
774 return Err(Error::CompactionGroup(format!(
775 "invalid split attempt for group {}: all member tables are moved",
776 parent_group_id
777 )));
778 }
779
780 fn check_table_ids_valid(cg_id_to_table_ids: &BTreeMap<CompactionGroupId, Vec<TableId>>) {
781 {
783 cg_id_to_table_ids
784 .iter()
785 .for_each(|(_cg_id, table_ids)| assert!(table_ids.is_sorted()));
786 }
787
788 {
790 let mut table_table_ids_vec = cg_id_to_table_ids.values().cloned().collect_vec();
791 table_table_ids_vec.sort_by(|a, b| a[0].cmp(&b[0]));
792 assert!(table_table_ids_vec.concat().is_sorted());
793 }
794
795 {
797 let mut all_table_ids = HashSet::new();
798 for table_ids in cg_id_to_table_ids.values() {
799 for table_id in table_ids {
800 assert!(all_table_ids.insert(*table_id));
801 }
802 }
803 }
804 }
805
806 let mut cg_id_to_table_ids: BTreeMap<CompactionGroupId, Vec<TableId>> = BTreeMap::new();
816 let table_id_to_split = *table_ids.first().unwrap();
817 let mut target_compaction_group_id: CompactionGroupId = 0.into();
818 let result_vec = self
819 .split_compaction_group_impl(
820 parent_group_id,
821 table_ids,
822 table_id_to_split,
823 VirtualNode::ZERO,
824 partition_vnode_count,
825 validate_policy,
826 )
827 .await?;
828 assert!(result_vec.len() <= 2);
829
830 let mut finish_move = false;
831 for (cg_id, table_ids_after_split) in result_vec {
832 if table_ids_after_split.contains(&table_id_to_split) {
833 target_compaction_group_id = cg_id;
834 }
835
836 if table_ids_after_split == table_ids {
837 finish_move = true;
838 }
839
840 cg_id_to_table_ids.insert(cg_id, table_ids_after_split);
841 }
842 check_table_ids_valid(&cg_id_to_table_ids);
843
844 if finish_move {
845 return Ok((target_compaction_group_id, cg_id_to_table_ids));
846 }
847
848 let table_id_to_split = *table_ids.last().unwrap();
851 let result_vec = self
852 .split_compaction_group_impl(
853 target_compaction_group_id,
854 table_ids,
855 table_id_to_split,
856 VirtualNode::MAX_REPRESENTABLE,
857 partition_vnode_count,
858 validate_policy,
859 )
860 .await?;
861 assert!(result_vec.len() <= 2);
862 for (cg_id, table_ids_after_split) in result_vec {
863 if table_ids_after_split.contains(&table_id_to_split) {
864 target_compaction_group_id = cg_id;
865 }
866 cg_id_to_table_ids.insert(cg_id, table_ids_after_split);
867 }
868 check_table_ids_valid(&cg_id_to_table_ids);
869
870 Ok((target_compaction_group_id, cg_id_to_table_ids))
871 }
872}