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, 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    /// 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(
102            &self
103                .versioning
104                .read_with_process_name("compaction_group_ids")
105                .await
106                .current_version,
107        )
108        .collect_vec()
109    }
110
111    /// The implementation acquires `compaction_group_manager` lock.
112    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    /// Registers `table_fragments` to compaction groups.
122    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            // materialized_view
133            pairs.push((mv_table, StaticCompactionGroupId::MaterializedView));
134        }
135        // internal states
136        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    /// Unregisters `table_fragments` from compaction groups
145    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    /// Unregisters stale members and groups
155    /// The caller should ensure `table_fragments_list` remain unchanged during `purge`.
156    /// Currently `purge` is only called during meta service start ups.
157    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        // As we have released versioning lock, the version that `to_unregister` is calculated from
171        // may not be the same as the one used in unregister_table_ids. It is OK.
172        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    /// The implementation acquires `versioning` lock.
180    ///
181    /// The method name is temporarily added with a `_for_test` prefix to mark
182    /// that it's currently only used in test.
183    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        // All NewCompactionGroup pairs are mapped to one new compaction group.
213        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        // Remove member tables
325        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        // Defer schedule-state cleanup until commit succeeds: it is not transactional and
353        // cannot be restored automatically if the group deletion fails.
354        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        // Purge may cause write to meta store. If it hurts performance while holding versioning
383        // lock, consider to make it in batch.
384        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        // Serialize removal with commit's statistics publication. Cleaning up before taking
400        // versioning could let an in-flight commit recreate a deleted table's history.
401        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        // No need to handle DeltaType::GroupDestroy during time travel.
409        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            // Avoid lock conflicts with `try_update_write_limits``
419            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            // Update write limits with lock
434            self.try_update_write_limits(compaction_group_ids).await;
435        }
436
437        Ok(())
438    }
439
440    /// Gets complete compaction group info.
441    /// It is the aggregate of `HummockVersion` and `CompactionGroupConfig`
442    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        // 1. Due to version compatibility, we fix some of the configuration of older versions after hummock starts.
598        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
609/// We muse ensure there is an entry exists in [`CompactionGroupManager`] for any
610/// compaction group found in current hummock version. That's done by invoking
611/// `get_or_insert_compaction_group_config` or `get_or_insert_compaction_group_configs` before
612/// adding any group in current hummock version:
613/// 1. initialize default static compaction group.
614/// 2. register new table to new compaction group.
615/// 3. move existent table to new compaction group.
616pub(crate) struct CompactionGroupManager {
617    compaction_groups: BTreeMap<CompactionGroupId, CompactionGroup>,
618    default_config: Arc<CompactionConfig>,
619    /// Tables that write limit is trigger for.
620    pub write_limit: HashMap<CompactionGroupId, WriteLimit>,
621}
622
623impl CompactionGroupManager {
624    /// Starts a transaction to update compaction group configs.
625    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    /// Tries to get compaction group config for `compaction_group_id`.
651    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    /// Tries to get compaction group config for `compaction_group_id`.
661    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                // Deprecated. Keep accepting the field for old clients but do not apply it.
777            }
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    /// Inserts compaction group configs if they do not exist.
848    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    /// Tries to get compaction group config for `compaction_group_id`.
876    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    /// Removes stale group configs.
884    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        // Test register_table_fragments
1302        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        // Test unregister_table_fragments
1340        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        // Test purge_stale_members: table fragments
1346        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}