Skip to main content

risingwave_meta/hummock/manager/
versioning.rs

1// Copyright 2022 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::cmp;
16use std::collections::Bound::{Excluded, Included};
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::sync::Arc;
19
20use itertools::Itertools;
21use risingwave_hummock_sdk::change_log::{EpochNewChangeLog, TableChangeLog, TableChangeLogs};
22use risingwave_hummock_sdk::compaction_group::StateTableId;
23use risingwave_hummock_sdk::compaction_group::hummock_version_ext::{
24    BranchedSstInfo, get_compaction_group_ids, get_table_compaction_group_id_mapping,
25};
26use risingwave_hummock_sdk::sstable_info::SstableInfo;
27use risingwave_hummock_sdk::table_stats::{PbTableStatsMap, add_prost_table_stats_map};
28use risingwave_hummock_sdk::version::{
29    HummockVersion, HummockVersionDelta, MAX_HUMMOCK_VERSION_ID,
30};
31use risingwave_hummock_sdk::{
32    CompactionGroupId, HummockContextId, HummockObjectId, HummockSstableId, HummockSstableObjectId,
33    HummockVersionId, get_stale_object_ids,
34};
35use risingwave_meta_model::{Epoch, hummock_table_change_log};
36use risingwave_pb::common::WorkerNode;
37use risingwave_pb::hummock::write_limits::WriteLimit;
38use risingwave_pb::hummock::{HummockPinnedVersion, HummockVersionStats};
39use risingwave_pb::id::TableId;
40use risingwave_pb::meta::subscribe_response::{Info, Operation};
41use sea_orm::{EntityTrait, QuerySelect, TransactionTrait};
42
43use super::GroupStateValidator;
44use crate::MetaResult;
45use crate::hummock::HummockManager;
46use crate::hummock::error::Result;
47use crate::hummock::manager::checkpoint::HummockVersionCheckpoint;
48use crate::hummock::manager::commit_multi_var;
49use crate::hummock::manager::context::ContextInfo;
50use crate::hummock::manager::transaction::HummockVersionTransaction;
51use crate::hummock::metrics_utils::{LocalTableMetrics, trigger_write_stop_stats};
52use crate::hummock::model::CompactionGroup;
53use crate::hummock::model::ext::to_table_change_log_meta_store_model;
54use crate::model::VarTransaction;
55
56#[derive(Default)]
57pub struct Versioning {
58    // Volatile states below
59    /// Avoid commit epoch epochs
60    /// Don't persist compaction version delta to meta store
61    pub disable_commit_epochs: bool,
62    /// Latest hummock version
63    pub current_version: Arc<HummockVersion>,
64    pub local_metrics: HashMap<TableId, LocalTableMetrics>,
65    pub time_travel_snapshot_interval_counter: u64,
66    /// Used to avoid the attempts to rewrite the same SST to meta store
67    pub last_time_travel_snapshot_sst_ids: HashSet<HummockSstableId>,
68
69    // Persistent states below
70    pub hummock_version_deltas: BTreeMap<HummockVersionId, HummockVersionDelta>,
71    /// Stats for latest hummock version.
72    pub version_stats: HummockVersionStats,
73    pub checkpoint: HummockVersionCheckpoint,
74    pub table_change_log: HashMap<TableId, TableChangeLog>,
75}
76
77impl ContextInfo {
78    pub fn min_pinned_version_id(&self) -> HummockVersionId {
79        let mut min_pinned_version_id = MAX_HUMMOCK_VERSION_ID;
80        for id in self
81            .pinned_versions
82            .values()
83            .map(|v| v.min_pinned_id)
84            .chain(self.version_safe_points.iter().cloned())
85        {
86            min_pinned_version_id = cmp::min(id, min_pinned_version_id);
87        }
88        min_pinned_version_id
89    }
90}
91
92impl Versioning {
93    pub(super) fn mark_next_time_travel_version_snapshot(&mut self) {
94        self.time_travel_snapshot_interval_counter = u64::MAX;
95    }
96
97    pub fn get_tracked_object_ids(
98        &self,
99        min_pinned_version_id: HummockVersionId,
100    ) -> HashSet<HummockObjectId> {
101        // object ids in checkpoint version
102        let mut tracked_object_ids = self
103            .checkpoint
104            .version
105            .get_object_ids()
106            .chain(
107                self.table_change_log
108                    .values()
109                    .flat_map(|c| c.get_object_ids()),
110            )
111            .collect::<HashSet<_>>();
112        // add object ids added between checkpoint version and current version
113        for (_, delta) in self.hummock_version_deltas.range((
114            Excluded(self.checkpoint.version.id),
115            Included(self.current_version.id),
116        )) {
117            tracked_object_ids.extend(delta.newly_added_object_ids(false));
118        }
119        // add stale object ids before the checkpoint version
120        tracked_object_ids.extend(
121            self.checkpoint
122                .stale_objects
123                .iter()
124                .filter(|(version_id, _)| **version_id >= min_pinned_version_id)
125                .flat_map(|(_, objects)| get_stale_object_ids(objects)),
126        );
127        tracked_object_ids
128    }
129}
130
131impl HummockManager {
132    pub async fn list_pinned_version(&self) -> Vec<HummockPinnedVersion> {
133        self.context_info
134            .read()
135            .await
136            .pinned_versions
137            .values()
138            .cloned()
139            .collect_vec()
140    }
141
142    pub async fn list_workers(
143        &self,
144        context_ids: &[HummockContextId],
145    ) -> MetaResult<HashMap<HummockContextId, WorkerNode>> {
146        let mut workers = HashMap::new();
147        for context_id in context_ids {
148            if let Some(worker_node) = self
149                .metadata_manager()
150                .get_worker_by_id(*context_id as _)
151                .await?
152            {
153                workers.insert(*context_id, worker_node);
154            }
155        }
156        Ok(workers)
157    }
158
159    /// Gets current version without pinning it.
160    /// Should not be called inside [`HummockManager`], because it requests locks internally.
161    ///
162    /// Note: this method can hurt performance because it will clone a large object.
163    #[cfg(any(test, feature = "test"))]
164    pub async fn get_current_version(&self) -> HummockVersion {
165        self.on_current_version(|version| version.clone()).await
166    }
167
168    pub async fn on_current_version<T>(&self, mut f: impl FnMut(&HummockVersion) -> T) -> T {
169        f(self.versioning.read().await.current_version.as_ref())
170    }
171
172    pub async fn on_current_version_and_table_change_log<T>(
173        &self,
174        mut f: impl FnMut(&HummockVersion, &TableChangeLogs) -> T,
175    ) -> T {
176        let guard = self.versioning.read().await;
177        f(&guard.current_version, &guard.table_change_log)
178    }
179
180    pub async fn get_version_id(&self) -> HummockVersionId {
181        self.on_current_version(|version| version.id).await
182    }
183
184    /// Gets the mapping from table id to compaction group id
185    pub async fn get_table_compaction_group_id_mapping(
186        &self,
187    ) -> HashMap<StateTableId, CompactionGroupId> {
188        get_table_compaction_group_id_mapping(&self.versioning.read().await.current_version)
189    }
190
191    /// Get version deltas from meta store
192    pub async fn list_version_deltas(
193        &self,
194        start_id: HummockVersionId,
195        num_limit: u32,
196    ) -> Result<Vec<HummockVersionDelta>> {
197        let versioning = self.versioning.read().await;
198        let version_deltas = versioning
199            .hummock_version_deltas
200            .range(start_id..)
201            .map(|(_id, delta)| delta)
202            .take(num_limit as _)
203            .cloned()
204            .collect();
205        Ok(version_deltas)
206    }
207
208    pub async fn get_version_stats(&self) -> HummockVersionStats {
209        self.versioning.read().await.version_stats.clone()
210    }
211
212    /// Updates write limits for `target_groups` and sends notification.
213    /// Returns true if `write_limit` has been modified.
214    /// The implementation acquires `versioning` lock and `compaction_group_manager` lock.
215    pub(super) async fn try_update_write_limits(
216        &self,
217        target_group_ids: &[CompactionGroupId],
218    ) -> bool {
219        let versioning = self.versioning.read().await;
220        let mut cg_manager = self.compaction_group_manager.write().await;
221        let target_group_configs = target_group_ids
222            .iter()
223            .filter_map(|id| {
224                cg_manager
225                    .try_get_compaction_group_config(*id)
226                    .map(|config| (*id, config))
227            })
228            .collect();
229        let mut new_write_limits = calc_new_write_limits(
230            target_group_configs,
231            cg_manager.write_limit.clone(),
232            &versioning.current_version,
233        );
234        let all_group_ids: HashSet<_> =
235            HashSet::from_iter(get_compaction_group_ids(&versioning.current_version));
236        new_write_limits.retain(|group_id, _| all_group_ids.contains(group_id));
237        if new_write_limits == cg_manager.write_limit {
238            return false;
239        }
240        tracing::debug!("Hummock stopped write is updated: {:#?}", new_write_limits);
241        trigger_write_stop_stats(&self.metrics, &new_write_limits);
242        cg_manager.write_limit = new_write_limits;
243        self.env
244            .notification_manager()
245            .notify_hummock_without_version(
246                Operation::Add,
247                Info::HummockWriteLimits(risingwave_pb::hummock::WriteLimits {
248                    write_limits: cg_manager.write_limit.clone(),
249                }),
250            );
251        true
252    }
253
254    /// Gets write limits.
255    /// The implementation acquires `versioning` lock.
256    pub async fn write_limits(&self) -> HashMap<CompactionGroupId, WriteLimit> {
257        let guard = self.compaction_group_manager.read().await;
258        guard.write_limit.clone()
259    }
260
261    pub async fn list_branched_objects(&self) -> BTreeMap<HummockSstableObjectId, BranchedSstInfo> {
262        let guard = self.versioning.read().await;
263        guard.current_version.build_branched_sst_info()
264    }
265
266    pub async fn rebuild_table_stats(&self) -> Result<()> {
267        let mut versioning = self.versioning.write().await;
268        let new_stats = rebuild_table_stats(&versioning.current_version);
269        let mut version_stats = VarTransaction::new(&mut versioning.version_stats);
270        // version_stats.hummock_version_id is always 0 in meta store.
271        version_stats.table_stats = new_stats.table_stats;
272        commit_multi_var!(self.meta_store_ref(), version_stats)?;
273        Ok(())
274    }
275
276    pub async fn may_fill_backward_state_table_info(&self) -> Result<()> {
277        let mut versioning = self.versioning.write().await;
278        if versioning
279            .current_version
280            .need_fill_backward_compatible_state_table_info_delta()
281        {
282            let versioning: &mut Versioning = &mut versioning;
283            let mut version = HummockVersionTransaction::new(
284                &mut versioning.current_version,
285                &mut versioning.hummock_version_deltas,
286                &mut versioning.table_change_log,
287                self.env.notification_manager(),
288                None,
289                &self.metrics,
290                &self.env.opts,
291                &self.version_stat_tx,
292            );
293            let mut new_version_delta = version.new_delta();
294            new_version_delta.with_latest_version(|version, delta| {
295                version.may_fill_backward_compatible_state_table_info_delta(delta)
296            });
297            new_version_delta.pre_apply();
298            commit_multi_var!(self.meta_store_ref(), version)?;
299        }
300        Ok(())
301    }
302
303    pub async fn may_fill_backward_table_change_logs(&self) -> Result<()> {
304        let is_nonempty_meta_store =
305            risingwave_meta_model::hummock_table_change_log::Entity::find()
306                .select_only()
307                .columns([
308                    hummock_table_change_log::Column::TableId,
309                    hummock_table_change_log::Column::CheckpointEpoch,
310                ])
311                .into_tuple::<(TableId, Epoch)>()
312                .one(&self.env.meta_store_ref().conn)
313                .await?
314                .is_some();
315
316        let table_change_logs = {
317            let mut versioning = self.versioning.write().await;
318            #[expect(deprecated)]
319            if versioning.current_version.table_change_log.is_empty() {
320                tracing::info!("No legacy table change log to migrate.");
321                return Ok(());
322            }
323            let version = Arc::make_mut(&mut versioning.current_version);
324            if is_nonempty_meta_store {
325                tracing::info!("meta store table change log is non-empty.");
326                // Clear legacy in-mem state.
327                #[expect(deprecated)]
328                version.table_change_log = HashMap::default();
329                // Either there are no table change logs to commit to the metastore, or the operation has already been completed.
330                return Ok(());
331            }
332
333            // Clear legacy in-mem state.
334            #[expect(deprecated)]
335            let logs = std::mem::take(&mut version.table_change_log);
336            if logs.values().all(|t| t.is_empty()) {
337                return Ok(());
338            }
339            logs
340        };
341
342        // Store table change log in meta store.
343        let insert_batch_size = self.env.opts.table_change_log_insert_batch_size as usize;
344        let count = {
345            let iter = table_change_logs
346                .iter()
347                .flat_map(|(table_id, change_logs)| {
348                    change_logs
349                        .iter()
350                        .map(move |change_log| (table_id, change_log))
351                });
352
353            use futures::stream::{self, StreamExt};
354            let mut stream = stream::iter(iter).chunks(insert_batch_size);
355            let mut count = 0;
356            let txn = self.env.meta_store_ref().conn.begin().await?;
357            while let Some(change_log_batch) = stream.next().await {
358                if change_log_batch.is_empty() {
359                    break;
360                }
361                count += change_log_batch.len();
362                let insert_many = change_log_batch
363                    .into_iter()
364                    .map(|(table_id, change_log)| {
365                        to_table_change_log_meta_store_model(*table_id, change_log)
366                    })
367                    .collect::<Vec<_>>();
368                risingwave_meta_model::hummock_table_change_log::Entity::insert_many(insert_many)
369                    .exec(&txn)
370                    .await?;
371            }
372            txn.commit().await?;
373            count
374        };
375        tracing::info!("Migrated {count} table change log to meta store.");
376        // Initialize new in-mem state.
377        let mut versioning = self.versioning.write().await;
378        versioning.table_change_log = table_change_logs;
379        Ok(())
380    }
381
382    pub async fn get_table_change_logs(
383        &self,
384        epoch_only: bool,
385        start_epoch_inclusive: Option<u64>,
386        end_epoch_inclusive: Option<u64>,
387        table_ids: Option<HashSet<TableId>>,
388        exclude_empty: bool,
389        limit: Option<u32>,
390    ) -> TableChangeLogs {
391        let _timer = self.metrics.table_change_log_get_latency.start_timer();
392        self.on_current_version_and_table_change_log(|_, table_change_logs| {
393            table_change_logs
394                .iter()
395                .filter_map(|(id, change_log)| {
396                    if let Some(table_filter) = &table_ids
397                        && !table_filter.contains(id)
398                    {
399                        return None;
400                    }
401                    let filtered_change_logs = change_log
402                        .filter_epoch((
403                            start_epoch_inclusive.unwrap_or(0),
404                            end_epoch_inclusive.unwrap_or(u64::MAX),
405                        ))
406                        .filter(|change_log| {
407                            if exclude_empty
408                                && change_log.new_value.is_empty()
409                                && change_log.old_value.is_empty()
410                            {
411                                return false;
412                            }
413                            true
414                        })
415                        .take(limit.map(|l| l as usize).unwrap_or(usize::MAX))
416                        .map(|change_log| {
417                            if epoch_only {
418                                EpochNewChangeLog {
419                                    new_value: vec![],
420                                    old_value: vec![],
421                                    non_checkpoint_epochs: change_log.non_checkpoint_epochs.clone(),
422                                    checkpoint_epoch: change_log.checkpoint_epoch,
423                                }
424                            } else {
425                                change_log.clone()
426                            }
427                        });
428                    Some((id.to_owned(), TableChangeLog::new(filtered_change_logs)))
429                })
430                .collect()
431        })
432        .await
433    }
434}
435
436/// Calculates write limits for `target_groups`.
437/// Returns a new complete write limits snapshot based on `origin_snapshot` and `version`.
438pub(super) fn calc_new_write_limits(
439    target_groups: HashMap<CompactionGroupId, CompactionGroup>,
440    origin_snapshot: HashMap<CompactionGroupId, WriteLimit>,
441    version: &HummockVersion,
442) -> HashMap<CompactionGroupId, WriteLimit> {
443    let mut new_write_limits = origin_snapshot;
444    for (id, config) in &target_groups {
445        let levels = match version.levels.get(id) {
446            None => {
447                new_write_limits.remove(id);
448                continue;
449            }
450            Some(levels) => levels,
451        };
452
453        let group_state = GroupStateValidator::check_single_group_write_stop(
454            levels,
455            config.compaction_config.as_ref(),
456        );
457
458        if group_state.is_write_stop() {
459            new_write_limits.insert(
460                *id,
461                WriteLimit {
462                    table_ids: version
463                        .state_table_info
464                        .compaction_group_member_table_ids(*id)
465                        .iter()
466                        .copied()
467                        .collect(),
468                    reason: group_state.reason().unwrap().to_owned(),
469                },
470            );
471            continue;
472        }
473        // No condition is met.
474        new_write_limits.remove(id);
475    }
476    new_write_limits
477}
478
479/// Rebuilds table stats from the given version.
480/// Note that the result is approximate value. See `estimate_table_stats`.
481fn rebuild_table_stats(version: &HummockVersion) -> HummockVersionStats {
482    let mut stats = HummockVersionStats {
483        hummock_version_id: version.id,
484        table_stats: Default::default(),
485    };
486    for level in version.get_combined_levels() {
487        for sst in &level.table_infos {
488            let changes = estimate_table_stats(sst);
489            add_prost_table_stats_map(&mut stats.table_stats, &changes);
490        }
491    }
492    stats
493}
494
495/// Estimates table stats change from the given file.
496/// - The file stats is evenly distributed among multiple tables within the file.
497/// - The total key size and total value size are estimated based on key range and file size.
498/// - Branched files may lead to an overestimation.
499fn estimate_table_stats(sst: &SstableInfo) -> PbTableStatsMap {
500    let mut changes: PbTableStatsMap = HashMap::default();
501    let weighted_value =
502        |value: i64| -> i64 { (value as f64 / sst.table_ids.len() as f64).ceil() as i64 };
503    let key_range = &sst.key_range;
504    let estimated_key_size: u64 = (key_range.left.len() + key_range.right.len()) as u64 / 2;
505    let mut estimated_total_key_size = estimated_key_size * sst.total_key_count;
506    if estimated_total_key_size > sst.uncompressed_file_size {
507        estimated_total_key_size = sst.uncompressed_file_size / 2;
508        tracing::warn!(
509            %sst.sst_id,
510            "Calculated estimated_total_key_size {} > uncompressed_file_size {}. Use uncompressed_file_size/2 as estimated_total_key_size instead.",
511            estimated_total_key_size,
512            sst.uncompressed_file_size
513        );
514    }
515    let estimated_total_value_size = sst.uncompressed_file_size - estimated_total_key_size;
516    for table_id in &sst.table_ids {
517        let e = changes.entry(*table_id).or_default();
518        e.total_key_count += weighted_value(sst.total_key_count as i64);
519        e.total_key_size += weighted_value(estimated_total_key_size as i64);
520        e.total_value_size += weighted_value(estimated_total_value_size as i64);
521    }
522    changes
523}
524
525#[cfg(test)]
526mod tests {
527    use std::collections::HashMap;
528    use std::sync::Arc;
529
530    use itertools::Itertools;
531    use risingwave_hummock_sdk::key_range::KeyRange;
532    use risingwave_hummock_sdk::level::{Level, Levels};
533    use risingwave_hummock_sdk::sstable_info::SstableInfoInner;
534    use risingwave_hummock_sdk::version::{HummockVersion, MAX_HUMMOCK_VERSION_ID};
535    use risingwave_hummock_sdk::{CompactionGroupId, HummockVersionId};
536    use risingwave_pb::hummock::write_limits::WriteLimit;
537    use risingwave_pb::hummock::{HummockPinnedVersion, HummockVersionStats};
538
539    use crate::hummock::compaction::compaction_config::CompactionConfigBuilder;
540    use crate::hummock::manager::context::ContextInfo;
541    use crate::hummock::manager::versioning::{
542        calc_new_write_limits, estimate_table_stats, rebuild_table_stats,
543    };
544    use crate::hummock::model::CompactionGroup;
545
546    #[test]
547    fn test_min_pinned_version_id() {
548        let mut context_info = ContextInfo::default();
549        assert_eq!(context_info.min_pinned_version_id(), MAX_HUMMOCK_VERSION_ID);
550        context_info.pinned_versions.insert(
551            1.into(),
552            HummockPinnedVersion {
553                context_id: 1.into(),
554                min_pinned_id: 10.into(),
555            },
556        );
557        assert_eq!(context_info.min_pinned_version_id(), 10);
558        context_info
559            .version_safe_points
560            .push(HummockVersionId::new(5));
561        assert_eq!(context_info.min_pinned_version_id(), 5);
562        context_info.version_safe_points.clear();
563        assert_eq!(context_info.min_pinned_version_id(), 10);
564        context_info.pinned_versions.clear();
565        assert_eq!(context_info.min_pinned_version_id(), MAX_HUMMOCK_VERSION_ID);
566    }
567
568    #[test]
569    fn test_calc_new_write_limits() {
570        let add_level_to_l0 = |levels: &mut Levels| {
571            levels.l0.sub_levels.push(Level::default());
572        };
573        let set_sub_level_number_threshold_for_group_1 =
574            |target_groups: &mut HashMap<CompactionGroupId, CompactionGroup>,
575             sub_level_number_threshold: u64| {
576                target_groups.insert(
577                    1.into(),
578                    CompactionGroup {
579                        group_id: 1.into(),
580                        compaction_config: Arc::new(
581                            CompactionConfigBuilder::new()
582                                .level0_stop_write_threshold_sub_level_number(
583                                    sub_level_number_threshold,
584                                )
585                                .build(),
586                        ),
587                    },
588                );
589            };
590
591        let set_level_0_max_sst_count_threshold_for_group_1 =
592            |target_groups: &mut HashMap<CompactionGroupId, CompactionGroup>,
593             max_sst_count_threshold: u32| {
594                target_groups.insert(
595                    1.into(),
596                    CompactionGroup {
597                        group_id: 1.into(),
598                        compaction_config: Arc::new(
599                            CompactionConfigBuilder::new()
600                                .level0_stop_write_threshold_max_sst_count(Some(
601                                    max_sst_count_threshold,
602                                ))
603                                .build(),
604                        ),
605                    },
606                );
607            };
608
609        let set_level_0_max_size_threshold_for_group_1 =
610            |target_groups: &mut HashMap<CompactionGroupId, CompactionGroup>,
611             max_size_threshold: u64| {
612                target_groups.insert(
613                    1.into(),
614                    CompactionGroup {
615                        group_id: 1.into(),
616                        compaction_config: Arc::new(
617                            CompactionConfigBuilder::new()
618                                .level0_stop_write_threshold_max_size(Some(max_size_threshold))
619                                .build(),
620                        ),
621                    },
622                );
623            };
624
625        let mut target_groups: HashMap<CompactionGroupId, CompactionGroup> = Default::default();
626        set_sub_level_number_threshold_for_group_1(&mut target_groups, 10);
627        let origin_snapshot: HashMap<CompactionGroupId, WriteLimit> = [(
628            2.into(),
629            WriteLimit {
630                table_ids: [1, 2, 3].into_iter().map_into().collect(),
631                reason: "for test".to_owned(),
632            },
633        )]
634        .into_iter()
635        .collect();
636        let mut version: HummockVersion = Default::default();
637        for group_id in 1..=3 {
638            version.levels.insert(group_id.into(), Levels::default());
639        }
640        let new_write_limits =
641            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
642        assert_eq!(
643            new_write_limits, origin_snapshot,
644            "write limit should not be triggered for group 1"
645        );
646        assert_eq!(new_write_limits.len(), 1);
647        for _ in 1..=10 {
648            add_level_to_l0(version.levels.get_mut(&1).unwrap());
649            let new_write_limits =
650                calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
651            assert_eq!(
652                new_write_limits, origin_snapshot,
653                "write limit should not be triggered for group 1"
654            );
655        }
656        add_level_to_l0(version.levels.get_mut(&1).unwrap());
657        let new_write_limits =
658            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
659        assert_ne!(
660            new_write_limits, origin_snapshot,
661            "write limit should be triggered for group 1"
662        );
663        assert_eq!(
664            new_write_limits.get(&1).as_ref().unwrap().reason,
665            "WriteStop(l0_level_count: 11, threshold: 10) too many L0 sub levels"
666        );
667        assert_eq!(new_write_limits.len(), 2);
668
669        set_sub_level_number_threshold_for_group_1(&mut target_groups, 100);
670        let new_write_limits =
671            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
672        assert_eq!(
673            new_write_limits, origin_snapshot,
674            "write limit should not be triggered for group 1"
675        );
676
677        set_sub_level_number_threshold_for_group_1(&mut target_groups, 5);
678        let new_write_limits =
679            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
680        assert_ne!(
681            new_write_limits, origin_snapshot,
682            "write limit should be triggered for group 1"
683        );
684        assert_eq!(
685            new_write_limits.get(&1).as_ref().unwrap().reason,
686            "WriteStop(l0_level_count: 11, threshold: 5) too many L0 sub levels"
687        );
688
689        set_sub_level_number_threshold_for_group_1(&mut target_groups, 100);
690        let last_level = version
691            .levels
692            .get_mut(&1)
693            .unwrap()
694            .l0
695            .sub_levels
696            .last_mut()
697            .unwrap();
698        last_level.table_infos.extend(vec![
699            SstableInfoInner {
700                key_range: KeyRange::default(),
701                table_ids: vec![1.into(), 2.into(), 3.into()],
702                total_key_count: 100,
703                sst_size: 100,
704                uncompressed_file_size: 100,
705                ..Default::default()
706            }
707            .into(),
708            SstableInfoInner {
709                key_range: KeyRange::default(),
710                table_ids: vec![1.into(), 2.into(), 3.into()],
711                total_key_count: 100,
712                sst_size: 100,
713                uncompressed_file_size: 100,
714                ..Default::default()
715            }
716            .into(),
717        ]);
718        version.levels.get_mut(&1).unwrap().l0.total_file_size += 200;
719        let new_write_limits =
720            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
721        assert_eq!(
722            new_write_limits, origin_snapshot,
723            "write limit should not be triggered for group 1"
724        );
725
726        set_level_0_max_size_threshold_for_group_1(&mut target_groups, 10);
727        let new_write_limits =
728            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
729        assert_ne!(
730            new_write_limits, origin_snapshot,
731            "write limit should be triggered for group 1"
732        );
733        assert_eq!(
734            new_write_limits.get(&1).as_ref().unwrap().reason,
735            "WriteStop(l0_size: 200, threshold: 10) too large L0 size"
736        );
737
738        set_level_0_max_size_threshold_for_group_1(&mut target_groups, 10000);
739        let new_write_limits =
740            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
741        assert_eq!(
742            new_write_limits, origin_snapshot,
743            "write limit should not be triggered for group 1"
744        );
745
746        set_level_0_max_sst_count_threshold_for_group_1(&mut target_groups, 1);
747        let new_write_limits =
748            calc_new_write_limits(target_groups.clone(), origin_snapshot.clone(), &version);
749        assert_ne!(
750            new_write_limits, origin_snapshot,
751            "write limit should be triggered for group 1"
752        );
753        assert_eq!(
754            new_write_limits.get(&1).as_ref().unwrap().reason,
755            "WriteStop(l0_sst_count: 2, threshold: 1) too many L0 sst files"
756        );
757
758        set_level_0_max_sst_count_threshold_for_group_1(&mut target_groups, 100);
759        let new_write_limits =
760            calc_new_write_limits(target_groups, origin_snapshot.clone(), &version);
761
762        assert_eq!(
763            new_write_limits, origin_snapshot,
764            "write limit should not be triggered for group 1"
765        );
766    }
767
768    #[test]
769    fn test_estimate_table_stats() {
770        let sst = SstableInfoInner {
771            key_range: KeyRange {
772                left: vec![1; 10].into(),
773                right: vec![1; 20].into(),
774                ..Default::default()
775            },
776            table_ids: vec![1.into(), 2.into(), 3.into()],
777            total_key_count: 6000,
778            uncompressed_file_size: 6_000_000,
779            ..Default::default()
780        }
781        .into();
782        let changes = estimate_table_stats(&sst);
783        assert_eq!(changes.len(), 3);
784        for stats in changes.values() {
785            assert_eq!(stats.total_key_count, 6000 / 3);
786            assert_eq!(stats.total_key_size, (10 + 20) / 2 * 6000 / 3);
787            assert_eq!(
788                stats.total_value_size,
789                (6_000_000 - (10 + 20) / 2 * 6000) / 3
790            );
791        }
792
793        let mut version = HummockVersion::default();
794        version.id = HummockVersionId::new(123);
795
796        for cg in 1..3 {
797            version.levels.insert(
798                cg.into(),
799                Levels {
800                    levels: vec![Level {
801                        table_infos: vec![sst.clone()],
802                        ..Default::default()
803                    }],
804                    ..Default::default()
805                },
806            );
807        }
808        let HummockVersionStats {
809            hummock_version_id,
810            table_stats,
811        } = rebuild_table_stats(&version);
812        assert_eq!(hummock_version_id, version.id);
813        assert_eq!(table_stats.len(), 3);
814        for (tid, stats) in table_stats {
815            assert_eq!(
816                stats.total_key_count,
817                changes.get(&tid).unwrap().total_key_count * 2
818            );
819            assert_eq!(
820                stats.total_key_size,
821                changes.get(&tid).unwrap().total_key_size * 2
822            );
823            assert_eq!(
824                stats.total_value_size,
825                changes.get(&tid).unwrap().total_value_size * 2
826            );
827        }
828    }
829
830    #[test]
831    fn test_estimate_table_stats_large_key_range() {
832        let sst = SstableInfoInner {
833            key_range: KeyRange {
834                left: vec![1; 1000].into(),
835                right: vec![1; 2000].into(),
836                ..Default::default()
837            },
838            table_ids: vec![1.into(), 2.into(), 3.into()],
839            total_key_count: 6000,
840            uncompressed_file_size: 60_000,
841            ..Default::default()
842        }
843        .into();
844        let changes = estimate_table_stats(&sst);
845        assert_eq!(changes.len(), 3);
846        for t in &sst.table_ids {
847            let stats = changes.get(t).unwrap();
848            assert_eq!(stats.total_key_count, 6000 / 3);
849            assert_eq!(stats.total_key_size, 60_000 / 2 / 3);
850            assert_eq!(stats.total_value_size, (60_000 - 60_000 / 2) / 3);
851        }
852    }
853}