risingwave_meta/hummock/manager/compaction/compaction_group_schedule/
merge_policy.rs1use std::collections::HashSet;
18
19use risingwave_common::catalog::TableId;
20use risingwave_hummock_sdk::compaction_group::StaticCompactionGroupId;
21use risingwave_hummock_sdk::version::HummockVersion;
22
23use super::CompactionGroupStatistic;
24use crate::hummock::error::{Error, Result};
25use crate::hummock::manager::compaction::GroupStateValidator;
26use crate::hummock::model::CompactionGroup;
27use crate::hummock::table_write_throughput_statistic::TableWriteThroughputStatisticManager;
28use crate::manager::MetaOpts;
29
30pub(super) fn check_is_low_write_throughput(
31 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
32 mut table_ids: impl Iterator<Item = TableId>,
33 opts: &MetaOpts,
34) -> bool {
35 let now = tokio::time::Instant::now();
36 let threshold = opts
37 .table_low_write_throughput_threshold
38 .min(opts.table_high_write_throughput_threshold);
39 table_ids.all(|table_id| {
40 table_write_throughput_statistic_manager
43 .max_write_throughput(table_id, now)
44 .is_some_and(|peak| peak <= threshold)
45 })
46}
47
48pub(super) fn validate_group_merge(
49 group: &CompactionGroupStatistic,
50 next_group: &CompactionGroupStatistic,
51 created_tables: &HashSet<TableId>,
52 table_write_throughput_statistic_manager: &TableWriteThroughputStatisticManager,
53 opts: &MetaOpts,
54) -> Result<()> {
55 if group.table_statistic.is_empty() || next_group.table_statistic.is_empty() {
56 return Err(Error::CompactionGroup(format!(
57 "group-{} or group-{} is empty",
58 group.group_id, next_group.group_id
59 )));
60 }
61
62 let mut left = &group.table_statistic;
64 let mut right = &next_group.table_statistic;
65 if left.first_key_value().unwrap().0 > right.first_key_value().unwrap().0 {
66 std::mem::swap(&mut left, &mut right);
67 }
68 if left.last_key_value().unwrap().0 >= right.first_key_value().unwrap().0 {
69 return Err(Error::CompactionGroup(format!(
70 "group-{} and group-{} have overlapping table id ranges, not mergeable",
71 group.group_id, next_group.group_id
72 )));
73 }
74
75 validate_group_config(
76 &group.compaction_group_config,
77 &next_group.compaction_group_config,
78 group.group_size.saturating_add(next_group.group_size),
79 opts,
80 )?;
81
82 for candidate in [group, next_group] {
83 if candidate
84 .table_statistic
85 .keys()
86 .any(|table_id| !created_tables.contains(table_id))
87 {
88 return Err(Error::CompactionGroup(format!(
89 "Cannot merge groups {} and {}: group {} contains creating tables",
90 group.group_id, next_group.group_id, candidate.group_id
91 )));
92 }
93 if !check_is_low_write_throughput(
94 table_write_throughput_statistic_manager,
95 candidate.table_statistic.keys().copied(),
96 opts,
97 ) {
98 return Err(Error::CompactionGroup(format!(
99 "Cannot merge groups {} and {}: group {} without sufficiently observed low throughput",
100 group.group_id, next_group.group_id, candidate.group_id
101 )));
102 }
103 }
104
105 Ok(())
106}
107
108pub(super) fn validate_group_config(
109 group: &CompactionGroup,
110 next_group: &CompactionGroup,
111 combined_size: u64,
112 opts: &MetaOpts,
113) -> Result<()> {
114 if (group.group_id == StaticCompactionGroupId::StateDefault
116 && next_group.group_id == StaticCompactionGroupId::MaterializedView)
117 || (group.group_id == StaticCompactionGroupId::MaterializedView
118 && next_group.group_id == StaticCompactionGroupId::StateDefault)
119 {
120 return Err(Error::CompactionGroup(format!(
121 "group-{} and group-{} are both StaticCompactionGroupId",
122 group.group_id, next_group.group_id
123 )));
124 }
125
126 if group
127 .compaction_config
128 .disable_auto_group_scheduling
129 .unwrap_or(false)
130 || next_group
131 .compaction_config
132 .disable_auto_group_scheduling
133 .unwrap_or(false)
134 {
135 return Err(Error::CompactionGroup(format!(
136 "group-{} or group-{} disable_auto_group_scheduling",
137 group.group_id, next_group.group_id
138 )));
139 }
140
141 let mut left = group.compaction_config.as_ref().clone();
143 let mut right = next_group.compaction_config.as_ref().clone();
144 left.split_weight_by_vnode = 0;
145 right.split_weight_by_vnode = 0;
146 if left != right {
147 let left_config = group.compaction_config.as_ref();
148 let right_config = next_group.compaction_config.as_ref();
149
150 tracing::warn!(
151 group_id = %group.group_id,
152 next_group_id = %next_group.group_id,
153 left_config = ?left_config,
154 right_config = ?right_config,
155 "compaction config semantic mismatch detected while merging compaction groups"
156 );
157
158 return Err(Error::CompactionGroup(format!(
159 "Cannot merge group {} and next_group {} with different compaction config (split_weight_by_vnode is excluded from comparison). left_config: {:?}, right_config: {:?}",
160 group.group_id, next_group.group_id, left_config, right_config
161 )));
162 }
163
164 let size_limit = (group.max_estimated_group_size() as f64 * opts.split_group_size_ratio) as u64;
165 if combined_size > size_limit {
166 return Err(Error::CompactionGroup(format!(
167 "Cannot merge huge groups {} and {}: combined_size {} size_limit {}",
168 group.group_id, next_group.group_id, combined_size, size_limit
169 )));
170 }
171 Ok(())
172}
173
174pub(super) fn validate_group_levels(
175 group: &CompactionGroup,
176 next_group: &CompactionGroup,
177 opts: &MetaOpts,
178 version: &HummockVersion,
179) -> Result<()> {
180 let group_levels = version.levels.get(&group.group_id).ok_or_else(|| {
181 Error::CompactionGroup(format!(
182 "cannot merge compaction group {} because it does not exist",
183 group.group_id
184 ))
185 })?;
186 let next_group_levels = version.levels.get(&next_group.group_id).ok_or_else(|| {
187 Error::CompactionGroup(format!(
188 "cannot merge compaction group {} because it does not exist",
189 next_group.group_id
190 ))
191 })?;
192 for (candidate, levels) in [(group, group_levels), (next_group, next_group_levels)] {
193 let state = GroupStateValidator::group_state(levels, &candidate.compaction_config);
194 if state.is_write_stop() || state.is_emergency() {
195 return Err(Error::CompactionGroup(format!(
196 "Cannot merge groups {} and {}: write limit on group {}",
197 group.group_id, next_group.group_id, candidate.group_id
198 )));
199 }
200 }
201
202 let l0_sub_level_count_after_merge =
204 group_levels.l0.sub_levels.len() + next_group_levels.l0.sub_levels.len();
205 if GroupStateValidator::write_stop_sub_level_count(
206 (l0_sub_level_count_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
207 as usize,
208 &group.compaction_config,
209 ) {
210 return Err(Error::CompactionGroup(format!(
211 "Cannot merge write limit group {} next group {}, will trigger write stop after merge",
212 group.group_id, next_group.group_id
213 )));
214 }
215
216 let l0_file_count_after_merge = group_levels
217 .l0
218 .sub_levels
219 .iter()
220 .chain(next_group_levels.l0.sub_levels.iter())
221 .map(|level| level.table_infos.len())
222 .sum::<usize>();
223 if GroupStateValidator::write_stop_l0_file_count(
224 (l0_file_count_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
225 as usize,
226 &group.compaction_config,
227 ) {
228 return Err(Error::CompactionGroup(format!(
229 "Cannot merge write limit next group {} group {}, will trigger write stop after merge",
230 next_group.group_id, group.group_id
231 )));
232 }
233
234 let l0_size_after_merge =
235 group_levels.l0.total_file_size + next_group_levels.l0.total_file_size;
236
237 if GroupStateValidator::write_stop_l0_size(
238 (l0_size_after_merge as f64 * opts.compaction_group_merge_dimension_threshold) as u64,
239 &group.compaction_config,
240 ) {
241 return Err(Error::CompactionGroup(format!(
242 "Cannot merge write limit next group {} group {}, will trigger write stop after merge",
243 next_group.group_id, group.group_id
244 )));
245 }
246
247 if GroupStateValidator::emergency_l0_file_count(
249 (l0_file_count_after_merge as f64 * opts.compaction_group_merge_dimension_threshold)
250 as usize,
251 &group.compaction_config,
252 ) {
253 return Err(Error::CompactionGroup(format!(
254 "Cannot merge emergency group {} next group {}, will trigger emergency after merge",
255 group.group_id, next_group.group_id
256 )));
257 }
258 Ok(())
259}