Skip to main content

risingwave_meta/hummock/manager/
commit_epoch.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::sync::Arc;
17
18use itertools::Itertools;
19use risingwave_common::bail;
20use risingwave_common::catalog::TableId;
21use risingwave_common::config::meta::default::compaction_config;
22use risingwave_hummock_sdk::change_log::EpochNewChangeLog;
23use risingwave_hummock_sdk::compaction_group::group_split::split_sst_with_table_ids;
24use risingwave_hummock_sdk::sstable_info::SstableInfo;
25use risingwave_hummock_sdk::table_stats::{
26    PbTableStatsMap, add_prost_table_stats_map, purge_prost_table_stats, to_prost_table_stats_map,
27};
28use risingwave_hummock_sdk::table_watermark::TableWatermarks;
29use risingwave_hummock_sdk::vector_index::VectorIndexDelta;
30use risingwave_hummock_sdk::version::HummockVersionStateTableInfo;
31use risingwave_hummock_sdk::{
32    CompactionGroupId, HummockContextId, HummockSstableObjectId, LocalSstableInfo,
33};
34use risingwave_pb::hummock::{CompactionConfig, compact_task};
35use sea_orm::TransactionTrait;
36
37use crate::hummock::error::{Error, Result};
38use crate::hummock::manager::compaction_group_manager::CompactionGroupManager;
39use crate::hummock::manager::transaction::{
40    HummockVersionStatsTransaction, HummockVersionTransaction,
41};
42use crate::hummock::manager::versioning::Versioning;
43use crate::hummock::metrics_utils::{
44    get_or_create_local_table_stat, trigger_epoch_stat, trigger_local_table_stat, trigger_sst_stat,
45};
46use crate::hummock::model::CompactionGroup;
47use crate::hummock::sequence::{next_compaction_group_id, next_sstable_id};
48use crate::hummock::time_travel::should_mark_next_time_travel_version_snapshot;
49use crate::hummock::{
50    CommittedTableChangeLogProgress, HummockManager, commit_multi_var_with_provided_txn,
51};
52
53pub struct NewTableFragmentInfo {
54    pub table_ids: HashSet<TableId>,
55}
56
57#[derive(Default)]
58pub struct CommitEpochInfo {
59    pub sstables: Vec<LocalSstableInfo>,
60    pub new_table_watermarks: HashMap<TableId, TableWatermarks>,
61    pub sst_to_context: HashMap<HummockSstableObjectId, HummockContextId>,
62    pub new_table_fragment_infos: Vec<NewTableFragmentInfo>,
63    pub change_log_delta: HashMap<TableId, EpochNewChangeLog>,
64    pub vector_index_delta: HashMap<TableId, VectorIndexDelta>,
65    /// `table_id` -> `committed_epoch`
66    pub tables_to_commit: HashMap<TableId, u64>,
67    pub table_change_log_progress: Vec<CommittedTableChangeLogProgress>,
68
69    pub truncate_tables: HashSet<TableId>,
70}
71
72impl HummockManager {
73    /// Caller should ensure `epoch` > `committed_epoch` of `tables_to_commit`
74    /// if tables are not newly added via `new_table_fragment_info`
75    pub async fn commit_epoch(&self, commit_info: CommitEpochInfo) -> Result<()> {
76        let CommitEpochInfo {
77            mut sstables,
78            new_table_watermarks,
79            sst_to_context,
80            new_table_fragment_infos,
81            change_log_delta,
82            vector_index_delta,
83            tables_to_commit,
84            table_change_log_progress,
85            truncate_tables,
86        } = commit_info;
87        let mut versioning_guard = self
88            .versioning
89            .write_with_process_name("commit_epoch")
90            .await;
91        // Prevent commit new epochs if this flag is set
92        if versioning_guard.disable_commit_epochs {
93            return Ok(());
94        }
95
96        assert!(!tables_to_commit.is_empty());
97
98        let versioning: &mut Versioning = &mut versioning_guard;
99        self.commit_epoch_sanity_check(
100            &tables_to_commit,
101            &sstables,
102            &sst_to_context,
103            &versioning.current_version,
104        )
105        .await?;
106
107        // Consume and aggregate table stats.
108        let mut table_stats_change = PbTableStatsMap::default();
109        for s in &mut sstables {
110            add_prost_table_stats_map(
111                &mut table_stats_change,
112                &to_prost_table_stats_map(s.table_stats.clone()),
113            );
114        }
115
116        let table_change_log_object_ids_before_commit = versioning
117            .table_change_log
118            .values()
119            .flat_map(|l| l.get_object_ids())
120            .collect::<HashSet<_>>();
121
122        let mut version = HummockVersionTransaction::new(
123            &mut versioning.current_version,
124            &mut versioning.hummock_version_deltas,
125            &mut versioning.table_change_log,
126            self.env.notification_manager(),
127            Some(&self.table_committed_epoch_notifiers),
128            &self.metrics,
129            &self.env.opts,
130            &self.version_stat_tx,
131        );
132
133        let state_table_info = &version.latest_version().state_table_info;
134        let mut table_compaction_group_mapping = state_table_info.build_table_compaction_group_id();
135        let mut new_table_ids = HashMap::new();
136        let mut new_compaction_groups = Vec::new();
137        let mut compaction_group_manager_txn = None;
138        let mut compaction_group_config: Option<Arc<CompactionConfig>> = None;
139
140        // Add new table
141        for NewTableFragmentInfo { table_ids } in new_table_fragment_infos {
142            let (compaction_group_manager, compaction_group_config) =
143                if let Some(compaction_group_manager) = &mut compaction_group_manager_txn {
144                    (
145                        compaction_group_manager,
146                        (*compaction_group_config
147                            .as_ref()
148                            .expect("must be set with compaction_group_manager_txn"))
149                        .clone(),
150                    )
151                } else {
152                    let compaction_group_manager_guard = self
153                        .compaction_group_manager
154                        .write_with_process_name("commit_epoch")
155                        .await;
156                    let new_compaction_group_config =
157                        compaction_group_manager_guard.default_compaction_config();
158                    compaction_group_config = Some(new_compaction_group_config.clone());
159                    (
160                        compaction_group_manager_txn.insert(
161                            CompactionGroupManager::start_owned_compaction_groups_txn(
162                                compaction_group_manager_guard,
163                            ),
164                        ),
165                        new_compaction_group_config,
166                    )
167                };
168            let new_compaction_group_id = next_compaction_group_id(&self.env).await?;
169            let new_compaction_group = CompactionGroup {
170                group_id: new_compaction_group_id,
171                compaction_config: compaction_group_config.clone(),
172            };
173
174            new_compaction_groups.push(new_compaction_group.clone());
175            compaction_group_manager.insert(new_compaction_group_id, new_compaction_group);
176
177            on_handle_add_new_table(
178                state_table_info,
179                &table_ids,
180                new_compaction_group_id,
181                &mut table_compaction_group_mapping,
182                &mut new_table_ids,
183            )?;
184        }
185
186        let commit_sstables = self
187            .correct_commit_ssts(sstables, &table_compaction_group_mapping)
188            .await?;
189
190        let modified_compaction_groups = commit_sstables.keys().cloned().collect_vec();
191        // fill compaction_groups
192        let mut group_id_to_config = HashMap::new();
193        if let Some(compaction_group_manager) = compaction_group_manager_txn.as_ref() {
194            for cg_id in &modified_compaction_groups {
195                let compaction_group = compaction_group_manager
196                    .get(cg_id)
197                    .unwrap_or_else(|| panic!("compaction group {} should be created", cg_id))
198                    .compaction_config();
199                group_id_to_config.insert(*cg_id, compaction_group);
200            }
201        } else {
202            let compaction_group_manager = self
203                .compaction_group_manager
204                .read_with_process_name("commit_epoch")
205                .await;
206            for cg_id in &modified_compaction_groups {
207                let compaction_group = compaction_group_manager
208                    .try_get_compaction_group_config(*cg_id)
209                    .unwrap_or_else(|| panic!("compaction group {} should be created", cg_id))
210                    .compaction_config();
211                group_id_to_config.insert(*cg_id, compaction_group);
212            }
213        }
214
215        let group_id_to_sub_levels =
216            rewrite_commit_sstables_to_sub_level(commit_sstables, &group_id_to_config);
217
218        // build group_id to truncate tables
219        let mut group_id_to_truncate_tables: HashMap<CompactionGroupId, HashSet<TableId>> =
220            HashMap::new();
221        for table_id in &truncate_tables {
222            if let Some(compaction_group_id) = table_compaction_group_mapping.get(table_id) {
223                group_id_to_truncate_tables
224                    .entry(*compaction_group_id)
225                    .or_default()
226                    .insert(*table_id);
227            } else {
228                bail!(
229                    "table {} doesn't belong to any compaction group, skip truncating",
230                    table_id
231                );
232            }
233        }
234
235        let time_travel_delta = version.pre_commit_epoch(
236            &tables_to_commit,
237            new_compaction_groups,
238            group_id_to_sub_levels,
239            &new_table_ids,
240            new_table_watermarks,
241            change_log_delta,
242            vector_index_delta,
243            group_id_to_truncate_tables,
244        );
245
246        if should_mark_next_time_travel_version_snapshot(&time_travel_delta) {
247            // Unable to invoke mark_next_time_travel_version_snapshot because versioning is already mutable borrowed.
248            versioning.time_travel_snapshot_interval_counter = u64::MAX;
249        }
250
251        // Apply stats changes.
252        let mut version_stats = HummockVersionStatsTransaction::new(
253            &mut versioning.version_stats,
254            self.env.notification_manager(),
255        );
256        add_prost_table_stats_map(&mut version_stats.table_stats, &table_stats_change);
257        if purge_prost_table_stats(
258            &mut version_stats.table_stats,
259            version.latest_version(),
260            &truncate_tables,
261        ) {
262            self.metrics.version_stats.reset();
263            versioning.local_metrics.clear();
264        }
265
266        trigger_local_table_stat(
267            &self.metrics,
268            &mut versioning.local_metrics,
269            &version_stats,
270            &table_stats_change,
271        );
272        for (table_id, stats) in &table_stats_change {
273            if stats.total_key_size == 0
274                && stats.total_value_size == 0
275                && stats.total_key_count == 0
276            {
277                continue;
278            }
279            let stats_value = std::cmp::max(0, stats.total_key_size + stats.total_value_size);
280            let table_metrics = get_or_create_local_table_stat(
281                &self.metrics,
282                *table_id,
283                &mut versioning.local_metrics,
284            );
285            table_metrics.inc_write_throughput(stats_value as u64);
286        }
287        let mut time_travel_version = None;
288        if versioning.time_travel_snapshot_interval_counter
289            >= self.env.opts.hummock_time_travel_snapshot_interval
290        {
291            versioning.time_travel_snapshot_interval_counter = 0;
292            time_travel_version = Some(version.latest_version());
293        } else {
294            versioning.time_travel_snapshot_interval_counter = versioning
295                .time_travel_snapshot_interval_counter
296                .saturating_add(1);
297        }
298        let time_travel_tables_to_commit =
299            table_compaction_group_mapping
300                .iter()
301                .filter_map(|(table_id, cg_id)| {
302                    tables_to_commit
303                        .get(table_id)
304                        .map(|committed_epoch| (table_id, cg_id, *committed_epoch))
305                });
306        let time_travel_table_ids: HashSet<_> = self
307            .metadata_manager
308            .catalog_controller
309            .list_time_travel_table_ids()
310            .await
311            .map_err(|e| Error::Internal(e.into()))?
312            .into_iter()
313            .collect();
314        let mut txn = self.env.meta_store_ref().conn.begin().await?;
315        let version_snapshot_sst_ids = self
316            .write_time_travel_metadata(
317                &txn,
318                time_travel_version,
319                time_travel_delta,
320                time_travel_table_ids,
321                &versioning.last_time_travel_snapshot_sst_ids,
322                time_travel_tables_to_commit,
323            )
324            .await?;
325        commit_multi_var_with_provided_txn!(
326            txn,
327            version,
328            version_stats,
329            compaction_group_manager_txn
330        )?;
331        if let Some(version_snapshot_sst_ids) = version_snapshot_sst_ids {
332            versioning.last_time_travel_snapshot_sst_ids = version_snapshot_sst_ids;
333        }
334
335        for compaction_group_id in &modified_compaction_groups {
336            trigger_sst_stat(
337                &self.metrics,
338                None,
339                &versioning.current_version,
340                *compaction_group_id,
341            );
342        }
343        trigger_epoch_stat(&self.metrics, &versioning.current_version);
344        let table_change_log_object_ids_after_commit = versioning
345            .table_change_log
346            .values()
347            .flat_map(|l| l.get_object_ids())
348            .collect::<HashSet<_>>();
349        // Publish candidates while the committed groups are protected from deletion.
350        if !self.env.opts.compaction_deterministic_test {
351            for id in &modified_compaction_groups {
352                self.try_send_compaction_request(*id, compact_task::TaskType::Dynamic);
353            }
354        }
355        {
356            // Hand off publication from versioning to statistics. A merge that sees the
357            // committed version must wait for its load observations, but the per-table
358            // sampling work itself runs after releasing the version lock.
359            let stats = (!self.env.opts.compaction_deterministic_test)
360                .then(|| self.table_write_throughput_statistic_manager.write());
361            drop(versioning_guard);
362            if let Some(mut stats) = stats {
363                // Commit membership is authoritative: raw SST statistics can still mention
364                // dropped tables. Empty commits for live tables are real zero observations.
365                let now = tokio::time::Instant::now();
366                for &table_id in tables_to_commit.keys() {
367                    let bytes = table_stats_change.get(&table_id).map_or(0, |stat| {
368                        (stat.total_value_size + stat.total_key_size).max(0) as u64
369                    });
370                    stats.record_commit(table_id, bytes, now);
371                }
372            }
373        }
374        self.update_table_change_log_progress(table_change_log_progress);
375        drop(table_stats_change);
376        let may_delete_object_ids =
377            &table_change_log_object_ids_before_commit - &table_change_log_object_ids_after_commit;
378        self.gc_manager
379            .add_may_delete_object_ids(may_delete_object_ids.into_iter());
380
381        if !modified_compaction_groups.is_empty() {
382            self.try_update_write_limits(&modified_compaction_groups)
383                .await;
384        }
385        #[cfg(test)]
386        {
387            self.check_state_consistency().await;
388        }
389        Ok(())
390    }
391
392    async fn correct_commit_ssts(
393        &self,
394        sstables: Vec<LocalSstableInfo>,
395        table_compaction_group_mapping: &HashMap<TableId, CompactionGroupId>,
396    ) -> Result<BTreeMap<CompactionGroupId, Vec<SstableInfo>>> {
397        let mut new_sst_id_number = 0;
398        let mut sst_to_cg_vec = Vec::with_capacity(sstables.len());
399        let commit_object_id_vec = sstables.iter().map(|s| s.sst_info.object_id).collect_vec();
400        for commit_sst in sstables {
401            let mut group_table_ids: BTreeMap<CompactionGroupId, Vec<TableId>> = BTreeMap::new();
402            for table_id in &commit_sst.sst_info.table_ids {
403                match table_compaction_group_mapping.get(table_id) {
404                    Some(cg_id_from_meta) => {
405                        group_table_ids
406                            .entry(*cg_id_from_meta)
407                            .or_default()
408                            .push(*table_id);
409                    }
410                    None => {
411                        tracing::warn!(
412                            %table_id,
413                            object_id = %commit_sst.sst_info.object_id,
414                            "table doesn't belong to any compaction group",
415                        );
416                    }
417                }
418            }
419
420            new_sst_id_number += group_table_ids.len() * 2; // `split_sst` will split the SST into two parts and consumer 2 SST IDs
421            sst_to_cg_vec.push((commit_sst, group_table_ids));
422        }
423
424        // Generate new SST IDs for each compaction group
425        // `next_sstable_id` will update the global SST ID and reserve the new SST IDs
426        // So we need to get the new SST ID first and then split the SSTs
427        let mut new_sst_id = next_sstable_id(&self.env, new_sst_id_number).await?;
428        let mut commit_sstables: BTreeMap<CompactionGroupId, Vec<SstableInfo>> = BTreeMap::new();
429
430        for (mut sst, group_table_ids) in sst_to_cg_vec {
431            let len = group_table_ids.len();
432            for (index, (group_id, match_ids)) in group_table_ids.into_iter().enumerate() {
433                if sst.sst_info.table_ids == match_ids {
434                    // The SST contains all the tables in the group should be last key
435                    assert!(
436                        index == len - 1,
437                        "SST should be the last key in the group {} index {} len {}",
438                        group_id,
439                        index,
440                        len
441                    );
442                    commit_sstables
443                        .entry(group_id)
444                        .or_default()
445                        .push(sst.sst_info);
446                    break;
447                }
448
449                let origin_sst_size = sst.sst_info.sst_size;
450                let new_sst_size = match_ids
451                    .iter()
452                    .map(|id| {
453                        let stat = sst.table_stats.get(id).unwrap();
454                        stat.total_compressed_size
455                    })
456                    .sum();
457
458                if new_sst_size == 0 {
459                    tracing::warn!(
460                        id = %sst.sst_info.sst_id,
461                        object_id = %sst.sst_info.object_id,
462                        match_ids = ?match_ids,
463                        "Sstable doesn't contain any data for tables",
464                    );
465                }
466
467                let old_sst_size = origin_sst_size.saturating_sub(new_sst_size);
468                if old_sst_size == 0 {
469                    tracing::warn!(
470                        id = %sst.sst_info.sst_id,
471                        object_id = %sst.sst_info.object_id,
472                        match_ids = ?match_ids,
473                        origin_sst_size = origin_sst_size,
474                        new_sst_size = new_sst_size,
475                        "Sstable doesn't contain any data for tables",
476                    );
477                }
478                let (modified_sst_info, branch_sst) = split_sst_with_table_ids(
479                    &sst.sst_info,
480                    &mut new_sst_id,
481                    old_sst_size,
482                    new_sst_size,
483                    match_ids,
484                );
485                sst.sst_info = modified_sst_info;
486
487                commit_sstables
488                    .entry(group_id)
489                    .or_default()
490                    .push(branch_sst);
491            }
492        }
493
494        // order check
495        for ssts in commit_sstables.values() {
496            let object_ids = ssts.iter().map(|s| s.object_id).collect_vec();
497            assert!(is_ordered_subset(&commit_object_id_vec, &object_ids));
498        }
499
500        Ok(commit_sstables)
501    }
502}
503
504fn on_handle_add_new_table(
505    state_table_info: &HummockVersionStateTableInfo,
506    table_ids: impl IntoIterator<Item = &TableId>,
507    compaction_group_id: CompactionGroupId,
508    table_compaction_group_mapping: &mut HashMap<TableId, CompactionGroupId>,
509    new_table_ids: &mut HashMap<TableId, CompactionGroupId>,
510) -> Result<()> {
511    for table_id in table_ids {
512        if let Some(info) = state_table_info.info().get(table_id) {
513            return Err(Error::CompactionGroup(format!(
514                "table {} already exist {:?}",
515                table_id, info,
516            )));
517        }
518        table_compaction_group_mapping.insert(*table_id, compaction_group_id);
519        new_table_ids.insert(*table_id, compaction_group_id);
520    }
521
522    Ok(())
523}
524
525/// Rewrite the commit sstables to sub-levels based on the compaction group config.
526/// The type of `compaction_group_manager_txn` is too complex to be used in the function signature. So we use `HashMap` instead.
527fn rewrite_commit_sstables_to_sub_level(
528    commit_sstables: BTreeMap<CompactionGroupId, Vec<SstableInfo>>,
529    group_id_to_config: &HashMap<CompactionGroupId, Arc<CompactionConfig>>,
530) -> BTreeMap<CompactionGroupId, Vec<Vec<SstableInfo>>> {
531    let mut overlapping_sstables: BTreeMap<CompactionGroupId, Vec<Vec<SstableInfo>>> =
532        BTreeMap::new();
533    for (group_id, inserted_table_infos) in commit_sstables {
534        let config = group_id_to_config
535            .get(&group_id)
536            .expect("compaction group should exist");
537
538        let mut accumulated_size = 0;
539        let mut ssts = vec![];
540        let sub_level_size_limit = config
541            .max_overlapping_level_size
542            .unwrap_or(compaction_config::max_overlapping_level_size());
543
544        let level = overlapping_sstables.entry(group_id).or_default();
545
546        for sst in inserted_table_infos {
547            accumulated_size += sst.sst_size;
548            ssts.push(sst);
549            if accumulated_size > sub_level_size_limit {
550                level.push(ssts);
551
552                // reset the accumulated size and ssts
553                accumulated_size = 0;
554                ssts = vec![];
555            }
556        }
557
558        if !ssts.is_empty() {
559            level.push(ssts);
560        }
561
562        // The uploader organizes the ssts in decreasing epoch order, so the level needs to be reversed to ensure that the latest epoch is at the top.
563        level.reverse();
564    }
565
566    overlapping_sstables
567}
568
569fn is_ordered_subset<T: PartialEq>(vec_1: &Vec<T>, vec_2: &Vec<T>) -> bool {
570    let mut vec_2_iter = vec_2.iter().peekable();
571    for item in vec_1 {
572        if vec_2_iter.peek() == Some(&item) {
573            vec_2_iter.next();
574        }
575    }
576
577    vec_2_iter.peek().is_none()
578}