1use 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 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 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 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 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.gc_manager.change_log_retention,
127 self.env.notification_manager(),
128 Some(&self.table_committed_epoch_notifiers),
129 &self.metrics,
130 &self.env.opts,
131 &self.version_stat_tx,
132 );
133
134 let state_table_info = &version.latest_version().state_table_info;
135 let mut table_compaction_group_mapping = state_table_info.build_table_compaction_group_id();
136 let mut new_table_ids = HashMap::new();
137 let mut new_compaction_groups = Vec::new();
138 let mut compaction_group_manager_txn = None;
139 let mut compaction_group_config: Option<Arc<CompactionConfig>> = None;
140
141 for NewTableFragmentInfo { table_ids } in new_table_fragment_infos {
143 let (compaction_group_manager, compaction_group_config) =
144 if let Some(compaction_group_manager) = &mut compaction_group_manager_txn {
145 (
146 compaction_group_manager,
147 (*compaction_group_config
148 .as_ref()
149 .expect("must be set with compaction_group_manager_txn"))
150 .clone(),
151 )
152 } else {
153 let compaction_group_manager_guard = self
154 .compaction_group_manager
155 .write_with_process_name("commit_epoch")
156 .await;
157 let new_compaction_group_config =
158 compaction_group_manager_guard.default_compaction_config();
159 compaction_group_config = Some(new_compaction_group_config.clone());
160 (
161 compaction_group_manager_txn.insert(
162 CompactionGroupManager::start_owned_compaction_groups_txn(
163 compaction_group_manager_guard,
164 ),
165 ),
166 new_compaction_group_config,
167 )
168 };
169 let new_compaction_group_id = next_compaction_group_id(&self.env).await?;
170 let new_compaction_group = CompactionGroup {
171 group_id: new_compaction_group_id,
172 compaction_config: compaction_group_config.clone(),
173 };
174
175 new_compaction_groups.push(new_compaction_group.clone());
176 compaction_group_manager.insert(new_compaction_group_id, new_compaction_group);
177
178 on_handle_add_new_table(
179 state_table_info,
180 &table_ids,
181 new_compaction_group_id,
182 &mut table_compaction_group_mapping,
183 &mut new_table_ids,
184 )?;
185 }
186
187 let commit_sstables = self
188 .correct_commit_ssts(sstables, &table_compaction_group_mapping)
189 .await?;
190
191 let modified_compaction_groups = commit_sstables.keys().cloned().collect_vec();
192 let mut group_id_to_config = HashMap::new();
194 if let Some(compaction_group_manager) = compaction_group_manager_txn.as_ref() {
195 for cg_id in &modified_compaction_groups {
196 let compaction_group = compaction_group_manager
197 .get(cg_id)
198 .unwrap_or_else(|| panic!("compaction group {} should be created", cg_id))
199 .compaction_config();
200 group_id_to_config.insert(*cg_id, compaction_group);
201 }
202 } else {
203 let compaction_group_manager = self
204 .compaction_group_manager
205 .read_with_process_name("commit_epoch")
206 .await;
207 for cg_id in &modified_compaction_groups {
208 let compaction_group = compaction_group_manager
209 .try_get_compaction_group_config(*cg_id)
210 .unwrap_or_else(|| panic!("compaction group {} should be created", cg_id))
211 .compaction_config();
212 group_id_to_config.insert(*cg_id, compaction_group);
213 }
214 }
215
216 let group_id_to_sub_levels =
217 rewrite_commit_sstables_to_sub_level(commit_sstables, &group_id_to_config);
218
219 let mut group_id_to_truncate_tables: HashMap<CompactionGroupId, HashSet<TableId>> =
221 HashMap::new();
222 for table_id in &truncate_tables {
223 if let Some(compaction_group_id) = table_compaction_group_mapping.get(table_id) {
224 group_id_to_truncate_tables
225 .entry(*compaction_group_id)
226 .or_default()
227 .insert(*table_id);
228 } else {
229 bail!(
230 "table {} doesn't belong to any compaction group, skip truncating",
231 table_id
232 );
233 }
234 }
235
236 let time_travel_delta = version.pre_commit_epoch(
237 &tables_to_commit,
238 new_compaction_groups,
239 group_id_to_sub_levels,
240 &new_table_ids,
241 new_table_watermarks,
242 change_log_delta,
243 vector_index_delta,
244 group_id_to_truncate_tables,
245 );
246
247 if should_mark_next_time_travel_version_snapshot(&time_travel_delta) {
248 versioning.time_travel_snapshot_interval_counter = u64::MAX;
250 }
251
252 let mut version_stats = HummockVersionStatsTransaction::new(
254 &mut versioning.version_stats,
255 self.env.notification_manager(),
256 );
257 add_prost_table_stats_map(&mut version_stats.table_stats, &table_stats_change);
258 if purge_prost_table_stats(
259 &mut version_stats.table_stats,
260 version.latest_version(),
261 &truncate_tables,
262 ) {
263 self.metrics.version_stats.reset();
264 versioning.local_metrics.clear();
265 }
266
267 trigger_local_table_stat(
268 &self.metrics,
269 &mut versioning.local_metrics,
270 &version_stats,
271 &table_stats_change,
272 );
273 for (table_id, stats) in &table_stats_change {
274 if stats.total_key_size == 0
275 && stats.total_value_size == 0
276 && stats.total_key_count == 0
277 {
278 continue;
279 }
280 let stats_value = std::cmp::max(0, stats.total_key_size + stats.total_value_size);
281 let table_metrics = get_or_create_local_table_stat(
282 &self.metrics,
283 *table_id,
284 &mut versioning.local_metrics,
285 );
286 table_metrics.inc_write_throughput(stats_value as u64);
287 }
288 let mut time_travel_version = None;
289 if versioning.time_travel_snapshot_interval_counter
290 >= self.env.opts.hummock_time_travel_snapshot_interval
291 {
292 versioning.time_travel_snapshot_interval_counter = 0;
293 time_travel_version = Some(version.latest_version());
294 } else {
295 versioning.time_travel_snapshot_interval_counter = versioning
296 .time_travel_snapshot_interval_counter
297 .saturating_add(1);
298 }
299 let time_travel_tables_to_commit =
300 table_compaction_group_mapping
301 .iter()
302 .filter_map(|(table_id, cg_id)| {
303 tables_to_commit
304 .get(table_id)
305 .map(|committed_epoch| (table_id, cg_id, *committed_epoch))
306 });
307 let time_travel_table_ids: HashSet<_> = self
308 .metadata_manager
309 .catalog_controller
310 .list_time_travel_table_ids()
311 .await
312 .map_err(|e| Error::Internal(e.into()))?
313 .into_iter()
314 .collect();
315 let mut txn = self.env.meta_store_ref().conn.begin().await?;
316 let version_snapshot_sst_ids = self
317 .write_time_travel_metadata(
318 &txn,
319 time_travel_version,
320 time_travel_delta,
321 time_travel_table_ids,
322 &versioning.last_time_travel_snapshot_sst_ids,
323 time_travel_tables_to_commit,
324 )
325 .await?;
326 commit_multi_var_with_provided_txn!(
327 txn,
328 version,
329 version_stats,
330 compaction_group_manager_txn
331 )?;
332 if let Some(version_snapshot_sst_ids) = version_snapshot_sst_ids {
333 versioning.last_time_travel_snapshot_sst_ids = version_snapshot_sst_ids;
334 }
335
336 for compaction_group_id in &modified_compaction_groups {
337 trigger_sst_stat(
338 &self.metrics,
339 None,
340 &versioning.current_version,
341 *compaction_group_id,
342 );
343 }
344 trigger_epoch_stat(&self.metrics, &versioning.current_version);
345 let table_change_log_object_ids_after_commit = versioning
346 .table_change_log
347 .values()
348 .flat_map(|l| l.get_object_ids())
349 .collect::<HashSet<_>>();
350 if !self.env.opts.compaction_deterministic_test {
352 for id in &modified_compaction_groups {
353 self.try_send_compaction_request(*id, compact_task::TaskType::Dynamic);
354 }
355 }
356 {
357 let stats = (!self.env.opts.compaction_deterministic_test)
361 .then(|| self.table_write_throughput_statistic_manager.write());
362 drop(versioning_guard);
363 if let Some(mut stats) = stats {
364 let now = tokio::time::Instant::now();
367 for &table_id in tables_to_commit.keys() {
368 let bytes = table_stats_change.get(&table_id).map_or(0, |stat| {
369 (stat.total_value_size + stat.total_key_size).max(0) as u64
370 });
371 stats.record_commit(table_id, bytes, now);
372 }
373 }
374 }
375 self.update_table_change_log_progress(table_change_log_progress);
376 drop(table_stats_change);
377 let may_delete_object_ids =
378 &table_change_log_object_ids_before_commit - &table_change_log_object_ids_after_commit;
379 self.gc_manager
380 .add_may_delete_object_ids(may_delete_object_ids.into_iter());
381
382 if !modified_compaction_groups.is_empty() {
383 self.try_update_write_limits(&modified_compaction_groups)
384 .await;
385 }
386 #[cfg(test)]
387 {
388 self.check_state_consistency().await;
389 }
390 Ok(())
391 }
392
393 async fn correct_commit_ssts(
394 &self,
395 sstables: Vec<LocalSstableInfo>,
396 table_compaction_group_mapping: &HashMap<TableId, CompactionGroupId>,
397 ) -> Result<BTreeMap<CompactionGroupId, Vec<SstableInfo>>> {
398 let mut new_sst_id_number = 0;
399 let mut sst_to_cg_vec = Vec::with_capacity(sstables.len());
400 let commit_object_id_vec = sstables.iter().map(|s| s.sst_info.object_id).collect_vec();
401 for commit_sst in sstables {
402 let mut group_table_ids: BTreeMap<CompactionGroupId, Vec<TableId>> = BTreeMap::new();
403 for table_id in &commit_sst.sst_info.table_ids {
404 match table_compaction_group_mapping.get(table_id) {
405 Some(cg_id_from_meta) => {
406 group_table_ids
407 .entry(*cg_id_from_meta)
408 .or_default()
409 .push(*table_id);
410 }
411 None => {
412 tracing::warn!(
413 %table_id,
414 object_id = %commit_sst.sst_info.object_id,
415 "table doesn't belong to any compaction group",
416 );
417 }
418 }
419 }
420
421 new_sst_id_number += group_table_ids.len() * 2; sst_to_cg_vec.push((commit_sst, group_table_ids));
423 }
424
425 let mut new_sst_id = next_sstable_id(&self.env, new_sst_id_number).await?;
429 let mut commit_sstables: BTreeMap<CompactionGroupId, Vec<SstableInfo>> = BTreeMap::new();
430
431 for (mut sst, group_table_ids) in sst_to_cg_vec {
432 let len = group_table_ids.len();
433 for (index, (group_id, match_ids)) in group_table_ids.into_iter().enumerate() {
434 if sst.sst_info.table_ids == match_ids {
435 assert!(
437 index == len - 1,
438 "SST should be the last key in the group {} index {} len {}",
439 group_id,
440 index,
441 len
442 );
443 commit_sstables
444 .entry(group_id)
445 .or_default()
446 .push(sst.sst_info);
447 break;
448 }
449
450 let origin_sst_size = sst.sst_info.sst_size;
451 let new_sst_size = match_ids
452 .iter()
453 .map(|id| {
454 let stat = sst.table_stats.get(id).unwrap();
455 stat.total_compressed_size
456 })
457 .sum();
458
459 if new_sst_size == 0 {
460 tracing::warn!(
461 id = %sst.sst_info.sst_id,
462 object_id = %sst.sst_info.object_id,
463 match_ids = ?match_ids,
464 "Sstable doesn't contain any data for tables",
465 );
466 }
467
468 let old_sst_size = origin_sst_size.saturating_sub(new_sst_size);
469 if old_sst_size == 0 {
470 tracing::warn!(
471 id = %sst.sst_info.sst_id,
472 object_id = %sst.sst_info.object_id,
473 match_ids = ?match_ids,
474 origin_sst_size = origin_sst_size,
475 new_sst_size = new_sst_size,
476 "Sstable doesn't contain any data for tables",
477 );
478 }
479 let (modified_sst_info, branch_sst) = split_sst_with_table_ids(
480 &sst.sst_info,
481 &mut new_sst_id,
482 old_sst_size,
483 new_sst_size,
484 match_ids,
485 );
486 sst.sst_info = modified_sst_info;
487
488 commit_sstables
489 .entry(group_id)
490 .or_default()
491 .push(branch_sst);
492 }
493 }
494
495 for ssts in commit_sstables.values() {
497 let object_ids = ssts.iter().map(|s| s.object_id).collect_vec();
498 assert!(is_ordered_subset(&commit_object_id_vec, &object_ids));
499 }
500
501 Ok(commit_sstables)
502 }
503}
504
505fn on_handle_add_new_table(
506 state_table_info: &HummockVersionStateTableInfo,
507 table_ids: impl IntoIterator<Item = &TableId>,
508 compaction_group_id: CompactionGroupId,
509 table_compaction_group_mapping: &mut HashMap<TableId, CompactionGroupId>,
510 new_table_ids: &mut HashMap<TableId, CompactionGroupId>,
511) -> Result<()> {
512 for table_id in table_ids {
513 if let Some(info) = state_table_info.info().get(table_id) {
514 return Err(Error::CompactionGroup(format!(
515 "table {} already exist {:?}",
516 table_id, info,
517 )));
518 }
519 table_compaction_group_mapping.insert(*table_id, compaction_group_id);
520 new_table_ids.insert(*table_id, compaction_group_id);
521 }
522
523 Ok(())
524}
525
526fn rewrite_commit_sstables_to_sub_level(
529 commit_sstables: BTreeMap<CompactionGroupId, Vec<SstableInfo>>,
530 group_id_to_config: &HashMap<CompactionGroupId, Arc<CompactionConfig>>,
531) -> BTreeMap<CompactionGroupId, Vec<Vec<SstableInfo>>> {
532 let mut overlapping_sstables: BTreeMap<CompactionGroupId, Vec<Vec<SstableInfo>>> =
533 BTreeMap::new();
534 for (group_id, inserted_table_infos) in commit_sstables {
535 let config = group_id_to_config
536 .get(&group_id)
537 .expect("compaction group should exist");
538
539 let mut accumulated_size = 0;
540 let mut ssts = vec![];
541 let sub_level_size_limit = config
542 .max_overlapping_level_size
543 .unwrap_or(compaction_config::max_overlapping_level_size());
544
545 let level = overlapping_sstables.entry(group_id).or_default();
546
547 for sst in inserted_table_infos {
548 accumulated_size += sst.sst_size;
549 ssts.push(sst);
550 if accumulated_size > sub_level_size_limit {
551 level.push(ssts);
552
553 accumulated_size = 0;
555 ssts = vec![];
556 }
557 }
558
559 if !ssts.is_empty() {
560 level.push(ssts);
561 }
562
563 level.reverse();
565 }
566
567 overlapping_sstables
568}
569
570fn is_ordered_subset<T: PartialEq>(vec_1: &Vec<T>, vec_2: &Vec<T>) -> bool {
571 let mut vec_2_iter = vec_2.iter().peekable();
572 for item in vec_1 {
573 if vec_2_iter.peek() == Some(&item) {
574 vec_2_iter.next();
575 }
576 }
577
578 vec_2_iter.peek().is_none()
579}