Coverage Report

Created: 2026-08-07 12:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/cloud/cloud_cumulative_compaction_policy.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "cloud/cloud_cumulative_compaction_policy.h"
19
20
#include <algorithm>
21
#include <iterator>
22
#include <list>
23
#include <ostream>
24
#include <string>
25
26
#include "cloud/config.h"
27
#include "common/config.h"
28
#include "common/logging.h"
29
#include "cpp/sync_point.h"
30
#include "storage/compaction/cumulative_compaction_time_series_policy.h"
31
#include "storage/olap_common.h"
32
#include "storage/tablet/tablet.h"
33
#include "storage/tablet/tablet_meta.h"
34
#include "util/defer_op.h"
35
36
namespace doris {
37
38
int64_t CloudCumulativeCompactionPolicy::calculate_cumulative_point(
39
        CloudTablet* tablet, const std::vector<RowsetSharedPtr>& rowsets,
40
        const RowsetSharedPtr& output_rowset, Version& last_delete_version,
41
17
        int64_t input_cumulative_point) {
42
17
    DORIS_CHECK(std::find(rowsets.begin(), rowsets.end(), output_rowset) != rowsets.end());
43
17
    int64_t cumulative_point = input_cumulative_point;
44
17
    Version no_delete_version {-1, -1};
45
30
    for (const auto& rowset : rowsets) {
46
30
        DORIS_CHECK_EQ(rowset->start_version(), cumulative_point);
47
30
        if (rowset->rowset_meta()->has_delete_predicate()) {
48
2
            cumulative_point = rowset->end_version() + 1;
49
2
            continue;
50
2
        }
51
28
        if (rowset->rowset_meta()->is_segments_overlapping()) {
52
3
            return cumulative_point;
53
3
        }
54
25
        Version& delete_version = rowset == output_rowset ? last_delete_version : no_delete_version;
55
25
        int64_t candidate_cumulative_point =
56
25
                new_cumulative_point(tablet, rowset, delete_version, cumulative_point);
57
25
        if (candidate_cumulative_point != rowset->end_version() + 1) {
58
9
            return cumulative_point;
59
9
        }
60
16
        cumulative_point = candidate_cumulative_point;
61
16
    }
62
5
    return cumulative_point;
63
17
}
64
65
CloudSizeBasedCumulativeCompactionPolicy::CloudSizeBasedCumulativeCompactionPolicy(
66
        int64_t promotion_size, double promotion_ratio, int64_t promotion_min_size,
67
        int64_t compaction_min_size)
68
265
        : _promotion_size(promotion_size),
69
265
          _promotion_ratio(promotion_ratio),
70
265
          _promotion_min_size(promotion_min_size),
71
265
          _compaction_min_size(compaction_min_size) {}
72
73
44
int64_t CloudSizeBasedCumulativeCompactionPolicy::_level_size(const int64_t size) {
74
44
    if (size < 1024) return 0;
75
29
    int64_t max_level = (int64_t)1
76
29
                        << (sizeof(_promotion_size) * 8 - 1 - __builtin_clzl(_promotion_size / 2));
77
29
    if (size >= max_level) return max_level;
78
23
    return (int64_t)1 << (sizeof(size) * 8 - 1 - __builtin_clzl(size));
79
29
}
80
81
void find_longest_consecutive_empty_rowsets(std::vector<RowsetSharedPtr>* result,
82
23
                                            const std::vector<RowsetSharedPtr>& candidate_rowsets) {
83
23
    std::vector<RowsetSharedPtr> current_sequence;
84
23
    std::vector<RowsetSharedPtr> longest_sequence;
85
86
331
    for (size_t i = 0; i < candidate_rowsets.size(); ++i) {
87
308
        auto& rowset = candidate_rowsets[i];
88
89
        // Check if rowset is empty and has no delete predicate
90
308
        if (rowset->num_segments() == 0 && !rowset->rowset_meta()->has_delete_predicate()) {
91
            // Check if this is consecutive with previous rowset
92
196
            if (current_sequence.empty() ||
93
196
                (current_sequence.back()->end_version() == rowset->start_version() - 1)) {
94
196
                current_sequence.push_back(rowset);
95
196
            } else {
96
                // Start new sequence if not consecutive
97
0
                if (current_sequence.size() > longest_sequence.size()) {
98
0
                    longest_sequence = current_sequence;
99
0
                }
100
0
                current_sequence.clear();
101
0
                current_sequence.push_back(rowset);
102
0
            }
103
196
        } else {
104
            // Non-empty rowset, check if we have a sequence to compare
105
112
            if (current_sequence.size() > longest_sequence.size()) {
106
11
                longest_sequence = current_sequence;
107
11
            }
108
112
            current_sequence.clear();
109
112
        }
110
308
    }
111
112
    // Check final sequence
113
23
    if (current_sequence.size() > longest_sequence.size()) {
114
3
        longest_sequence = current_sequence;
115
3
    }
116
117
23
    *result = longest_sequence;
118
23
}
119
120
int64_t CloudSizeBasedCumulativeCompactionPolicy::pick_input_rowsets(
121
        CloudTablet* tablet, const std::vector<RowsetSharedPtr>& candidate_rowsets,
122
        const int64_t max_compaction_score, const int64_t min_compaction_score,
123
        std::vector<RowsetSharedPtr>* input_rowsets, Version* last_delete_version,
124
21
        size_t* compaction_score, bool allow_delete) {
125
21
    DBUG_EXECUTE_IF(
126
21
            "CloudSizeBasedCumulativeCompactionPolicy::pick_input_rowsets.set_input_rowsets", {
127
21
                auto target_tablet_id = dp->param<int64_t>("tablet_id", -1);
128
21
                if (target_tablet_id == tablet->tablet_id()) {
129
21
                    auto start_version = dp->param<int64_t>("start_version", -1);
130
21
                    auto end_version = dp->param<int64_t>("end_version", -1);
131
21
                    for (auto& rowset : candidate_rowsets) {
132
21
                        if (rowset->start_version() >= start_version &&
133
21
                            rowset->end_version() <= end_version) {
134
21
                            input_rowsets->push_back(rowset);
135
21
                        }
136
21
                    }
137
21
                    LOG_INFO(
138
21
                            "[CloudSizeBasedCumulativeCompactionPolicy::pick_input_rowsets.set_"
139
21
                            "input_rowsets] tablet_id={}, start={}, end={}, "
140
21
                            "input_rowsets->size()={}",
141
21
                            target_tablet_id, start_version, end_version, input_rowsets->size());
142
21
                    return input_rowsets->size();
143
21
                }
144
21
            })
145
146
21
    size_t promotion_size = cloud_promotion_size(tablet);
147
21
    auto max_version = tablet->max_version().first;
148
21
    int transient_size = 0;
149
21
    *compaction_score = 0;
150
21
    int64_t total_size = 0;
151
21
    bool skip_trim = false; // Skip trim for Empty Rowset Compaction
152
21
    RowsetSharedPtr last_popped;
153
154
    // DEFER: trim input_rowsets from back if score > max_compaction_score
155
    // This ensures we don't return more rowsets than allowed by max_compaction_score,
156
    // while still collecting enough rowsets to pass min_compaction_score check after level_size removal.
157
    // Must be placed after variable initialization and before collection loop.
158
21
    DEFER({
159
21
        if (skip_trim) {
160
21
            return;
161
21
        }
162
        // Keep at least 1 rowset to avoid removing the only rowset (consistent with fallback branch)
163
21
        while (input_rowsets->size() > 1 &&
164
21
               *compaction_score > static_cast<size_t>(max_compaction_score)) {
165
21
            last_popped = std::move(input_rowsets->back());
166
21
            *compaction_score -= last_popped->rowset_meta()->get_compaction_score();
167
21
            total_size -= last_popped->rowset_meta()->total_disk_size();
168
21
            input_rowsets->pop_back();
169
21
        }
170
        // A single non-overlapping rowset cannot be compacted by itself. Restore the direct
171
        // successor and accept a one-off max-score overshoot to keep the input mergeable.
172
21
        if (input_rowsets->size() == 1 && last_popped != nullptr &&
173
21
            !input_rowsets->front()->rowset_meta()->is_segments_overlapping()) {
174
21
            *compaction_score += last_popped->rowset_meta()->get_compaction_score();
175
21
            total_size += last_popped->rowset_meta()->total_disk_size();
176
21
            input_rowsets->push_back(std::move(last_popped));
177
21
        }
178
21
    });
179
180
316
    for (auto& rowset : candidate_rowsets) {
181
        // check whether this rowset is delete version
182
316
        if (!allow_delete && rowset->rowset_meta()->has_delete_predicate()) {
183
0
            *last_delete_version = rowset->version();
184
0
            if (!input_rowsets->empty()) {
185
                // we meet a delete version, and there were other versions before.
186
                // we should compact those version before handling them over to base compaction
187
0
                break;
188
0
            } else {
189
                // we meet a delete version, and no other versions before, skip it and continue
190
0
                input_rowsets->clear();
191
0
                *compaction_score = 0;
192
0
                transient_size = 0;
193
0
                continue;
194
0
            }
195
0
        }
196
316
        if (tablet->tablet_state() == TABLET_NOTREADY) {
197
            // If tablet under alter, keep latest 10 version so that base tablet max version
198
            // not merged in new tablet, and then we can copy data from base tablet
199
19
            if (rowset->version().second > max_version - 10) {
200
10
                continue;
201
10
            }
202
19
        }
203
        // Removed: max_compaction_score check here
204
        // We now collect all candidate rowsets and trim from back at return time via DEFER
205
306
        *compaction_score += rowset->rowset_meta()->get_compaction_score();
206
306
        total_size += rowset->rowset_meta()->total_disk_size();
207
208
306
        transient_size += 1;
209
306
        input_rowsets->push_back(rowset);
210
306
    }
211
212
    // if there is delete version, do compaction directly
213
21
    if (last_delete_version->first != -1) {
214
0
        if (input_rowsets->size() == 1) {
215
0
            auto rs_meta = input_rowsets->front()->rowset_meta();
216
            // if there is only one rowset and not overlapping,
217
            // we do not need to do cumulative compaction
218
0
            if (!rs_meta->is_segments_overlapping()) {
219
0
                input_rowsets->clear();
220
0
                *compaction_score = 0;
221
0
            }
222
0
        }
223
0
        return transient_size;
224
0
    }
225
226
    // Check if empty rowset compaction strategy is enabled
227
21
    if (config::enable_empty_rowset_compaction && !input_rowsets->empty()) {
228
        // Check if input_rowsets contain consecutive empty rowsets that meet criteria
229
20
        std::vector<RowsetSharedPtr> consecutive_empty_rowsets;
230
20
        find_longest_consecutive_empty_rowsets(&consecutive_empty_rowsets, *input_rowsets);
231
232
20
        if (!consecutive_empty_rowsets.empty() &&
233
20
            consecutive_empty_rowsets.size() >= config::empty_rowset_compaction_min_count &&
234
20
            static_cast<double>(consecutive_empty_rowsets.size()) /
235
5
                            static_cast<double>(input_rowsets->size()) >=
236
5
                    config::empty_rowset_compaction_min_ratio) {
237
            // Prioritize consecutive empty rowset compaction
238
            // Skip trim: empty rowset compaction has very low cost and the goal is to reduce rowset count
239
4
            *input_rowsets = consecutive_empty_rowsets;
240
4
            *compaction_score = consecutive_empty_rowsets.size();
241
4
            skip_trim = true;
242
4
            return consecutive_empty_rowsets.size();
243
4
        }
244
20
    }
245
246
17
    auto rs_begin = input_rowsets->begin();
247
17
    size_t new_compaction_score = *compaction_score;
248
17
    const bool can_handle_exhausted_input =
249
17
            (config::prioritize_query_perf_in_compaction && tablet->keys_type() != DUP_KEYS) ||
250
17
            *compaction_score >= static_cast<size_t>(max_compaction_score);
251
23
    while (rs_begin != input_rowsets->end()) {
252
22
        auto& rs_meta = (*rs_begin)->rowset_meta();
253
22
        int64_t current_level = _level_size(rs_meta->total_disk_size());
254
22
        int64_t remain_level = _level_size(total_size - rs_meta->total_disk_size());
255
        // if current level less then remain level, input rowsets contain current rowset
256
        // and process return; otherwise, input rowsets do not contain current rowset.
257
22
        if (current_level <= remain_level) {
258
11
            break;
259
11
        }
260
261
11
        auto next = std::next(rs_begin);
262
        // Keep the last suffix rowset for the singleton checks unless the exhausted-input
263
        // fallback below can select a useful input.
264
11
        if (next == input_rowsets->end() && !can_handle_exhausted_input) {
265
5
            break;
266
5
        }
267
6
        total_size -= rs_meta->total_disk_size();
268
6
        new_compaction_score -= rs_meta->get_compaction_score();
269
6
        rs_begin = next;
270
6
    }
271
17
    if (rs_begin == input_rowsets->end()) { // No suitable level size found in `input_rowsets`
272
1
        if (config::prioritize_query_perf_in_compaction && tablet->keys_type() != DUP_KEYS) {
273
            // While tablet's key type is not `DUP_KEYS`, compacting rowset in such tablets has a significant
274
            // positive impact on queries and reduces space amplification, so we ignore level limitation and
275
            // pick candidate rowsets as input rowsets.
276
1
            return transient_size;
277
1
        } else if (*compaction_score >= max_compaction_score) {
278
            // Score of `input_rowsets` exceed max compaction score, which means `input_rowsets` will never change and
279
            // this tablet will never execute cumulative compaction. MUST execute compaction on these `input_rowsets`
280
            // to reduce compaction score.
281
0
            RowsetSharedPtr rs_with_max_score;
282
0
            uint32_t max_score = 1;
283
0
            for (auto& rs : *input_rowsets) {
284
0
                if (rs->rowset_meta()->get_compaction_score() > max_score) {
285
0
                    max_score = rs->rowset_meta()->get_compaction_score();
286
0
                    rs_with_max_score = rs;
287
0
                }
288
0
            }
289
0
            if (rs_with_max_score) {
290
0
                input_rowsets->clear();
291
0
                input_rowsets->push_back(std::move(rs_with_max_score));
292
0
                *compaction_score = max_score;
293
0
                return transient_size;
294
0
            }
295
            // no rowset is OVERLAPPING, return all input rowsets (DEFER will trim to max_compaction_score)
296
0
            return transient_size;
297
0
        }
298
1
    }
299
16
    input_rowsets->erase(input_rowsets->begin(), rs_begin);
300
16
    *compaction_score = new_compaction_score;
301
302
16
    VLOG_CRITICAL << "cumulative compaction size_based policy, compaction_score = "
303
0
                  << *compaction_score << ", total_size = " << total_size
304
0
                  << ", calc promotion size value = " << promotion_size
305
0
                  << ", tablet = " << tablet->tablet_id() << ", input_rowset size "
306
0
                  << input_rowsets->size();
307
308
    // empty return
309
16
    if (input_rowsets->empty()) {
310
0
        return transient_size;
311
0
    }
312
313
    // if we have a sufficient number of segments, we should process the compaction.
314
    // otherwise, we check number of segments and total_size whether can do compaction.
315
16
    if (total_size < _compaction_min_size && *compaction_score < min_compaction_score) {
316
2
        input_rowsets->clear();
317
2
        *compaction_score = 0;
318
14
    } else if (total_size >= _compaction_min_size && input_rowsets->size() == 1) {
319
3
        auto rs_meta = input_rowsets->front()->rowset_meta();
320
        // if there is only one rowset and not overlapping,
321
        // we do not need to do compaction
322
3
        if (!rs_meta->is_segments_overlapping()) {
323
1
            input_rowsets->clear();
324
1
            *compaction_score = 0;
325
1
        }
326
3
    }
327
16
    return transient_size;
328
16
}
329
330
41
int64_t CloudSizeBasedCumulativeCompactionPolicy::cloud_promotion_size(CloudTablet* t) const {
331
41
    int64_t promotion_size = int64_t(cast_set<double>(t->base_size()) * _promotion_ratio);
332
    // promotion_size is between _size_based_promotion_size and _size_based_promotion_min_size
333
41
    return promotion_size > _promotion_size       ? _promotion_size
334
41
           : promotion_size < _promotion_min_size ? _promotion_min_size
335
41
                                                  : promotion_size;
336
41
}
337
338
int64_t CloudSizeBasedCumulativeCompactionPolicy::new_cumulative_point(
339
        CloudTablet* tablet, const RowsetSharedPtr& output_rowset, Version& last_delete_version,
340
22
        int64_t last_cumulative_point) {
341
22
    TEST_INJECTION_POINT_RETURN_WITH_VALUE("new_cumulative_point", int64_t(0), output_rowset.get(),
342
22
                                           last_cumulative_point);
343
22
    DBUG_EXECUTE_IF("CloudSizeBasedCumulativeCompactionPolicy::new_cumulative_point", {
344
22
        auto target_tablet_id = dp->param<int64_t>("tablet_id", -1);
345
22
        auto cumu_point = dp->param<int64_t>("cumu_point", -1);
346
22
        if (target_tablet_id == tablet->tablet_id() && cumu_point != -1) {
347
22
            LOG_INFO(
348
22
                    "[CloudSizeBasedCumulativeCompactionPolicy::new_cumulative_point] "
349
22
                    "tablet_id={}, cumu_point={}",
350
22
                    target_tablet_id, cumu_point);
351
22
            return cumu_point;
352
22
        }
353
22
    });
354
    // for MoW table, if there's too many versions, the delete bitmap will grow to
355
    // a very big size, which may cause the tablet meta too big and the `save_meta`
356
    // operation too slow.
357
    // if the rowset should not promotion according to it's disk size, we should also
358
    // consider it's version count here.
359
22
    bool satisfy_promotion_version = tablet->enable_unique_key_merge_on_write() &&
360
22
                                     output_rowset->end_version() - output_rowset->start_version() >
361
2
                                             config::compaction_promotion_version_count;
362
    // if rowsets have delete version, move to the last directly.
363
    // if rowsets have no delete version, check output_rowset total disk size satisfies promotion size.
364
22
    return (last_delete_version.first != -1 ||
365
22
            output_rowset->total_disk_size() >= cloud_promotion_size(tablet) ||
366
22
            satisfy_promotion_version)
367
22
                   ? output_rowset->end_version() + 1
368
22
                   : last_cumulative_point;
369
22
}
370
371
int64_t CloudTimeSeriesCumulativeCompactionPolicy::pick_input_rowsets(
372
        CloudTablet* tablet, const std::vector<RowsetSharedPtr>& candidate_rowsets,
373
        const int64_t max_compaction_score, const int64_t min_compaction_score,
374
        std::vector<RowsetSharedPtr>* input_rowsets, Version* last_delete_version,
375
0
        size_t* compaction_score, bool allow_delete) {
376
0
    int64_t last_cumu = tablet->last_cumu_compaction_success_time();
377
0
    return TimeSeriesCumulativeCompactionPolicy::pick_input_rowsets(
378
0
            tablet, last_cumu, candidate_rowsets, max_compaction_score, min_compaction_score,
379
0
            input_rowsets, last_delete_version, compaction_score, allow_delete);
380
0
}
381
382
int64_t CloudTimeSeriesCumulativeCompactionPolicy::get_compaction_level(
383
        CloudTablet* tablet, const std::vector<RowsetSharedPtr>& input_rowsets,
384
0
        RowsetSharedPtr output_rowset) {
385
0
    return TimeSeriesCumulativeCompactionPolicy::get_compaction_level((BaseTablet*)tablet,
386
0
                                                                      input_rowsets, output_rowset);
387
0
}
388
389
int64_t CloudTimeSeriesCumulativeCompactionPolicy::new_cumulative_point(
390
        CloudTablet* tablet, const RowsetSharedPtr& output_rowset, Version& last_delete_version,
391
6
        int64_t last_cumulative_point) {
392
6
    if (tablet->tablet_state() != TABLET_RUNNING || output_rowset->num_segments() == 0) {
393
2
        return last_cumulative_point;
394
2
    }
395
396
4
    if (tablet->tablet_meta()->time_series_compaction_level_threshold() >= 2 &&
397
4
        output_rowset->rowset_meta()->compaction_level() < 2) {
398
1
        return last_cumulative_point;
399
1
    }
400
401
3
    return output_rowset->end_version() + 1;
402
4
}
403
404
} // namespace doris