Skip to main content

risingwave_meta/hummock/manager/compaction/
compaction_group_manager.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, 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    /// Should not be called inside [`HummockManager`], because it requests locks internally.
99    /// The implementation acquires `versioning` lock.
100    pub async fn compaction_group_ids(&self) -> Vec<CompactionGroupId> {
101        get_compaction_group_ids(&self.versioning.read().await.current_version).collect_vec()
102    }
103
104    /// The implementation acquires `compaction_group_manager` lock.
105    pub async fn get_compaction_group_map(&self) -> BTreeMap<CompactionGroupId, CompactionGroup> {
106        self.compaction_group_manager
107            .read()
108            .await
109            .compaction_groups
110            .clone()
111    }
112
113    #[cfg(test)]
114    /// Registers `table_fragments` to compaction groups.
115    pub async fn register_table_fragments(
116        &self,
117        mv_table: Option<TableId>,
118        mut internal_tables: Vec<TableId>,
119    ) -> Result<()> {
120        let mut pairs = vec![];
121        if let Some(mv_table) = mv_table {
122            if internal_tables.extract_if(.., |t| *t == mv_table).count() > 0 {
123                tracing::warn!("`mv_table` {} found in `internal_tables`", mv_table);
124            }
125            // materialized_view
126            pairs.push((mv_table, StaticCompactionGroupId::MaterializedView));
127        }
128        // internal states
129        for table_id in internal_tables {
130            pairs.push((table_id, StaticCompactionGroupId::StateDefault));
131        }
132        self.register_table_ids_for_test(&pairs).await?;
133        Ok(())
134    }
135
136    #[cfg(test)]
137    /// Unregisters `table_fragments` from compaction groups
138    pub async fn unregister_table_fragments_vec(
139        &self,
140        table_fragments: &[crate::model::StreamJobFragments],
141    ) {
142        self.unregister_table_ids(table_fragments.iter().flat_map(|t| t.all_table_ids()))
143            .await
144            .unwrap();
145    }
146
147    /// Unregisters stale members and groups
148    /// The caller should ensure `table_fragments_list` remain unchanged during `purge`.
149    /// Currently `purge` is only called during meta service start ups.
150    pub async fn purge(&self, valid_ids: &HashSet<TableId>) -> Result<()> {
151        let to_unregister = self
152            .versioning
153            .read()
154            .await
155            .current_version
156            .state_table_info
157            .info()
158            .keys()
159            .cloned()
160            .filter(|table_id| !valid_ids.contains(table_id))
161            .collect_vec();
162
163        // As we have released versioning lock, the version that `to_unregister` is calculated from
164        // may not be the same as the one used in unregister_table_ids. It is OK.
165        self.unregister_table_ids(to_unregister).await
166    }
167
168    /// The implementation acquires `versioning` lock.
169    ///
170    /// The method name is temporarily added with a `_for_test` prefix to mark
171    /// that it's currently only used in test.
172    pub async fn register_table_ids_for_test(
173        &self,
174        pairs: &[(impl Into<TableId> + Copy, CompactionGroupId)],
175    ) -> Result<()> {
176        if pairs.is_empty() {
177            return Ok(());
178        }
179        let mut versioning_guard = self.versioning.write().await;
180        let versioning = versioning_guard.deref_mut();
181        let mut compaction_group_manager = self.compaction_group_manager.write().await;
182        let current_version = &versioning.current_version;
183        let default_config = compaction_group_manager.default_compaction_config();
184        let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
185
186        for (table_id, _) in pairs {
187            let table_id = (*table_id).into();
188            if let Some(info) = current_version.state_table_info.info().get(&table_id) {
189                return Err(Error::CompactionGroup(format!(
190                    "table {} already {:?}",
191                    table_id, info
192                )));
193            }
194        }
195        // All NewCompactionGroup pairs are mapped to one new compaction group.
196        let new_compaction_group_id: OnceCell<CompactionGroupId> = OnceCell::new();
197        let mut version = HummockVersionTransaction::new(
198            &mut versioning.current_version,
199            &mut versioning.hummock_version_deltas,
200            &mut versioning.table_change_log,
201            self.env.notification_manager(),
202            None,
203            &self.metrics,
204            &self.env.opts,
205            &self.version_stat_tx,
206        );
207        let mut new_version_delta = version.new_delta();
208
209        let committed_epoch = new_version_delta
210            .latest_version()
211            .state_table_info
212            .info()
213            .values()
214            .map(|info| info.committed_epoch)
215            .max()
216            .unwrap_or(INVALID_EPOCH);
217
218        for (table_id, raw_group_id) in pairs {
219            let table_id = (*table_id).into();
220            let mut group_id = *raw_group_id;
221            if group_id == StaticCompactionGroupId::NewCompactionGroup {
222                let mut is_group_init = false;
223                group_id = *new_compaction_group_id
224                    .get_or_try_init(|| async {
225                        next_compaction_group_id(&self.env).await.inspect(|_| {
226                            is_group_init = true;
227                        })
228                    })
229                    .await?;
230                if is_group_init {
231                    let group_deltas = &mut new_version_delta
232                        .group_deltas
233                        .entry(group_id)
234                        .or_default()
235                        .group_deltas;
236
237                    let config =
238                        match compaction_groups_txn.try_get_compaction_group_config(group_id) {
239                            Some(config) => config.compaction_config.as_ref().clone(),
240                            None => {
241                                compaction_groups_txn
242                                    .create_compaction_groups(group_id, default_config.clone());
243                                default_config.as_ref().clone()
244                            }
245                        };
246
247                    let group_delta = GroupDelta::GroupConstruct(Box::new(PbGroupConstruct {
248                        group_config: Some(config),
249                        group_id,
250                        ..Default::default()
251                    }));
252
253                    group_deltas.push(group_delta);
254                }
255            }
256            assert!(
257                new_version_delta
258                    .state_table_info_delta
259                    .insert(
260                        table_id,
261                        PbStateTableInfoDelta {
262                            committed_epoch,
263                            compaction_group_id: *raw_group_id,
264                        }
265                    )
266                    .is_none()
267            );
268        }
269        new_version_delta.pre_apply();
270        commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
271
272        Ok(())
273    }
274
275    pub async fn unregister_table_ids(
276        &self,
277        table_ids: impl IntoIterator<Item = TableId>,
278    ) -> Result<()> {
279        let table_ids = table_ids.into_iter().collect_vec();
280        if table_ids.is_empty() {
281            return Ok(());
282        }
283
284        {
285            // Remove table write throughput statistics
286            // The Caller acquires `Send`, so we should safely use `write` lock before the await point.
287            // The table write throughput statistic accepts data inconsistencies (unregister table ids fail), so we can clean it up in advance.
288            let mut table_write_throughput_statistic_manager =
289                self.table_write_throughput_statistic_manager.write();
290            for &table_id in table_ids.iter().unique() {
291                table_write_throughput_statistic_manager.remove_table(table_id);
292            }
293        }
294
295        let mut versioning_guard = self.versioning.write().await;
296        let versioning = versioning_guard.deref_mut();
297        let mut version = HummockVersionTransaction::new(
298            &mut versioning.current_version,
299            &mut versioning.hummock_version_deltas,
300            &mut versioning.table_change_log,
301            self.env.notification_manager(),
302            None,
303            &self.metrics,
304            &self.env.opts,
305            &self.version_stat_tx,
306        );
307        let mut new_version_delta = version.new_delta();
308        struct UnregisterGroupChange {
309            remaining_member_count: usize,
310            removed_table_ids: HashSet<TableId>,
311        }
312        let mut group_changes: HashMap<CompactionGroupId, UnregisterGroupChange> = HashMap::new();
313        // Remove member tables
314        for table_id in table_ids.into_iter().unique() {
315            let version = new_version_delta.latest_version();
316            let Some(info) = version.state_table_info.info().get(&table_id) else {
317                continue;
318            };
319            let compaction_group_id = info.compaction_group_id;
320
321            let group_change =
322                group_changes
323                    .entry(compaction_group_id)
324                    .or_insert_with(|| UnregisterGroupChange {
325                        remaining_member_count: version
326                            .state_table_info
327                            .compaction_group_member_tables()
328                            .get(&compaction_group_id)
329                            .expect("should exist")
330                            .len(),
331                        removed_table_ids: HashSet::new(),
332                    });
333            group_change.remaining_member_count = group_change
334                .remaining_member_count
335                .checked_sub(1)
336                .expect("member table count should be positive");
337            assert!(group_change.removed_table_ids.insert(table_id));
338            new_version_delta.removed_table_ids.insert(table_id);
339        }
340
341        for (group_id, change) in group_changes {
342            if change.remaining_member_count == 0 && group_id > StaticCompactionGroupId::End {
343                let max_level = new_version_delta
344                    .latest_version()
345                    .get_compaction_group_levels(group_id)
346                    .levels
347                    .len();
348                new_version_delta
349                    .group_deltas
350                    .entry(group_id)
351                    .or_default()
352                    .group_deltas
353                    .push(GroupDelta::GroupDestroy(PbGroupDestroy {}));
354                remove_compaction_group_metrics(&self.metrics, group_id, max_level);
355                // clean up compaction schedule state for the removed group
356                self.compaction_state.remove_compaction_group(group_id);
357            } else {
358                new_version_delta
359                    .group_deltas
360                    .entry(group_id)
361                    .or_default()
362                    .group_deltas
363                    .push(GroupDelta::PruneTableIdsFromSsts(change.removed_table_ids));
364            }
365        }
366
367        new_version_delta.pre_apply();
368
369        // Purge may cause write to meta store. If it hurts performance while holding versioning
370        // lock, consider to make it in batch.
371        let mut compaction_group_manager = self.compaction_group_manager.write().await;
372        let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
373
374        compaction_groups_txn.purge(HashSet::from_iter(get_compaction_group_ids(
375            version.latest_version(),
376        )));
377        commit_multi_var!(self.meta_store_ref(), version, compaction_groups_txn)?;
378
379        // No need to handle DeltaType::GroupDestroy during time travel.
380        Ok(())
381    }
382
383    pub async fn update_compaction_config(
384        &self,
385        compaction_group_ids: &[CompactionGroupId],
386        config_to_update: &[MutableConfig],
387    ) -> Result<()> {
388        {
389            // Avoid lock conflicts with `try_update_write_limits``
390            let mut compaction_group_manager = self.compaction_group_manager.write().await;
391            let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
392            compaction_groups_txn
393                .update_compaction_config(compaction_group_ids, config_to_update)?;
394            commit_multi_var!(self.meta_store_ref(), compaction_groups_txn)?;
395        }
396
397        if config_to_update
398            .iter()
399            .any(|c| matches!(c, MutableConfig::Level0StopWriteThresholdSubLevelNumber(_)))
400        {
401            // Update write limits with lock
402            self.try_update_write_limits(compaction_group_ids).await;
403        }
404
405        Ok(())
406    }
407
408    /// Gets complete compaction group info.
409    /// It is the aggregate of `HummockVersion` and `CompactionGroupConfig`
410    pub async fn list_compaction_group(&self) -> Vec<CompactionGroupInfo> {
411        let mut versioning_guard = self.versioning.write().await;
412        let versioning = versioning_guard.deref_mut();
413        let current_version = &versioning.current_version;
414        let mut results = vec![];
415        let compaction_group_manager = self.compaction_group_manager.read().await;
416
417        for levels in current_version.levels.values() {
418            let compaction_config = compaction_group_manager
419                .try_get_compaction_group_config(levels.group_id)
420                .unwrap()
421                .compaction_config
422                .as_ref()
423                .clone();
424            let group = CompactionGroupInfo {
425                id: levels.group_id,
426                parent_id: levels.parent_group_id,
427                member_table_ids: current_version
428                    .state_table_info
429                    .compaction_group_member_table_ids(levels.group_id)
430                    .iter()
431                    .copied()
432                    .collect_vec(),
433                compaction_config: Some(compaction_config),
434            };
435            results.push(group);
436        }
437        results
438    }
439
440    pub(crate) fn calculate_compaction_group_statistic_from_snapshot(
441        current_version: &HummockVersion,
442        version_stats: &HummockVersionStats,
443        id_to_config: &BTreeMap<CompactionGroupId, CompactionGroup>,
444    ) -> Vec<CompactionGroupStatistic> {
445        let mut infos = vec![];
446        for group_id in current_version.levels.keys() {
447            let compaction_group_config = id_to_config
448                .get(group_id)
449                .expect("compaction group config should exist for every group in current version")
450                .clone();
451            let mut group_info = CompactionGroupStatistic {
452                group_id: *group_id,
453                compaction_group_config,
454                ..Default::default()
455            };
456
457            for table_id in current_version
458                .state_table_info
459                .compaction_group_member_table_ids(*group_id)
460            {
461                let stats_size = version_stats
462                    .table_stats
463                    .get(table_id)
464                    .map(|stats| stats.total_key_size + stats.total_value_size)
465                    .unwrap_or(0);
466                let table_size = stats_size.max(0) as u64;
467                group_info.group_size += table_size;
468                group_info.table_statistic.insert(*table_id, table_size);
469            }
470
471            infos.push(group_info);
472        }
473        infos
474    }
475
476    pub async fn calculate_compaction_group_statistic(&self) -> Vec<CompactionGroupStatistic> {
477        let versioning_guard = self.versioning.read().await;
478        let manager = self.compaction_group_manager.read().await;
479        Self::calculate_compaction_group_statistic_from_snapshot(
480            &versioning_guard.current_version,
481            &versioning_guard.version_stats,
482            &manager.compaction_groups,
483        )
484    }
485
486    pub(crate) async fn initial_compaction_group_config_after_load(
487        &self,
488        versioning_guard: &Versioning,
489        compaction_group_manager: &mut CompactionGroupManager,
490    ) -> Result<()> {
491        // 1. Due to version compatibility, we fix some of the configuration of older versions after hummock starts.
492        let current_version = &versioning_guard.current_version;
493        let all_group_ids = get_compaction_group_ids(current_version).collect_vec();
494        let default_config = compaction_group_manager.default_compaction_config();
495        let mut compaction_groups_txn = compaction_group_manager.start_compaction_groups_txn();
496        compaction_groups_txn.try_create_compaction_groups(&all_group_ids, default_config);
497        commit_multi_var!(self.meta_store_ref(), compaction_groups_txn)?;
498
499        Ok(())
500    }
501}
502
503/// We muse ensure there is an entry exists in [`CompactionGroupManager`] for any
504/// compaction group found in current hummock version. That's done by invoking
505/// `get_or_insert_compaction_group_config` or `get_or_insert_compaction_group_configs` before
506/// adding any group in current hummock version:
507/// 1. initialize default static compaction group.
508/// 2. register new table to new compaction group.
509/// 3. move existent table to new compaction group.
510pub(crate) struct CompactionGroupManager {
511    compaction_groups: BTreeMap<CompactionGroupId, CompactionGroup>,
512    default_config: Arc<CompactionConfig>,
513    /// Tables that write limit is trigger for.
514    pub write_limit: HashMap<CompactionGroupId, WriteLimit>,
515}
516
517impl CompactionGroupManager {
518    /// Starts a transaction to update compaction group configs.
519    pub fn start_compaction_groups_txn(&mut self) -> CompactionGroupTransaction<'_> {
520        CompactionGroupTransaction::new(&mut self.compaction_groups)
521    }
522
523    #[expect(clippy::type_complexity)]
524    pub fn start_owned_compaction_groups_txn<P: DerefMut<Target = Self>>(
525        inner: P,
526    ) -> BTreeMapTransactionInner<
527        CompactionGroupId,
528        CompactionGroup,
529        DerefMutForward<
530            Self,
531            BTreeMap<CompactionGroupId, CompactionGroup>,
532            P,
533            impl Fn(&Self) -> &BTreeMap<CompactionGroupId, CompactionGroup>,
534            impl Fn(&mut Self) -> &mut BTreeMap<CompactionGroupId, CompactionGroup>,
535        >,
536    > {
537        BTreeMapTransactionInner::new(DerefMutForward::new(
538            inner,
539            |mgr| &mgr.compaction_groups,
540            |mgr| &mut mgr.compaction_groups,
541        ))
542    }
543
544    /// Tries to get compaction group config for `compaction_group_id`.
545    pub(crate) fn try_get_compaction_group_config(
546        &self,
547        compaction_group_id: impl Into<CompactionGroupId>,
548    ) -> Option<CompactionGroup> {
549        self.compaction_groups
550            .get(&compaction_group_id.into())
551            .cloned()
552    }
553
554    /// Tries to get compaction group config for `compaction_group_id`.
555    pub(crate) fn default_compaction_config(&self) -> Arc<CompactionConfig> {
556        self.default_config.clone()
557    }
558}
559
560fn update_compaction_config(target: &mut CompactionConfig, items: &[MutableConfig]) -> Result<()> {
561    for item in items {
562        match item {
563            MutableConfig::MaxBytesForLevelBase(c) => {
564                target.max_bytes_for_level_base = *c;
565            }
566            MutableConfig::MaxBytesForLevelMultiplier(c) => {
567                target.max_bytes_for_level_multiplier = *c;
568            }
569            MutableConfig::MaxCompactionBytes(c) => {
570                target.max_compaction_bytes = *c;
571            }
572            MutableConfig::SubLevelMaxCompactionBytes(c) => {
573                target.sub_level_max_compaction_bytes = *c;
574            }
575            MutableConfig::Level0TierCompactFileNumber(c) => {
576                target.level0_tier_compact_file_number = *c;
577            }
578            MutableConfig::TargetFileSizeBase(c) => {
579                target.target_file_size_base = *c;
580            }
581            MutableConfig::CompactionFilterMask(c) => {
582                target.compaction_filter_mask = *c;
583            }
584            MutableConfig::MaxSubCompaction(c) => {
585                target.max_sub_compaction = *c;
586            }
587            MutableConfig::Level0StopWriteThresholdSubLevelNumber(c) => {
588                target.level0_stop_write_threshold_sub_level_number = *c;
589            }
590            MutableConfig::Level0SubLevelCompactLevelCount(c) => {
591                target.level0_sub_level_compact_level_count = *c;
592            }
593            MutableConfig::Level0OverlappingSubLevelCompactLevelCount(c) => {
594                target.level0_overlapping_sub_level_compact_level_count = *c;
595            }
596            MutableConfig::MaxSpaceReclaimBytes(c) => {
597                target.max_space_reclaim_bytes = *c;
598            }
599            MutableConfig::Level0MaxCompactFileNumber(c) => {
600                target.level0_max_compact_file_number = *c;
601            }
602            MutableConfig::EnableEmergencyPicker(c) => {
603                target.enable_emergency_picker = *c;
604            }
605            MutableConfig::TombstoneReclaimRatio(c) => {
606                target.tombstone_reclaim_ratio = *c;
607            }
608            MutableConfig::CompressionAlgorithm(c) => {
609                let level = c.get_level();
610                let max_level = try_u32_max_level(target.max_level)?;
611                if level > max_level {
612                    return Err(Error::CompactionGroup(format!(
613                        "invalid compression_algorithm level {}, max_level is {}",
614                        level, target.max_level
615                    )));
616                }
617
618                let Some(algorithm) = target.compression_algorithm.get_mut(level as usize) else {
619                    return Err(Error::CompactionGroup(format!(
620                        "invalid compression_algorithm level {}, compression_algorithm len is {}",
621                        level,
622                        target.compression_algorithm.len()
623                    )));
624                };
625                algorithm.clone_from(&c.compression_algorithm);
626            }
627            MutableConfig::ResetCompressionAlgorithm(reset) => {
628                if *reset {
629                    target.compression_algorithm =
630                        default_compaction_config::compression_algorithm_vec(try_u32_max_level(
631                            target.max_level,
632                        )?);
633                }
634            }
635            MutableConfig::MaxL0CompactLevelCount(c) => {
636                target.max_l0_compact_level_count = Some(*c);
637            }
638            MutableConfig::SstAllowedTrivialMoveMinSize(c) => {
639                target.sst_allowed_trivial_move_min_size = Some(*c);
640            }
641            MutableConfig::SplitWeightByVnode(c) => {
642                target.split_weight_by_vnode = *c;
643            }
644            MutableConfig::DisableAutoGroupScheduling(c) => {
645                target.disable_auto_group_scheduling = Some(*c);
646            }
647            MutableConfig::MaxOverlappingLevelSize(c) => {
648                target.max_overlapping_level_size = Some(*c);
649            }
650            MutableConfig::SstAllowedTrivialMoveMaxCount(c) => {
651                target.sst_allowed_trivial_move_max_count = Some(*c);
652            }
653            MutableConfig::EmergencyLevel0SstFileCount(c) => {
654                target.emergency_level0_sst_file_count = Some(*c);
655            }
656            MutableConfig::EmergencyLevel0SubLevelPartition(c) => {
657                target.emergency_level0_sub_level_partition = Some(*c);
658            }
659            MutableConfig::Level0StopWriteThresholdMaxSstCount(c) => {
660                target.level0_stop_write_threshold_max_sst_count = Some(*c);
661            }
662            MutableConfig::Level0StopWriteThresholdMaxSize(c) => {
663                target.level0_stop_write_threshold_max_size = Some(*c);
664            }
665            MutableConfig::EnableOptimizeL0IntervalSelection(c) => {
666                target.enable_optimize_l0_interval_selection = Some(*c);
667            }
668            #[expect(deprecated)]
669            MutableConfig::VnodeAlignedLevelSizeThreshold(_) => {
670                // Deprecated. Keep accepting the field for old clients but do not apply it.
671            }
672            MutableConfig::MaxKvCountForXor16(c) => {
673                target.max_kv_count_for_xor16 = optional_non_sentinel_u64_config(*c);
674            }
675            MutableConfig::MaxVnodeKeyRangeBytes(c) => {
676                target.max_vnode_key_range_bytes = optional_positive_u64_config(*c);
677            }
678            MutableConfig::SstableFilterType(c) => {
679                parse_sstable_filter_type(&c.filter_type).map_err(Error::CompactionGroup)?;
680                if target.sstable_filter_type.is_empty() {
681                    target.sstable_filter_type = default_compaction_config::sstable_filter_type();
682                    target
683                        .sstable_filter_type
684                        .resize(target.max_level as usize + 1, "xor16".to_owned());
685                }
686                let idx = c.get_level() as usize;
687                let level_entry = target.sstable_filter_type.get_mut(idx).ok_or_else(|| {
688                    Error::CompactionGroup(format!(
689                        "sstable_filter_type level {} is out of range",
690                        idx
691                    ))
692                })?;
693                level_entry.clone_from(&c.filter_type);
694            }
695            MutableConfig::SstableFilterLayout(c) => {
696                parse_sstable_filter_layout(&c.layout).map_err(Error::CompactionGroup)?;
697                if target.sstable_filter_layout.is_empty() {
698                    target.sstable_filter_layout =
699                        default_compaction_config::sstable_filter_layout();
700                    target
701                        .sstable_filter_layout
702                        .resize(target.max_level as usize + 1, "blocked".to_owned());
703                }
704                let idx = c.get_level() as usize;
705                let level_entry = target.sstable_filter_layout.get_mut(idx).ok_or_else(|| {
706                    Error::CompactionGroup(format!(
707                        "sstable_filter_layout level {} is out of range",
708                        idx
709                    ))
710                })?;
711                level_entry.clone_from(&c.layout);
712            }
713        }
714    }
715    Ok(())
716}
717
718fn optional_u64_config(value: u64) -> Option<u64> {
719    (value != u64::MIN && value != u64::MAX).then_some(value)
720}
721
722fn optional_non_sentinel_u64_config(value: u64) -> Option<u64> {
723    (value != u64::MAX).then_some(value)
724}
725
726fn optional_positive_u64_config(value: u64) -> Option<u64> {
727    optional_u64_config(value).filter(|value| *value > 0)
728}
729
730fn try_u32_max_level(max_level: u64) -> Result<u32> {
731    u32::try_from(max_level).map_err(|_| {
732        Error::CompactionGroup(format!(
733            "invalid max_level {}, expect <= {}",
734            max_level,
735            u32::MAX
736        ))
737    })
738}
739
740impl CompactionGroupTransaction<'_> {
741    /// Inserts compaction group configs if they do not exist.
742    pub fn try_create_compaction_groups(
743        &mut self,
744        compaction_group_ids: &[CompactionGroupId],
745        config: Arc<CompactionConfig>,
746    ) -> bool {
747        let mut trivial = true;
748        for id in compaction_group_ids {
749            if self.contains_key(id) {
750                continue;
751            }
752            let new_entry = CompactionGroup::new(*id, config.as_ref().clone());
753            self.insert(*id, new_entry);
754
755            trivial = false;
756        }
757
758        !trivial
759    }
760
761    pub fn create_compaction_groups(
762        &mut self,
763        compaction_group_id: CompactionGroupId,
764        config: Arc<CompactionConfig>,
765    ) {
766        self.try_create_compaction_groups(&[compaction_group_id], config);
767    }
768
769    /// Tries to get compaction group config for `compaction_group_id`.
770    pub(crate) fn try_get_compaction_group_config(
771        &self,
772        compaction_group_id: CompactionGroupId,
773    ) -> Option<&CompactionGroup> {
774        self.get(&compaction_group_id)
775    }
776
777    /// Removes stale group configs.
778    pub fn purge(&mut self, existing_groups: HashSet<CompactionGroupId>) {
779        let stale_group = self
780            .tree_ref()
781            .keys()
782            .cloned()
783            .filter(|k| !existing_groups.contains(k))
784            .collect_vec();
785        if stale_group.is_empty() {
786            return;
787        }
788        for group in stale_group {
789            self.remove(group);
790        }
791    }
792
793    pub(crate) fn update_compaction_config(
794        &mut self,
795        compaction_group_ids: &[CompactionGroupId],
796        config_to_update: &[MutableConfig],
797    ) -> Result<HashMap<CompactionGroupId, CompactionGroup>> {
798        let mut results = HashMap::default();
799        for compaction_group_id in compaction_group_ids.iter().unique() {
800            let group = self.get(compaction_group_id).ok_or_else(|| {
801                Error::CompactionGroup(format!("invalid group {}", *compaction_group_id))
802            })?;
803            let mut config = group.compaction_config.as_ref().clone();
804            update_compaction_config(&mut config, config_to_update)?;
805            if let Err(reason) = validate_compaction_config(&config) {
806                return Err(Error::CompactionGroup(reason));
807            }
808            let mut new_group = group.clone();
809            new_group.compaction_config = Arc::new(config);
810            self.insert(*compaction_group_id, new_group.clone());
811            results.insert(new_group.group_id(), new_group);
812        }
813
814        Ok(results)
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use std::collections::{BTreeMap, HashSet};
821    use std::sync::Arc;
822
823    use itertools::Itertools;
824    use risingwave_common::id::JobId;
825    use risingwave_hummock_sdk::CompactionGroupId;
826    use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::mutable_config::MutableConfig;
827    use risingwave_pb::hummock::rise_ctl_update_compaction_config_request::{
828        CompressionAlgorithm, SstableFilterLayout, SstableFilterType,
829    };
830
831    use crate::controller::SqlMetaStore;
832    use crate::hummock::commit_multi_var;
833    use crate::hummock::compaction::compaction_config::CompactionConfigBuilder;
834    use crate::hummock::error::Result;
835    use crate::hummock::manager::compaction_group_manager::CompactionGroupManager;
836    use crate::hummock::test_utils::setup_compute_env;
837    use crate::model::{Fragment, StreamJobFragments};
838
839    #[test]
840    fn test_update_compaction_config_filter_type_layout_backward_compat() {
841        let mut config = CompactionConfigBuilder::new().build();
842        config.sstable_filter_type.clear();
843        config.sstable_filter_layout.clear();
844
845        super::update_compaction_config(
846            &mut config,
847            &[MutableConfig::SstableFilterType(SstableFilterType {
848                level: 0,
849                filter_type: "xor8".to_owned(),
850            })],
851        )
852        .unwrap();
853        assert_eq!(
854            config.sstable_filter_type.len(),
855            config.max_level as usize + 1
856        );
857        assert_eq!(config.sstable_filter_type[0], "xor8");
858        assert_eq!(config.sstable_filter_type[5], "xor8");
859
860        super::update_compaction_config(
861            &mut config,
862            &[MutableConfig::SstableFilterType(SstableFilterType {
863                level: 1,
864                filter_type: "none".to_owned(),
865            })],
866        )
867        .unwrap();
868        assert_eq!(config.sstable_filter_type[1], "none");
869
870        super::update_compaction_config(
871            &mut config,
872            &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
873                level: 1,
874                layout: "plain".to_owned(),
875            })],
876        )
877        .unwrap();
878        assert_eq!(
879            config.sstable_filter_layout.len(),
880            config.max_level as usize + 1
881        );
882        assert_eq!(config.sstable_filter_layout[1], "plain");
883        assert_eq!(config.sstable_filter_layout[2], "blocked");
884    }
885
886    #[test]
887    fn test_update_compaction_config_rejects_out_of_range_level() {
888        let mut config = CompactionConfigBuilder::new().build();
889        let oob = config.max_level as u32 + 1;
890
891        assert!(
892            super::update_compaction_config(
893                &mut config,
894                &[MutableConfig::SstableFilterType(SstableFilterType {
895                    level: oob,
896                    filter_type: "xor8".to_owned(),
897                })],
898            )
899            .is_err()
900        );
901
902        assert!(
903            super::update_compaction_config(
904                &mut config,
905                &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
906                    level: oob,
907                    layout: "plain".to_owned(),
908                })],
909            )
910            .is_err()
911        );
912
913        assert!(
914            super::update_compaction_config(
915                &mut config,
916                &[MutableConfig::CompressionAlgorithm(CompressionAlgorithm {
917                    level: oob,
918                    compression_algorithm: "Zstd".to_owned(),
919                })],
920            )
921            .is_err()
922        );
923    }
924
925    #[test]
926    fn test_update_compaction_config_rejects_invalid_filter_metadata() {
927        let mut config = CompactionConfigBuilder::new().build();
928
929        assert!(
930            super::update_compaction_config(
931                &mut config,
932                &[MutableConfig::SstableFilterType(SstableFilterType {
933                    level: 0,
934                    filter_type: "unknown".to_owned(),
935                })],
936            )
937            .is_err()
938        );
939
940        let mut config = CompactionConfigBuilder::new().build();
941
942        assert!(
943            super::update_compaction_config(
944                &mut config,
945                &[MutableConfig::SstableFilterLayout(SstableFilterLayout {
946                    level: 0,
947                    layout: "unknown".to_owned(),
948                })],
949            )
950            .is_err()
951        );
952    }
953
954    #[test]
955    fn test_reset_compression_algorithm_false_is_noop() {
956        let mut config = CompactionConfigBuilder::new().build();
957        config.compression_algorithm[3] = "Zstd".to_owned();
958
959        super::update_compaction_config(
960            &mut config,
961            &[MutableConfig::ResetCompressionAlgorithm(false)],
962        )
963        .unwrap();
964
965        assert_eq!(config.compression_algorithm[3], "Zstd");
966    }
967
968    #[tokio::test]
969    async fn test_inner() {
970        let (env, ..) = setup_compute_env(8080).await;
971        let mut inner = CompactionGroupManager::new(&env).await.unwrap();
972        assert_eq!(inner.compaction_groups.len(), 2);
973
974        async fn update_compaction_config(
975            meta: &SqlMetaStore,
976            inner: &mut CompactionGroupManager,
977            cg_ids: &[impl Into<CompactionGroupId> + Copy],
978            config_to_update: &[MutableConfig],
979        ) -> Result<()> {
980            let cg_ids = cg_ids.iter().copied().map_into().collect_vec();
981            let mut compaction_groups_txn = inner.start_compaction_groups_txn();
982            compaction_groups_txn.update_compaction_config(&cg_ids, config_to_update)?;
983            commit_multi_var!(meta, compaction_groups_txn)
984        }
985
986        async fn insert_compaction_group_configs(
987            meta: &SqlMetaStore,
988            inner: &mut CompactionGroupManager,
989            cg_ids: &[u64],
990        ) {
991            let default_config = inner.default_compaction_config();
992            let mut compaction_groups_txn = inner.start_compaction_groups_txn();
993            if compaction_groups_txn.try_create_compaction_groups(
994                &cg_ids.iter().copied().map_into().collect_vec(),
995                default_config,
996            ) {
997                commit_multi_var!(meta, compaction_groups_txn).unwrap();
998            }
999        }
1000
1001        async fn insert_compaction_group_config_with_max_level(
1002            meta: &SqlMetaStore,
1003            inner: &mut CompactionGroupManager,
1004            cg_id: u64,
1005            max_level: u64,
1006        ) {
1007            let mut config = inner.default_compaction_config().as_ref().clone();
1008            config.max_level = max_level;
1009            config.compression_algorithm =
1010                super::default_compaction_config::compression_algorithm_vec(
1011                    super::try_u32_max_level(max_level).expect("max_level should fit u32 in test"),
1012                );
1013            let mut compaction_groups_txn = inner.start_compaction_groups_txn();
1014            compaction_groups_txn.create_compaction_groups(cg_id.into(), Arc::new(config));
1015            commit_multi_var!(meta, compaction_groups_txn).unwrap();
1016        }
1017
1018        update_compaction_config(env.meta_store_ref(), &mut inner, &[100, 200], &[])
1019            .await
1020            .unwrap_err();
1021        insert_compaction_group_configs(env.meta_store_ref(), &mut inner, &[100, 200]).await;
1022        assert_eq!(inner.compaction_groups.len(), 4);
1023        let mut inner = CompactionGroupManager::new(&env).await.unwrap();
1024        assert_eq!(inner.compaction_groups.len(), 4);
1025
1026        update_compaction_config(
1027            env.meta_store_ref(),
1028            &mut inner,
1029            &[100, 200],
1030            &[MutableConfig::MaxSubCompaction(123)],
1031        )
1032        .await
1033        .unwrap();
1034        assert_eq!(inner.compaction_groups.len(), 4);
1035        assert_eq!(
1036            inner
1037                .try_get_compaction_group_config(100)
1038                .unwrap()
1039                .compaction_config
1040                .max_sub_compaction,
1041            123
1042        );
1043        assert_eq!(
1044            inner
1045                .try_get_compaction_group_config(200)
1046                .unwrap()
1047                .compaction_config
1048                .max_sub_compaction,
1049            123
1050        );
1051
1052        insert_compaction_group_config_with_max_level(env.meta_store_ref(), &mut inner, 300, 4)
1053            .await;
1054        update_compaction_config(
1055            env.meta_store_ref(),
1056            &mut inner,
1057            &[300],
1058            &[MutableConfig::ResetCompressionAlgorithm(true)],
1059        )
1060        .await
1061        .unwrap();
1062        assert_eq!(
1063            inner
1064                .try_get_compaction_group_config(300)
1065                .unwrap()
1066                .compaction_config
1067                .compression_algorithm,
1068            super::default_compaction_config::compression_algorithm_vec(4)
1069        );
1070        let err = update_compaction_config(
1071            env.meta_store_ref(),
1072            &mut inner,
1073            &[300],
1074            &[MutableConfig::CompressionAlgorithm(CompressionAlgorithm {
1075                level: 6,
1076                compression_algorithm: "Zstd".to_owned(),
1077            })],
1078        )
1079        .await
1080        .unwrap_err();
1081        assert!(
1082            err.to_string()
1083                .contains("invalid compression_algorithm level 6")
1084        );
1085
1086        update_compaction_config(
1087            env.meta_store_ref(),
1088            &mut inner,
1089            &[100],
1090            &[MutableConfig::MaxKvCountForXor16(0)],
1091        )
1092        .await
1093        .unwrap();
1094        assert_eq!(
1095            inner
1096                .try_get_compaction_group_config(100)
1097                .unwrap()
1098                .compaction_config
1099                .max_kv_count_for_xor16,
1100            Some(0)
1101        );
1102        update_compaction_config(
1103            env.meta_store_ref(),
1104            &mut inner,
1105            &[100],
1106            &[MutableConfig::MaxKvCountForXor16(1024)],
1107        )
1108        .await
1109        .unwrap();
1110        assert_eq!(
1111            inner
1112                .try_get_compaction_group_config(100)
1113                .unwrap()
1114                .compaction_config
1115                .max_kv_count_for_xor16,
1116            Some(1024)
1117        );
1118        update_compaction_config(
1119            env.meta_store_ref(),
1120            &mut inner,
1121            &[100],
1122            &[MutableConfig::MaxKvCountForXor16(u64::MAX)],
1123        )
1124        .await
1125        .unwrap();
1126        assert_eq!(
1127            inner
1128                .try_get_compaction_group_config(100)
1129                .unwrap()
1130                .compaction_config
1131                .max_kv_count_for_xor16,
1132            None
1133        );
1134
1135        update_compaction_config(
1136            env.meta_store_ref(),
1137            &mut inner,
1138            &[100],
1139            &[MutableConfig::MaxVnodeKeyRangeBytes(0)],
1140        )
1141        .await
1142        .unwrap();
1143        assert_eq!(
1144            inner
1145                .try_get_compaction_group_config(100)
1146                .unwrap()
1147                .compaction_config
1148                .max_vnode_key_range_bytes,
1149            None
1150        );
1151        update_compaction_config(
1152            env.meta_store_ref(),
1153            &mut inner,
1154            &[100],
1155            &[MutableConfig::MaxVnodeKeyRangeBytes(1024)],
1156        )
1157        .await
1158        .unwrap();
1159        assert_eq!(
1160            inner
1161                .try_get_compaction_group_config(100)
1162                .unwrap()
1163                .compaction_config
1164                .max_vnode_key_range_bytes,
1165            Some(1024)
1166        );
1167    }
1168
1169    #[tokio::test]
1170    async fn test_manager() {
1171        let (_, compaction_group_manager, ..) = setup_compute_env(8080).await;
1172        let table_fragment_1 = StreamJobFragments::for_test(
1173            JobId::new(10),
1174            BTreeMap::from([(
1175                1.into(),
1176                Fragment {
1177                    fragment_id: 1.into(),
1178                    state_table_ids: vec![10.into(), 11.into(), 12.into(), 13.into()],
1179                    ..Default::default()
1180                },
1181            )]),
1182        );
1183        let table_fragment_2 = StreamJobFragments::for_test(
1184            JobId::new(20),
1185            BTreeMap::from([(
1186                2.into(),
1187                Fragment {
1188                    fragment_id: 2.into(),
1189                    state_table_ids: vec![20.into(), 21.into(), 22.into(), 23.into()],
1190                    ..Default::default()
1191                },
1192            )]),
1193        );
1194
1195        // Test register_table_fragments
1196        let registered_number = || async {
1197            compaction_group_manager
1198                .list_compaction_group()
1199                .await
1200                .iter()
1201                .map(|cg| cg.member_table_ids.len())
1202                .sum::<usize>()
1203        };
1204        let group_number =
1205            || async { compaction_group_manager.list_compaction_group().await.len() };
1206        assert_eq!(registered_number().await, 0);
1207
1208        compaction_group_manager
1209            .register_table_fragments(
1210                Some(table_fragment_1.stream_job_id().as_mv_table_id()),
1211                table_fragment_1
1212                    .internal_table_ids()
1213                    .into_iter()
1214                    .map_into()
1215                    .collect(),
1216            )
1217            .await
1218            .unwrap();
1219        assert_eq!(registered_number().await, 4);
1220        compaction_group_manager
1221            .register_table_fragments(
1222                Some(table_fragment_2.stream_job_id().as_mv_table_id()),
1223                table_fragment_2
1224                    .internal_table_ids()
1225                    .into_iter()
1226                    .map_into()
1227                    .collect(),
1228            )
1229            .await
1230            .unwrap();
1231        assert_eq!(registered_number().await, 8);
1232
1233        // Test unregister_table_fragments
1234        compaction_group_manager
1235            .unregister_table_fragments_vec(std::slice::from_ref(&table_fragment_1))
1236            .await;
1237        assert_eq!(registered_number().await, 4);
1238
1239        // Test purge_stale_members: table fragments
1240        compaction_group_manager
1241            .purge(&table_fragment_2.all_table_ids().collect())
1242            .await
1243            .unwrap();
1244        assert_eq!(registered_number().await, 4);
1245        compaction_group_manager
1246            .purge(&HashSet::new())
1247            .await
1248            .unwrap();
1249        assert_eq!(registered_number().await, 0);
1250
1251        assert_eq!(group_number().await, 2);
1252
1253        compaction_group_manager
1254            .register_table_fragments(
1255                Some(table_fragment_1.stream_job_id().as_mv_table_id()),
1256                table_fragment_1
1257                    .internal_table_ids()
1258                    .into_iter()
1259                    .map_into()
1260                    .collect(),
1261            )
1262            .await
1263            .unwrap();
1264        assert_eq!(registered_number().await, 4);
1265        assert_eq!(group_number().await, 2);
1266
1267        compaction_group_manager
1268            .unregister_table_fragments_vec(&[table_fragment_1])
1269            .await;
1270        assert_eq!(registered_number().await, 0);
1271        assert_eq!(group_number().await, 2);
1272    }
1273}