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