1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
16use std::ops::DerefMut;
17use std::sync::Arc;
18
19use itertools::Itertools;
20use risingwave_common::catalog::TableId;
21use risingwave_common::config::meta::default::compaction_config as default_compaction_config;
22use risingwave_common::util::epoch::INVALID_EPOCH;
23use risingwave_hummock_sdk::CompactionGroupId;
24use risingwave_hummock_sdk::compaction_group::StaticCompactionGroupId;
25use risingwave_hummock_sdk::compaction_group::hummock_version_ext::get_compaction_group_ids;
26use risingwave_hummock_sdk::filter_utils::{
27 parse_sstable_filter_layout, parse_sstable_filter_type,
28};
29use risingwave_hummock_sdk::version::{GroupDelta, HummockVersion};
30use risingwave_meta_model::compaction_config;
31use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::mutable_config::MutableConfig;
32use risingwave_pb::hummock::write_limits::WriteLimit;
33use risingwave_pb::hummock::{
34 CompactionConfig, CompactionGroupInfo, HummockVersionStats, PbGroupConstruct, PbGroupDestroy,
35 PbStateTableInfoDelta,
36};
37use sea_orm::EntityTrait;
38use tokio::sync::OnceCell;
39
40use super::CompactionGroupStatistic;
41use crate::hummock::compaction::compaction_config::{
42 CompactionConfigBuilder, validate_compaction_config,
43};
44use crate::hummock::error::{Error, Result};
45use crate::hummock::manager::transaction::HummockVersionTransaction;
46use crate::hummock::manager::versioning::Versioning;
47use crate::hummock::manager::{HummockManager, commit_multi_var};
48use crate::hummock::metrics_utils::remove_compaction_group_metrics;
49use crate::hummock::model::CompactionGroup;
50use crate::hummock::sequence::next_compaction_group_id;
51use crate::manager::MetaSrvEnv;
52use crate::model::{
53 BTreeMapTransaction, BTreeMapTransactionInner, DerefMutForward, MetadataModelError,
54};
55
56type CompactionGroupTransaction<'a> = BTreeMapTransaction<'a, CompactionGroupId, CompactionGroup>;
57
58impl CompactionGroupManager {
59 pub(crate) async fn new(env: &MetaSrvEnv) -> Result<CompactionGroupManager> {
60 let default_config = match env.opts.compaction_config.as_ref() {
61 None => CompactionConfigBuilder::new().build(),
62 Some(opt) => CompactionConfigBuilder::with_opt(opt).build(),
63 };
64 Self::new_with_config(env, default_config).await
65 }
66
67 pub(crate) async fn new_with_config(
68 env: &MetaSrvEnv,
69 default_config: CompactionConfig,
70 ) -> Result<CompactionGroupManager> {
71 let mut compaction_group_manager = CompactionGroupManager {
72 compaction_groups: BTreeMap::new(),
73 default_config: Arc::new(default_config),
74 write_limit: Default::default(),
75 };
76
77 let loaded_compaction_groups: BTreeMap<CompactionGroupId, CompactionGroup> =
78 compaction_config::Entity::find()
79 .all(&env.meta_store_ref().conn)
80 .await
81 .map_err(MetadataModelError::from)?
82 .into_iter()
83 .map(|m| (m.compaction_group_id as CompactionGroupId, m.into()))
84 .collect();
85
86 compaction_group_manager.init(loaded_compaction_groups);
87 Ok(compaction_group_manager)
88 }
89
90 fn init(&mut self, loaded_compaction_groups: BTreeMap<CompactionGroupId, CompactionGroup>) {
91 if !loaded_compaction_groups.is_empty() {
92 self.compaction_groups = loaded_compaction_groups;
93 }
94 }
95}
96
97impl HummockManager {
98 pub async fn compaction_group_ids(&self) -> Vec<CompactionGroupId> {
101 get_compaction_group_ids(
102 &self
103 .versioning
104 .read_with_process_name("compaction_group_ids")
105 .await
106 .current_version,
107 )
108 .collect_vec()
109 }
110
111 pub async fn get_compaction_group_map(&self) -> BTreeMap<CompactionGroupId, CompactionGroup> {
113 self.compaction_group_manager
114 .read_with_process_name("get_compaction_group_map")
115 .await
116 .compaction_groups
117 .clone()
118 }
119
120 #[cfg(test)]
121 pub async fn register_table_fragments(
123 &self,
124 mv_table: Option<TableId>,
125 mut internal_tables: Vec<TableId>,
126 ) -> Result<()> {
127 let mut pairs = vec![];
128 if let Some(mv_table) = mv_table {
129 if internal_tables.extract_if(.., |t| *t == mv_table).count() > 0 {
130 tracing::warn!("`mv_table` {} found in `internal_tables`", mv_table);
131 }
132 pairs.push((mv_table, StaticCompactionGroupId::MaterializedView));
134 }
135 for table_id in internal_tables {
137 pairs.push((table_id, StaticCompactionGroupId::StateDefault));
138 }
139 self.register_table_ids_for_test(&pairs).await?;
140 Ok(())
141 }
142
143 #[cfg(test)]
144 pub async fn unregister_table_fragments_vec(
146 &self,
147 table_fragments: &[crate::model::StreamJobFragments],
148 ) {
149 self.unregister_table_ids(table_fragments.iter().flat_map(|t| t.all_table_ids()))
150 .await
151 .unwrap();
152 }
153
154 pub async fn purge(&self, valid_ids: &HashSet<TableId>) -> Result<()> {
158 let to_unregister = self
159 .versioning
160 .read_with_process_name("purge")
161 .await
162 .current_version
163 .state_table_info
164 .info()
165 .keys()
166 .cloned()
167 .filter(|table_id| !valid_ids.contains(table_id))
168 .collect_vec();
169
170 self.unregister_table_ids(to_unregister).await?;
173 self.table_change_log_progress
174 .write()
175 .retain(|progress_table_id, _| valid_ids.contains(progress_table_id));
176 Ok(())
177 }
178
179 pub async fn register_table_ids_for_test(
184 &self,
185 pairs: &[(impl Into<TableId> + Copy, CompactionGroupId)],
186 ) -> Result<()> {
187 if pairs.is_empty() {
188 return Ok(());
189 }
190 let mut versioning_guard = self
191 .versioning
192 .write_with_process_name("register_table_ids_for_test")
193 .await;
194 let versioning = versioning_guard.deref_mut();
195 let mut compaction_group_manager = self
196 .compaction_group_manager
197 .write_with_process_name("register_table_ids_for_test")
198 .await;
199 let current_version = &versioning.current_version;
200 let default_config = compaction_group_manager.default_compaction_config();
201 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
202
203 for (table_id, _) in pairs {
204 let table_id = (*table_id).into();
205 if let Some(info) = current_version.state_table_info.info().get(&table_id) {
206 return Err(Error::CompactionGroup(format!(
207 "table {} already {:?}",
208 table_id, info
209 )));
210 }
211 }
212 let new_compaction_group_id: OnceCell<CompactionGroupId> = OnceCell::new();
214 let mut version = HummockVersionTransaction::new(
215 &mut versioning.current_version,
216 &mut versioning.hummock_version_deltas,
217 &mut versioning.table_change_log,
218 &self.gc_manager.change_log_retention,
219 self.env.notification_manager(),
220 None,
221 &self.metrics,
222 &self.env.opts,
223 &self.version_stat_tx,
224 );
225 let mut new_version_delta = version.new_delta();
226
227 let committed_epoch = new_version_delta
228 .latest_version()
229 .state_table_info
230 .info()
231 .values()
232 .map(|info| info.committed_epoch)
233 .max()
234 .unwrap_or(INVALID_EPOCH);
235
236 for (table_id, raw_group_id) in pairs {
237 let table_id = (*table_id).into();
238 let mut group_id = *raw_group_id;
239 if group_id == StaticCompactionGroupId::NewCompactionGroup {
240 let mut is_group_init = false;
241 group_id = *new_compaction_group_id
242 .get_or_try_init(|| async {
243 next_compaction_group_id(&self.env).await.inspect(|_| {
244 is_group_init = true;
245 })
246 })
247 .await?;
248 if is_group_init {
249 let group_deltas = &mut new_version_delta
250 .group_deltas
251 .entry(group_id)
252 .or_default()
253 .group_deltas;
254
255 let config =
256 match compaction_groups_txn.try_get_compaction_group_config(group_id) {
257 Some(config) => config.compaction_config.as_ref().clone(),
258 None => {
259 compaction_groups_txn
260 .create_compaction_groups(group_id, default_config.clone());
261 default_config.as_ref().clone()
262 }
263 };
264
265 let group_delta = GroupDelta::GroupConstruct(Box::new(PbGroupConstruct {
266 group_config: Some(config),
267 group_id,
268 ..Default::default()
269 }));
270
271 group_deltas.push(group_delta);
272 }
273 }
274 assert!(
275 new_version_delta
276 .state_table_info_delta
277 .insert(
278 table_id,
279 PbStateTableInfoDelta {
280 committed_epoch,
281 compaction_group_id: *raw_group_id,
282 }
283 )
284 .is_none()
285 );
286 }
287 new_version_delta.pre_apply();
288 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
289
290 Ok(())
291 }
292
293 pub async fn unregister_table_ids(
294 &self,
295 table_ids: impl IntoIterator<Item = TableId>,
296 ) -> Result<()> {
297 let table_ids = table_ids.into_iter().collect_vec();
298 if table_ids.is_empty() {
299 return Ok(());
300 }
301
302 let mut versioning_guard = self
303 .versioning
304 .write_with_process_name("unregister_table_ids")
305 .await;
306 let versioning = versioning_guard.deref_mut();
307 let mut version = HummockVersionTransaction::new(
308 &mut versioning.current_version,
309 &mut versioning.hummock_version_deltas,
310 &mut versioning.table_change_log,
311 &self.gc_manager.change_log_retention,
312 self.env.notification_manager(),
313 None,
314 &self.metrics,
315 &self.env.opts,
316 &self.version_stat_tx,
317 );
318 let mut new_version_delta = version.new_delta();
319 struct UnregisterGroupChange {
320 remaining_member_count: usize,
321 removed_table_ids: HashSet<TableId>,
322 }
323 let mut group_changes: HashMap<CompactionGroupId, UnregisterGroupChange> = HashMap::new();
324 for table_id in table_ids.iter().copied().unique() {
326 let version = new_version_delta.latest_version();
327 let Some(info) = version.state_table_info.info().get(&table_id) else {
328 continue;
329 };
330 let compaction_group_id = info.compaction_group_id;
331
332 let group_change =
333 group_changes
334 .entry(compaction_group_id)
335 .or_insert_with(|| UnregisterGroupChange {
336 remaining_member_count: version
337 .state_table_info
338 .compaction_group_member_tables()
339 .get(&compaction_group_id)
340 .expect("should exist")
341 .len(),
342 removed_table_ids: HashSet::new(),
343 });
344 group_change.remaining_member_count = group_change
345 .remaining_member_count
346 .checked_sub(1)
347 .expect("member table count should be positive");
348 assert!(group_change.removed_table_ids.insert(table_id));
349 new_version_delta.removed_table_ids.insert(table_id);
350 }
351
352 let mut removed_groups = vec![];
355 for (group_id, change) in group_changes {
356 if change.remaining_member_count == 0 && group_id > StaticCompactionGroupId::End {
357 let max_level = new_version_delta
358 .latest_version()
359 .get_compaction_group_levels(group_id)
360 .levels
361 .len();
362 new_version_delta
363 .group_deltas
364 .entry(group_id)
365 .or_default()
366 .group_deltas
367 .push(GroupDelta::GroupDestroy(PbGroupDestroy {}));
368 remove_compaction_group_metrics(&self.metrics, group_id, max_level);
369 removed_groups.push(group_id);
370 } else {
371 new_version_delta
372 .group_deltas
373 .entry(group_id)
374 .or_default()
375 .group_deltas
376 .push(GroupDelta::PruneTableIdsFromSsts(change.removed_table_ids));
377 }
378 }
379
380 new_version_delta.pre_apply();
381
382 let mut compaction_group_manager = self
385 .compaction_group_manager
386 .write_with_process_name("unregister_table_ids")
387 .await;
388 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
389
390 compaction_groups_txn.purge(HashSet::from_iter(get_compaction_group_ids(
391 version.latest_version(),
392 )));
393 commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
394
395 for group_id in removed_groups {
396 self.compaction_state.remove_compaction_group(group_id);
397 }
398
399 let mut stats = self.table_write_throughput_statistic_manager.write();
402 drop(compaction_group_manager);
403 drop(versioning_guard);
404 for table_id in table_ids {
405 stats.remove_table(table_id);
406 }
407
408 Ok(())
410 }
411
412 pub async fn update_compaction_config(
413 &self,
414 compaction_group_ids: &[CompactionGroupId],
415 config_to_update: &[MutableConfig],
416 ) -> Result<()> {
417 {
418 let mut compaction_group_manager = self
420 .compaction_group_manager
421 .write_with_process_name("update_compaction_config")
422 .await;
423 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
424 compaction_groups_txn
425 .update_compaction_config(compaction_group_ids, config_to_update)?;
426 commit_multi_var!(self.meta_store_ref(), compaction_groups_txn)?;
427 }
428
429 if config_to_update
430 .iter()
431 .any(|c| matches!(c, MutableConfig::Level0StopWriteThresholdSubLevelNumber(_)))
432 {
433 self.try_update_write_limits(compaction_group_ids).await;
435 }
436
437 Ok(())
438 }
439
440 pub async fn list_compaction_group(&self) -> Vec<CompactionGroupInfo> {
443 let mut versioning_guard = self
444 .versioning
445 .write_with_process_name("list_compaction_group")
446 .await;
447 let versioning = versioning_guard.deref_mut();
448 let current_version = &versioning.current_version;
449 let mut results = vec![];
450 let compaction_group_manager = self
451 .compaction_group_manager
452 .read_with_process_name("list_compaction_group")
453 .await;
454
455 for levels in current_version.levels.values() {
456 let compaction_config = compaction_group_manager
457 .try_get_compaction_group_config(levels.group_id)
458 .unwrap()
459 .compaction_config
460 .as_ref()
461 .clone();
462 let group = CompactionGroupInfo {
463 id: levels.group_id,
464 parent_id: levels.parent_group_id,
465 member_table_ids: current_version
466 .state_table_info
467 .compaction_group_member_table_ids(levels.group_id)
468 .iter()
469 .copied()
470 .collect_vec(),
471 compaction_config: Some(compaction_config),
472 };
473 results.push(group);
474 }
475 results
476 }
477
478 pub(crate) fn calculate_compaction_group_statistic_from_snapshot(
479 current_version: &HummockVersion,
480 version_stats: &HummockVersionStats,
481 id_to_config: &BTreeMap<CompactionGroupId, CompactionGroup>,
482 ) -> Vec<CompactionGroupStatistic> {
483 let mut infos = vec![];
484 for group_id in current_version.levels.keys() {
485 let compaction_group_config = id_to_config
486 .get(group_id)
487 .expect("compaction group config should exist for every group in current version")
488 .clone();
489 let mut group_info = CompactionGroupStatistic {
490 group_id: *group_id,
491 compaction_group_config,
492 ..Default::default()
493 };
494
495 for table_id in current_version
496 .state_table_info
497 .compaction_group_member_table_ids(*group_id)
498 {
499 let stats_size = version_stats
500 .table_stats
501 .get(table_id)
502 .map(|stats| stats.total_key_size + stats.total_value_size)
503 .unwrap_or(0);
504 let table_size = stats_size.max(0) as u64;
505 group_info.group_size += table_size;
506 group_info.table_statistic.insert(*table_id, table_size);
507 }
508
509 infos.push(group_info);
510 }
511 infos
512 }
513
514 pub async fn calculate_compaction_group_statistic(&self) -> Vec<CompactionGroupStatistic> {
515 let versioning_guard = self
516 .versioning
517 .read_with_process_name("calculate_compaction_group_statistic")
518 .await;
519 let manager = self
520 .compaction_group_manager
521 .read_with_process_name("calculate_compaction_group_statistic")
522 .await;
523 Self::calculate_compaction_group_statistic_from_snapshot(
524 &versioning_guard.current_version,
525 &versioning_guard.version_stats,
526 &manager.compaction_groups,
527 )
528 }
529
530 pub(crate) async fn calculate_compaction_group_statistic_for_tables(
531 &self,
532 table_ids: &[TableId],
533 ) -> Vec<CompactionGroupStatistic> {
534 let groups = {
535 let versioning = self
536 .versioning
537 .read_with_process_name("calculate_compaction_group_statistic_for_tables")
538 .await;
539 let manager = self
540 .compaction_group_manager
541 .read_with_process_name("calculate_compaction_group_statistic_for_tables")
542 .await;
543 let version = &versioning.current_version;
544 let group_ids: BTreeSet<_> = table_ids
545 .iter()
546 .filter_map(|table_id| {
547 version
548 .state_table_info
549 .info()
550 .get(table_id)
551 .map(|info| info.compaction_group_id)
552 })
553 .collect();
554 group_ids
555 .into_iter()
556 .map(|group_id| {
557 let config = manager
558 .try_get_compaction_group_config(group_id)
559 .expect("current group config should exist");
560 let tables = version
561 .state_table_info
562 .compaction_group_member_table_ids(group_id)
563 .iter()
564 .map(|table_id| {
565 let size = versioning
566 .version_stats
567 .table_stats
568 .get(table_id)
569 .map(|stats| stats.total_key_size + stats.total_value_size)
570 .unwrap_or(0)
571 .max(0) as u64;
572 (*table_id, size)
573 })
574 .collect_vec();
575 (group_id, config, tables)
576 })
577 .collect_vec()
578 };
579 groups
580 .into_iter()
581 .map(
582 |(group_id, compaction_group_config, tables)| CompactionGroupStatistic {
583 group_id,
584 group_size: tables.iter().map(|(_, size)| size).sum(),
585 table_statistic: tables.into_iter().collect(),
586 compaction_group_config,
587 },
588 )
589 .collect()
590 }
591
592 pub(crate) async fn initial_compaction_group_config_after_load(
593 &self,
594 versioning_guard: &Versioning,
595 compaction_group_manager: &mut CompactionGroupManager,
596 ) -> Result<()> {
597 let current_version = &versioning_guard.current_version;
599 let all_group_ids = get_compaction_group_ids(current_version).collect_vec();
600 let default_config = compaction_group_manager.default_compaction_config();
601 let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
602 compaction_groups_txn.try_create_compaction_groups(&all_group_ids, default_config);
603 commit_multi_var!(self.meta_store_ref(), compaction_groups_txn)?;
604
605 Ok(())
606 }
607}
608
609pub(crate) struct CompactionGroupManager {
617 compaction_groups: BTreeMap<CompactionGroupId, CompactionGroup>,
618 default_config: Arc<CompactionConfig>,
619 pub write_limit: HashMap<CompactionGroupId, WriteLimit>,
621}
622
623impl CompactionGroupManager {
624 pub fn start_compaction_groups_txn(&mut self) -> CompactionGroupTransaction<'_> {
626 CompactionGroupTransaction::new(&mut self.compaction_groups)
627 }
628
629 #[expect(clippy::type_complexity)]
630 pub fn start_owned_compaction_groups_txn<P: DerefMut<Target = Self>>(
631 inner: P,
632 ) -> BTreeMapTransactionInner<
633 CompactionGroupId,
634 CompactionGroup,
635 DerefMutForward<
636 Self,
637 BTreeMap<CompactionGroupId, CompactionGroup>,
638 P,
639 impl Fn(&Self) -> &BTreeMap<CompactionGroupId, CompactionGroup>,
640 impl Fn(&mut Self) -> &mut BTreeMap<CompactionGroupId, CompactionGroup>,
641 >,
642 > {
643 BTreeMapTransactionInner::new(DerefMutForward::new(
644 inner,
645 |mgr| &mgr.compaction_groups,
646 |mgr| &mut mgr.compaction_groups,
647 ))
648 }
649
650 pub(crate) fn try_get_compaction_group_config(
652 &self,
653 compaction_group_id: impl Into<CompactionGroupId>,
654 ) -> Option<CompactionGroup> {
655 self.compaction_groups
656 .get(&compaction_group_id.into())
657 .cloned()
658 }
659
660 pub(crate) fn default_compaction_config(&self) -> Arc<CompactionConfig> {
662 self.default_config.clone()
663 }
664}
665
666fn update_compaction_config(target: &mut CompactionConfig, items: &[MutableConfig]) -> Result<()> {
667 for item in items {
668 match item {
669 MutableConfig::MaxBytesForLevelBase(c) => {
670 target.max_bytes_for_level_base = *c;
671 }
672 MutableConfig::MaxBytesForLevelMultiplier(c) => {
673 target.max_bytes_for_level_multiplier = *c;
674 }
675 MutableConfig::MaxCompactionBytes(c) => {
676 target.max_compaction_bytes = *c;
677 }
678 MutableConfig::SubLevelMaxCompactionBytes(c) => {
679 target.sub_level_max_compaction_bytes = *c;
680 }
681 MutableConfig::Level0TierCompactFileNumber(c) => {
682 target.level0_tier_compact_file_number = *c;
683 }
684 MutableConfig::TargetFileSizeBase(c) => {
685 target.target_file_size_base = *c;
686 }
687 MutableConfig::CompactionFilterMask(c) => {
688 target.compaction_filter_mask = *c;
689 }
690 MutableConfig::MaxSubCompaction(c) => {
691 target.max_sub_compaction = *c;
692 }
693 MutableConfig::Level0StopWriteThresholdSubLevelNumber(c) => {
694 target.level0_stop_write_threshold_sub_level_number = *c;
695 }
696 MutableConfig::Level0SubLevelCompactLevelCount(c) => {
697 target.level0_sub_level_compact_level_count = *c;
698 }
699 MutableConfig::Level0OverlappingSubLevelCompactLevelCount(c) => {
700 target.level0_overlapping_sub_level_compact_level_count = *c;
701 }
702 MutableConfig::MaxSpaceReclaimBytes(c) => {
703 target.max_space_reclaim_bytes = *c;
704 }
705 MutableConfig::Level0MaxCompactFileNumber(c) => {
706 target.level0_max_compact_file_number = *c;
707 }
708 MutableConfig::EnableEmergencyPicker(c) => {
709 target.enable_emergency_picker = *c;
710 }
711 MutableConfig::TombstoneReclaimRatio(c) => {
712 target.tombstone_reclaim_ratio = *c;
713 }
714 MutableConfig::CompressionAlgorithm(c) => {
715 let level = c.get_level();
716 let max_level = try_u32_max_level(target.max_level)?;
717 if level > max_level {
718 return Err(Error::CompactionGroup(format!(
719 "invalid compression_algorithm level {}, max_level is {}",
720 level, target.max_level
721 )));
722 }
723
724 let Some(algorithm) = target.compression_algorithm.get_mut(level as usize) else {
725 return Err(Error::CompactionGroup(format!(
726 "invalid compression_algorithm level {}, compression_algorithm len is {}",
727 level,
728 target.compression_algorithm.len()
729 )));
730 };
731 algorithm.clone_from(&c.compression_algorithm);
732 }
733 MutableConfig::ResetCompressionAlgorithm(reset) => {
734 if *reset {
735 target.compression_algorithm =
736 default_compaction_config::compression_algorithm_vec(try_u32_max_level(
737 target.max_level,
738 )?);
739 }
740 }
741 MutableConfig::MaxL0CompactLevelCount(c) => {
742 target.max_l0_compact_level_count = Some(*c);
743 }
744 MutableConfig::SstAllowedTrivialMoveMinSize(c) => {
745 target.sst_allowed_trivial_move_min_size = Some(*c);
746 }
747 MutableConfig::SplitWeightByVnode(c) => {
748 target.split_weight_by_vnode = *c;
749 }
750 MutableConfig::DisableAutoGroupScheduling(c) => {
751 target.disable_auto_group_scheduling = Some(*c);
752 }
753 MutableConfig::MaxOverlappingLevelSize(c) => {
754 target.max_overlapping_level_size = Some(*c);
755 }
756 MutableConfig::SstAllowedTrivialMoveMaxCount(c) => {
757 target.sst_allowed_trivial_move_max_count = Some(*c);
758 }
759 MutableConfig::EmergencyLevel0SstFileCount(c) => {
760 target.emergency_level0_sst_file_count = Some(*c);
761 }
762 MutableConfig::EmergencyLevel0SubLevelPartition(c) => {
763 target.emergency_level0_sub_level_partition = Some(*c);
764 }
765 MutableConfig::Level0StopWriteThresholdMaxSstCount(c) => {
766 target.level0_stop_write_threshold_max_sst_count = Some(*c);
767 }
768 MutableConfig::Level0StopWriteThresholdMaxSize(c) => {
769 target.level0_stop_write_threshold_max_size = Some(*c);
770 }
771 MutableConfig::EnableOptimizeL0IntervalSelection(c) => {
772 target.enable_optimize_l0_interval_selection = Some(*c);
773 }
774 #[expect(deprecated)]
775 MutableConfig::VnodeAlignedLevelSizeThreshold(_) => {
776 }
778 MutableConfig::MaxKvCountForXor16(c) => {
779 target.max_kv_count_for_xor16 = optional_non_sentinel_u64_config(*c);
780 }
781 MutableConfig::MaxVnodeKeyRangeBytes(c) => {
782 target.max_vnode_key_range_bytes = optional_positive_u64_config(*c);
783 }
784 MutableConfig::SstableFilterType(c) => {
785 parse_sstable_filter_type(&c.filter_type).map_err(Error::CompactionGroup)?;
786 if target.sstable_filter_type.is_empty() {
787 target.sstable_filter_type = default_compaction_config::sstable_filter_type();
788 target
789 .sstable_filter_type
790 .resize(target.max_level as usize + 1, "xor16".to_owned());
791 }
792 let idx = c.get_level() as usize;
793 let level_entry = target.sstable_filter_type.get_mut(idx).ok_or_else(|| {
794 Error::CompactionGroup(format!(
795 "sstable_filter_type level {} is out of range",
796 idx
797 ))
798 })?;
799 level_entry.clone_from(&c.filter_type);
800 }
801 MutableConfig::SstableFilterLayout(c) => {
802 parse_sstable_filter_layout(&c.layout).map_err(Error::CompactionGroup)?;
803 if target.sstable_filter_layout.is_empty() {
804 target.sstable_filter_layout =
805 default_compaction_config::sstable_filter_layout();
806 target
807 .sstable_filter_layout
808 .resize(target.max_level as usize + 1, "blocked".to_owned());
809 }
810 let idx = c.get_level() as usize;
811 let level_entry = target.sstable_filter_layout.get_mut(idx).ok_or_else(|| {
812 Error::CompactionGroup(format!(
813 "sstable_filter_layout level {} is out of range",
814 idx
815 ))
816 })?;
817 level_entry.clone_from(&c.layout);
818 }
819 }
820 }
821 Ok(())
822}
823
824fn optional_u64_config(value: u64) -> Option<u64> {
825 (value != u64::MIN && value != u64::MAX).then_some(value)
826}
827
828fn optional_non_sentinel_u64_config(value: u64) -> Option<u64> {
829 (value != u64::MAX).then_some(value)
830}
831
832fn optional_positive_u64_config(value: u64) -> Option<u64> {
833 optional_u64_config(value).filter(|value| *value > 0)
834}
835
836fn try_u32_max_level(max_level: u64) -> Result<u32> {
837 u32::try_from(max_level).map_err(|_| {
838 Error::CompactionGroup(format!(
839 "invalid max_level {}, expect <= {}",
840 max_level,
841 u32::MAX
842 ))
843 })
844}
845
846impl CompactionGroupTransaction<'_> {
847 pub fn try_create_compaction_groups(
849 &mut self,
850 compaction_group_ids: &[CompactionGroupId],
851 config: Arc<CompactionConfig>,
852 ) -> bool {
853 let mut trivial = true;
854 for id in compaction_group_ids {
855 if self.contains_key(id) {
856 continue;
857 }
858 let new_entry = CompactionGroup::new(*id, config.as_ref().clone());
859 self.insert(*id, new_entry);
860
861 trivial = false;
862 }
863
864 !trivial
865 }
866
867 pub fn create_compaction_groups(
868 &mut self,
869 compaction_group_id: CompactionGroupId,
870 config: Arc<CompactionConfig>,
871 ) {
872 self.try_create_compaction_groups(&[compaction_group_id], config);
873 }
874
875 pub(crate) fn try_get_compaction_group_config(
877 &self,
878 compaction_group_id: CompactionGroupId,
879 ) -> Option<&CompactionGroup> {
880 self.get(&compaction_group_id)
881 }
882
883 pub fn purge(&mut self, existing_groups: HashSet<CompactionGroupId>) {
885 let stale_group = self
886 .tree_ref()
887 .keys()
888 .cloned()
889 .filter(|k| !existing_groups.contains(k))
890 .collect_vec();
891 if stale_group.is_empty() {
892 return;
893 }
894 for group in stale_group {
895 self.remove(group);
896 }
897 }
898
899 pub(crate) fn update_compaction_config(
900 &mut self,
901 compaction_group_ids: &[CompactionGroupId],
902 config_to_update: &[MutableConfig],
903 ) -> Result<HashMap<CompactionGroupId, CompactionGroup>> {
904 let mut results = HashMap::default();
905 for compaction_group_id in compaction_group_ids.iter().unique() {
906 let group = self.get(compaction_group_id).ok_or_else(|| {
907 Error::CompactionGroup(format!("invalid group {}", *compaction_group_id))
908 })?;
909 let mut config = group.compaction_config.as_ref().clone();
910 update_compaction_config(&mut config, config_to_update)?;
911 if let Err(reason) = validate_compaction_config(&config) {
912 return Err(Error::CompactionGroup(reason));
913 }
914 let mut new_group = group.clone();
915 new_group.compaction_config = Arc::new(config);
916 self.insert(*compaction_group_id, new_group.clone());
917 results.insert(new_group.group_id(), new_group);
918 }
919
920 Ok(results)
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use std::collections::{BTreeMap, HashSet};
927 use std::sync::Arc;
928
929 use itertools::Itertools;
930 use risingwave_common::id::JobId;
931 use risingwave_hummock_sdk::CompactionGroupId;
932 use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::mutable_config::MutableConfig;
933 use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::{
934 CompressionAlgorithm, SstableFilterLayout, SstableFilterType,
935 };
936
937 use crate::controller::SqlMetaStore;
938 use crate::hummock::commit_multi_var;
939 use crate::hummock::compaction::compaction_config::CompactionConfigBuilder;
940 use crate::hummock::error::Result;
941 use crate::hummock::manager::compaction_group_manager::CompactionGroupManager;
942 use crate::hummock::test_utils::setup_compute_env;
943 use crate::model::{Fragment, StreamJobFragments};
944
945 #[test]
946 fn test_update_compaction_config_filter_type_layout_backward_compat() {
947 let mut config = CompactionConfigBuilder::new().build();
948 config.sstable_filter_type.clear();
949 config.sstable_filter_layout.clear();
950
951 super::update_compaction_config(
952 &mut config,
953 &[MutableConfig::SstableFilterType(SstableFilterType {
954 level: 0,
955 filter_type: "xor8".to_owned(),
956 })],
957 )
958 .unwrap();
959 assert_eq!(
960 config.sstable_filter_type.len(),
961 config.max_level as usize + 1
962 );
963 assert_eq!(config.sstable_filter_type[0], "xor8");
964 assert_eq!(config.sstable_filter_type[5], "xor8");
965
966 super::update_compaction_config(
967 &mut config,
968 &[MutableConfig::SstableFilterType(SstableFilterType {
969 level: 1,
970 filter_type: "none".to_owned(),
971 })],
972 )
973 .unwrap();
974 assert_eq!(config.sstable_filter_type[1], "none");
975
976 super::update_compaction_config(
977 &mut config,
978 &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
979 level: 1,
980 layout: "plain".to_owned(),
981 })],
982 )
983 .unwrap();
984 assert_eq!(
985 config.sstable_filter_layout.len(),
986 config.max_level as usize + 1
987 );
988 assert_eq!(config.sstable_filter_layout[1], "plain");
989 assert_eq!(config.sstable_filter_layout[2], "blocked");
990 }
991
992 #[test]
993 fn test_update_compaction_config_rejects_out_of_range_level() {
994 let mut config = CompactionConfigBuilder::new().build();
995 let oob = config.max_level as u32 + 1;
996
997 assert!(
998 super::update_compaction_config(
999 &mut config,
1000 &[MutableConfig::SstableFilterType(SstableFilterType {
1001 level: oob,
1002 filter_type: "xor8".to_owned(),
1003 })],
1004 )
1005 .is_err()
1006 );
1007
1008 assert!(
1009 super::update_compaction_config(
1010 &mut config,
1011 &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
1012 level: oob,
1013 layout: "plain".to_owned(),
1014 })],
1015 )
1016 .is_err()
1017 );
1018
1019 assert!(
1020 super::update_compaction_config(
1021 &mut config,
1022 &[MutableConfig::CompressionAlgorithm(CompressionAlgorithm {
1023 level: oob,
1024 compression_algorithm: "Zstd".to_owned(),
1025 })],
1026 )
1027 .is_err()
1028 );
1029 }
1030
1031 #[test]
1032 fn test_update_compaction_config_rejects_invalid_filter_metadata() {
1033 let mut config = CompactionConfigBuilder::new().build();
1034
1035 assert!(
1036 super::update_compaction_config(
1037 &mut config,
1038 &[MutableConfig::SstableFilterType(SstableFilterType {
1039 level: 0,
1040 filter_type: "unknown".to_owned(),
1041 })],
1042 )
1043 .is_err()
1044 );
1045
1046 let mut config = CompactionConfigBuilder::new().build();
1047
1048 assert!(
1049 super::update_compaction_config(
1050 &mut config,
1051 &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
1052 level: 0,
1053 layout: "unknown".to_owned(),
1054 })],
1055 )
1056 .is_err()
1057 );
1058 }
1059
1060 #[test]
1061 fn test_reset_compression_algorithm_false_is_noop() {
1062 let mut config = CompactionConfigBuilder::new().build();
1063 config.compression_algorithm[3] = "Zstd".to_owned();
1064
1065 super::update_compaction_config(
1066 &mut config,
1067 &[MutableConfig::ResetCompressionAlgorithm(false)],
1068 )
1069 .unwrap();
1070
1071 assert_eq!(config.compression_algorithm[3], "Zstd");
1072 }
1073
1074 #[tokio::test]
1075 async fn test_inner() {
1076 let (env, ..) = setup_compute_env(8080).await;
1077 let mut inner = CompactionGroupManager::new(&env).await.unwrap();
1078 assert_eq!(inner.compaction_groups.len(), 2);
1079
1080 async fn update_compaction_config(
1081 meta: &SqlMetaStore,
1082 inner: &mut CompactionGroupManager,
1083 cg_ids: &[impl Into<CompactionGroupId> + Copy],
1084 config_to_update: &[MutableConfig],
1085 ) -> Result<()> {
1086 let cg_ids = cg_ids.iter().copied().map_into().collect_vec();
1087 let mut compaction_groups_txn = inner.start_compaction_groups_txn();
1088 compaction_groups_txn.update_compaction_config(&cg_ids, config_to_update)?;
1089 commit_multi_var!(meta, compaction_groups_txn)
1090 }
1091
1092 async fn insert_compaction_group_configs(
1093 meta: &SqlMetaStore,
1094 inner: &mut CompactionGroupManager,
1095 cg_ids: &[u64],
1096 ) {
1097 let default_config = inner.default_compaction_config();
1098 let mut compaction_groups_txn = inner.start_compaction_groups_txn();
1099 if compaction_groups_txn.try_create_compaction_groups(
1100 &cg_ids.iter().copied().map_into().collect_vec(),
1101 default_config,
1102 ) {
1103 commit_multi_var!(meta, compaction_groups_txn).unwrap();
1104 }
1105 }
1106
1107 async fn insert_compaction_group_config_with_max_level(
1108 meta: &SqlMetaStore,
1109 inner: &mut CompactionGroupManager,
1110 cg_id: u64,
1111 max_level: u64,
1112 ) {
1113 let mut config = inner.default_compaction_config().as_ref().clone();
1114 config.max_level = max_level;
1115 config.compression_algorithm =
1116 super::default_compaction_config::compression_algorithm_vec(
1117 super::try_u32_max_level(max_level).expect("max_level should fit u32 in test"),
1118 );
1119 let mut compaction_groups_txn = inner.start_compaction_groups_txn();
1120 compaction_groups_txn.create_compaction_groups(cg_id.into(), Arc::new(config));
1121 commit_multi_var!(meta, compaction_groups_txn).unwrap();
1122 }
1123
1124 update_compaction_config(env.meta_store_ref(), &mut inner, &[100, 200], &[])
1125 .await
1126 .unwrap_err();
1127 insert_compaction_group_configs(env.meta_store_ref(), &mut inner, &[100, 200]).await;
1128 assert_eq!(inner.compaction_groups.len(), 4);
1129 let mut inner = CompactionGroupManager::new(&env).await.unwrap();
1130 assert_eq!(inner.compaction_groups.len(), 4);
1131
1132 update_compaction_config(
1133 env.meta_store_ref(),
1134 &mut inner,
1135 &[100, 200],
1136 &[MutableConfig::MaxSubCompaction(123)],
1137 )
1138 .await
1139 .unwrap();
1140 assert_eq!(inner.compaction_groups.len(), 4);
1141 assert_eq!(
1142 inner
1143 .try_get_compaction_group_config(100)
1144 .unwrap()
1145 .compaction_config
1146 .max_sub_compaction,
1147 123
1148 );
1149 assert_eq!(
1150 inner
1151 .try_get_compaction_group_config(200)
1152 .unwrap()
1153 .compaction_config
1154 .max_sub_compaction,
1155 123
1156 );
1157
1158 insert_compaction_group_config_with_max_level(env.meta_store_ref(), &mut inner, 300, 4)
1159 .await;
1160 update_compaction_config(
1161 env.meta_store_ref(),
1162 &mut inner,
1163 &[300],
1164 &[MutableConfig::ResetCompressionAlgorithm(true)],
1165 )
1166 .await
1167 .unwrap();
1168 assert_eq!(
1169 inner
1170 .try_get_compaction_group_config(300)
1171 .unwrap()
1172 .compaction_config
1173 .compression_algorithm,
1174 super::default_compaction_config::compression_algorithm_vec(4)
1175 );
1176 let err = update_compaction_config(
1177 env.meta_store_ref(),
1178 &mut inner,
1179 &[300],
1180 &[MutableConfig::CompressionAlgorithm(CompressionAlgorithm {
1181 level: 6,
1182 compression_algorithm: "Zstd".to_owned(),
1183 })],
1184 )
1185 .await
1186 .unwrap_err();
1187 assert!(
1188 err.to_string()
1189 .contains("invalid compression_algorithm level 6")
1190 );
1191
1192 update_compaction_config(
1193 env.meta_store_ref(),
1194 &mut inner,
1195 &[100],
1196 &[MutableConfig::MaxKvCountForXor16(0)],
1197 )
1198 .await
1199 .unwrap();
1200 assert_eq!(
1201 inner
1202 .try_get_compaction_group_config(100)
1203 .unwrap()
1204 .compaction_config
1205 .max_kv_count_for_xor16,
1206 Some(0)
1207 );
1208 update_compaction_config(
1209 env.meta_store_ref(),
1210 &mut inner,
1211 &[100],
1212 &[MutableConfig::MaxKvCountForXor16(1024)],
1213 )
1214 .await
1215 .unwrap();
1216 assert_eq!(
1217 inner
1218 .try_get_compaction_group_config(100)
1219 .unwrap()
1220 .compaction_config
1221 .max_kv_count_for_xor16,
1222 Some(1024)
1223 );
1224 update_compaction_config(
1225 env.meta_store_ref(),
1226 &mut inner,
1227 &[100],
1228 &[MutableConfig::MaxKvCountForXor16(u64::MAX)],
1229 )
1230 .await
1231 .unwrap();
1232 assert_eq!(
1233 inner
1234 .try_get_compaction_group_config(100)
1235 .unwrap()
1236 .compaction_config
1237 .max_kv_count_for_xor16,
1238 None
1239 );
1240
1241 update_compaction_config(
1242 env.meta_store_ref(),
1243 &mut inner,
1244 &[100],
1245 &[MutableConfig::MaxVnodeKeyRangeBytes(0)],
1246 )
1247 .await
1248 .unwrap();
1249 assert_eq!(
1250 inner
1251 .try_get_compaction_group_config(100)
1252 .unwrap()
1253 .compaction_config
1254 .max_vnode_key_range_bytes,
1255 None
1256 );
1257 update_compaction_config(
1258 env.meta_store_ref(),
1259 &mut inner,
1260 &[100],
1261 &[MutableConfig::MaxVnodeKeyRangeBytes(1024)],
1262 )
1263 .await
1264 .unwrap();
1265 assert_eq!(
1266 inner
1267 .try_get_compaction_group_config(100)
1268 .unwrap()
1269 .compaction_config
1270 .max_vnode_key_range_bytes,
1271 Some(1024)
1272 );
1273 }
1274
1275 #[tokio::test]
1276 async fn test_manager() {
1277 let (_, compaction_group_manager, ..) = setup_compute_env(8080).await;
1278 let table_fragment_1 = StreamJobFragments::for_test(
1279 JobId::new(10),
1280 BTreeMap::from([(
1281 1.into(),
1282 Fragment {
1283 fragment_id: 1.into(),
1284 state_table_ids: vec![10.into(), 11.into(), 12.into(), 13.into()],
1285 ..Default::default()
1286 },
1287 )]),
1288 );
1289 let table_fragment_2 = StreamJobFragments::for_test(
1290 JobId::new(20),
1291 BTreeMap::from([(
1292 2.into(),
1293 Fragment {
1294 fragment_id: 2.into(),
1295 state_table_ids: vec![20.into(), 21.into(), 22.into(), 23.into()],
1296 ..Default::default()
1297 },
1298 )]),
1299 );
1300
1301 let registered_number = || async {
1303 compaction_group_manager
1304 .list_compaction_group()
1305 .await
1306 .iter()
1307 .map(|cg| cg.member_table_ids.len())
1308 .sum::<usize>()
1309 };
1310 let group_number =
1311 || async { compaction_group_manager.list_compaction_group().await.len() };
1312 assert_eq!(registered_number().await, 0);
1313
1314 compaction_group_manager
1315 .register_table_fragments(
1316 Some(table_fragment_1.stream_job_id().as_mv_table_id()),
1317 table_fragment_1
1318 .internal_table_ids()
1319 .into_iter()
1320 .map_into()
1321 .collect(),
1322 )
1323 .await
1324 .unwrap();
1325 assert_eq!(registered_number().await, 4);
1326 compaction_group_manager
1327 .register_table_fragments(
1328 Some(table_fragment_2.stream_job_id().as_mv_table_id()),
1329 table_fragment_2
1330 .internal_table_ids()
1331 .into_iter()
1332 .map_into()
1333 .collect(),
1334 )
1335 .await
1336 .unwrap();
1337 assert_eq!(registered_number().await, 8);
1338
1339 compaction_group_manager
1341 .unregister_table_fragments_vec(std::slice::from_ref(&table_fragment_1))
1342 .await;
1343 assert_eq!(registered_number().await, 4);
1344
1345 compaction_group_manager
1347 .purge(&table_fragment_2.all_table_ids().collect())
1348 .await
1349 .unwrap();
1350 assert_eq!(registered_number().await, 4);
1351 compaction_group_manager
1352 .purge(&HashSet::new())
1353 .await
1354 .unwrap();
1355 assert_eq!(registered_number().await, 0);
1356
1357 assert_eq!(group_number().await, 2);
1358
1359 compaction_group_manager
1360 .register_table_fragments(
1361 Some(table_fragment_1.stream_job_id().as_mv_table_id()),
1362 table_fragment_1
1363 .internal_table_ids()
1364 .into_iter()
1365 .map_into()
1366 .collect(),
1367 )
1368 .await
1369 .unwrap();
1370 assert_eq!(registered_number().await, 4);
1371 assert_eq!(group_number().await, 2);
1372
1373 compaction_group_manager
1374 .unregister_table_fragments_vec(&[table_fragment_1])
1375 .await;
1376 assert_eq!(registered_number().await, 0);
1377 assert_eq!(group_number().await, 2);
1378 }
1379}