Coverage Report

Created: 2026-08-05 20:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/storage/compaction/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 "storage/compaction/cumulative_compaction_policy.h"
19
20
#include <algorithm>
21
#include <iterator>
22
#include <list>
23
#include <ostream>
24
#include <string>
25
26
#include "common/config.h"
27
#include "common/logging.h"
28
#include "storage/compaction/cumulative_compaction_time_series_policy.h"
29
#include "storage/olap_common.h"
30
#include "storage/tablet/tablet.h"
31
#include "storage/tablet/tablet_meta.h"
32
#include "util/debug_points.h"
33
#include "util/defer_op.h"
34
35
namespace doris {
36
37
SizeBasedCumulativeCompactionPolicy::SizeBasedCumulativeCompactionPolicy(
38
        int64_t promotion_size, double promotion_ratio, int64_t promotion_min_size,
39
        int64_t promotion_version_count, int64_t compaction_min_size)
40
577
        : _promotion_size(promotion_size),
41
577
          _promotion_ratio(promotion_ratio),
42
577
          _promotion_min_size(promotion_min_size),
43
577
          _promotion_version_count(promotion_version_count),
44
577
          _compaction_min_size(compaction_min_size) {}
45
46
void SizeBasedCumulativeCompactionPolicy::calculate_cumulative_point(
47
        Tablet* tablet, const RowsetMetaMapContainer& all_metas, int64_t current_cumulative_point,
48
75
        int64_t* ret_cumulative_point) {
49
75
    *ret_cumulative_point = Tablet::K_INVALID_CUMULATIVE_POINT;
50
75
    if (current_cumulative_point != Tablet::K_INVALID_CUMULATIVE_POINT) {
51
        // only calculate the point once.
52
        // after that, cumulative point will be updated along with compaction process.
53
21
        return;
54
21
    }
55
    // empty return
56
54
    if (all_metas.empty()) {
57
0
        return;
58
0
    }
59
60
54
    std::list<RowsetMetaSharedPtr> existing_rss;
61
3.10k
    for (const auto& [_, rs] : all_metas) {
62
3.10k
        existing_rss.emplace_back(rs);
63
3.10k
    }
64
65
    // sort the existing rowsets by version in ascending order
66
18.7k
    existing_rss.sort([](const RowsetMetaSharedPtr& a, const RowsetMetaSharedPtr& b) {
67
        // simple because 2 versions are certainly not overlapping
68
18.7k
        return a->version().first < b->version().first;
69
18.7k
    });
70
71
    // calculate promotion size
72
54
    auto base_rowset_meta = existing_rss.begin();
73
74
54
    if (tablet->tablet_state() == TABLET_RUNNING) {
75
        // check base rowset first version must be zero
76
        // for tablet which state is not TABLET_RUNNING, there may not have base version.
77
54
        CHECK((*base_rowset_meta)->start_version() == 0);
78
79
54
        int64_t promotion_size = 0;
80
54
        _calc_promotion_size(tablet, *base_rowset_meta, &promotion_size);
81
82
54
        int64_t prev_version = -1;
83
110
        for (const RowsetMetaSharedPtr& rs : existing_rss) {
84
110
            if (rs->version().first > prev_version + 1) {
85
                // There is a hole, do not continue
86
0
                break;
87
0
            }
88
89
110
            bool is_delete = rs->has_delete_predicate();
90
91
            // break the loop if segments in this rowset is overlapping.
92
110
            if (!is_delete && rs->is_segments_overlapping()) {
93
14
                *ret_cumulative_point = rs->version().first;
94
14
                break;
95
14
            }
96
97
            // check the rowset is whether less than promotion size
98
96
            if (!is_delete && rs->version().first != 0 && rs->total_disk_size() < promotion_size) {
99
38
                *ret_cumulative_point = rs->version().first;
100
38
                break;
101
38
            }
102
103
            // include one situation: When the segment is not deleted, and is singleton delta, and is NONOVERLAPPING, ret_cumulative_point increase
104
58
            prev_version = rs->version().second;
105
58
            *ret_cumulative_point = prev_version + 1;
106
58
        }
107
54
        VLOG_NOTICE
108
0
                << "cumulative compaction size_based policy, calculate cumulative point value = "
109
0
                << *ret_cumulative_point << ", calc promotion size value = " << promotion_size
110
0
                << " tablet = " << tablet->tablet_id();
111
54
    } else if (tablet->tablet_state() == TABLET_NOTREADY) {
112
        // tablet under alter process
113
        // we choose version next to the base version as cumulative point
114
0
        for (const RowsetMetaSharedPtr& rs : existing_rss) {
115
0
            if (rs->version().first > 0) {
116
0
                *ret_cumulative_point = rs->version().first;
117
0
                break;
118
0
            }
119
0
        }
120
0
    }
121
54
}
122
123
void SizeBasedCumulativeCompactionPolicy::_calc_promotion_size(Tablet* tablet,
124
                                                               RowsetMetaSharedPtr base_rowset_meta,
125
70
                                                               int64_t* promotion_size) {
126
70
    int64_t base_size = base_rowset_meta->total_disk_size();
127
70
    *promotion_size = int64_t(cast_set<double>(base_size) * _promotion_ratio);
128
129
    // promotion_size is between _promotion_size and _promotion_min_size
130
70
    if (*promotion_size >= _promotion_size) {
131
12
        *promotion_size = _promotion_size;
132
58
    } else if (*promotion_size <= _promotion_min_size) {
133
58
        *promotion_size = _promotion_min_size;
134
58
    }
135
70
    _refresh_tablet_promotion_size(tablet, *promotion_size);
136
70
}
137
138
void SizeBasedCumulativeCompactionPolicy::_refresh_tablet_promotion_size(Tablet* tablet,
139
70
                                                                         int64_t promotion_size) {
140
70
    tablet->set_cumulative_promotion_size(promotion_size);
141
70
}
142
143
void SizeBasedCumulativeCompactionPolicy::update_cumulative_point(
144
        Tablet* tablet, const std::vector<RowsetSharedPtr>& input_rowsets,
145
1
        RowsetSharedPtr output_rowset, Version& last_delete_version) {
146
1
    if (tablet->tablet_state() != TABLET_RUNNING) {
147
        // if tablet under alter process, do not update cumulative point
148
0
        return;
149
0
    }
150
    // if rowsets have delete version, move to the last directly
151
1
    if (last_delete_version.first != -1) {
152
0
        tablet->set_cumulative_layer_point(output_rowset->end_version() + 1);
153
1
    } else {
154
        // if rowsets have no delete version, check output_rowset total disk size
155
        // satisfies promotion size.
156
1
        size_t total_size = output_rowset->rowset_meta()->total_disk_size();
157
1
        if (total_size >= tablet->cumulative_promotion_size()) {
158
0
            tablet->set_cumulative_layer_point(output_rowset->end_version() + 1);
159
1
        } else if (tablet->enable_unique_key_merge_on_write() &&
160
1
                   output_rowset->end_version() - output_rowset->start_version() >
161
0
                           _promotion_version_count) {
162
            // for MoW table, if there's too many versions, the delete bitmap will grow to
163
            // a very big size, which may cause the tablet meta too big and the `save_meta`
164
            // operation too slow.
165
            // if the rowset should not promotion according to it's disk size, we should also
166
            // consider it's version count here.
167
0
            tablet->set_cumulative_layer_point(output_rowset->end_version() + 1);
168
0
        }
169
1
    }
170
1
}
171
172
16
uint32_t SizeBasedCumulativeCompactionPolicy::calc_cumulative_compaction_score(Tablet* tablet) {
173
16
    uint32_t score = 0;
174
16
    bool base_rowset_exist = false;
175
16
    const int64_t point = tablet->cumulative_layer_point();
176
16
    int64_t promotion_size = 0;
177
178
16
    std::vector<RowsetMetaSharedPtr> rowset_to_compact;
179
16
    int64_t total_size = 0;
180
181
16
    RowsetMetaSharedPtr first_meta;
182
16
    int64_t first_version = INT64_MAX;
183
    // NOTE: tablet._meta_lock is hold
184
16
    auto& rs_metas = tablet->tablet_meta()->all_rs_metas();
185
    // check the base rowset and collect the rowsets of cumulative part
186
1.00k
    for (const auto& [_, rs_meta] : rs_metas) {
187
1.00k
        if (rs_meta->start_version() < first_version) {
188
263
            first_version = rs_meta->start_version();
189
263
            first_meta = rs_meta;
190
263
        }
191
        // check base rowset
192
1.00k
        if (rs_meta->start_version() == 0) {
193
16
            base_rowset_exist = true;
194
16
        }
195
1.00k
        if (rs_meta->end_version() < point || !rs_meta->is_local()) {
196
            // all_rs_metas() is not sorted, so we use _continue_ other than _break_ here.
197
16
            continue;
198
993
        } else {
199
            // collect the rowsets of cumulative part
200
993
            total_size += rs_meta->total_disk_size();
201
993
            score += rs_meta->get_compaction_score();
202
993
            rowset_to_compact.push_back(rs_meta);
203
993
        }
204
1.00k
    }
205
206
16
    if (first_meta == nullptr) {
207
0
        return 0;
208
0
    }
209
210
    // Use "first"(not base) version to calc promotion size
211
    // because some tablet do not have base version(under alter operation)
212
16
    _calc_promotion_size(tablet, first_meta, &promotion_size);
213
214
    // If base version does not exist, but its state is RUNNING.
215
    // It is abnormal, do not select it and set *score = 0
216
16
    if (!base_rowset_exist && tablet->tablet_state() == TABLET_RUNNING) {
217
0
        LOG(WARNING) << "tablet state is running but have no base version";
218
0
        return 0;
219
0
    }
220
221
    // if total_size is greater than promotion_size, return total score
222
16
    if (total_size >= promotion_size) {
223
0
        return score;
224
0
    }
225
226
    // sort the rowsets of cumulative part
227
16
    std::sort(rowset_to_compact.begin(), rowset_to_compact.end(), RowsetMeta::comparator);
228
229
    // calculate the rowsets to do cumulative compaction
230
    // eg: size of rowset_to_compact are:
231
    // 128, 16, 16, 16
232
    // we will choose [16,16,16] to compact.
233
16
    for (auto& rs_meta : rowset_to_compact) {
234
16
        int64_t current_level = _level_size(rs_meta->total_disk_size());
235
16
        int64_t remain_level = _level_size(total_size - rs_meta->total_disk_size());
236
        // if current level less then remain level, score contains current rowset
237
        // and process return; otherwise, score does not contains current rowset.
238
16
        if (current_level <= remain_level) {
239
16
            return score;
240
16
        }
241
0
        total_size -= rs_meta->total_disk_size();
242
0
        score -= rs_meta->get_compaction_score();
243
0
    }
244
0
    return score;
245
16
}
246
247
int SizeBasedCumulativeCompactionPolicy::pick_input_rowsets(
248
        Tablet* tablet, const std::vector<RowsetSharedPtr>& candidate_rowsets,
249
        const int64_t max_compaction_score, const int64_t min_compaction_score,
250
        std::vector<RowsetSharedPtr>* input_rowsets, Version* last_delete_version,
251
68
        size_t* compaction_score, bool allow_delete) {
252
68
    DBUG_EXECUTE_IF("SizeBasedCumulativeCompactionPolicy::pick_input_rowsets.set_input_rowsets", {
253
68
        auto target_tablet_id = dp->param<int64_t>("tablet_id", -1);
254
68
        if (target_tablet_id == tablet->tablet_id()) {
255
68
            auto start_version = dp->param<int64_t>("start_version", -1);
256
68
            auto end_version = dp->param<int64_t>("end_version", -1);
257
68
            for (auto& rowset : candidate_rowsets) {
258
68
                if (rowset->start_version() >= start_version &&
259
68
                    rowset->end_version() <= end_version) {
260
68
                    input_rowsets->push_back(rowset);
261
68
                }
262
68
            }
263
68
        }
264
68
        return cast_set<uint32_t>(input_rowsets->size());
265
68
    })
266
267
68
    size_t promotion_size = tablet->cumulative_promotion_size();
268
68
    auto max_version = tablet->max_version().first;
269
68
    int transient_size = 0;
270
68
    *compaction_score = 0;
271
68
    int64_t total_size = 0;
272
68
    RowsetSharedPtr last_popped;
273
274
    // DEFER: trim input_rowsets from back if score > max_compaction_score
275
    // This ensures we don't return more rowsets than allowed by max_compaction_score,
276
    // while still collecting enough rowsets to pass min_compaction_score check after level_size removal.
277
    // Must be placed after variable initialization and before collection loop.
278
68
    DEFER({
279
        // Keep at least 1 rowset to avoid removing the only rowset (consistent with fallback branch)
280
68
        while (input_rowsets->size() > 1 &&
281
68
               *compaction_score > static_cast<size_t>(max_compaction_score)) {
282
68
            last_popped = std::move(input_rowsets->back());
283
68
            *compaction_score -= last_popped->rowset_meta()->get_compaction_score();
284
68
            total_size -= last_popped->rowset_meta()->total_disk_size();
285
68
            input_rowsets->pop_back();
286
68
        }
287
        // A single non-overlapping rowset cannot be compacted by itself. Restore the direct
288
        // successor and accept a one-off max-score overshoot to keep the input mergeable.
289
68
        if (input_rowsets->size() == 1 && last_popped != nullptr &&
290
68
            !input_rowsets->front()->rowset_meta()->is_segments_overlapping()) {
291
68
            *compaction_score += last_popped->rowset_meta()->get_compaction_score();
292
68
            total_size += last_popped->rowset_meta()->total_disk_size();
293
68
            input_rowsets->push_back(std::move(last_popped));
294
68
        }
295
68
    });
296
297
3.59k
    for (auto& rowset : candidate_rowsets) {
298
        // check whether this rowset is delete version
299
3.59k
        if (!allow_delete && rowset->rowset_meta()->has_delete_predicate()) {
300
3
            *last_delete_version = rowset->version();
301
3
            if (!input_rowsets->empty()) {
302
                // we meet a delete version, and there were other versions before.
303
                // we should compact those version before handling them over to base compaction
304
3
                break;
305
3
            } else {
306
                // we meet a delete version, and no other versions before, skip it and continue
307
0
                input_rowsets->clear();
308
0
                *compaction_score = 0;
309
0
                transient_size = 0;
310
0
                continue;
311
0
            }
312
3
        }
313
3.59k
        if (tablet->tablet_state() == TABLET_NOTREADY) {
314
            // If tablet under alter, keep latest 10 version so that base tablet max version
315
            // not merged in new tablet, and then we can copy data from base tablet
316
0
            if (rowset->version().second < max_version - 10) {
317
0
                continue;
318
0
            }
319
0
        }
320
        // Removed: max_compaction_score check here
321
        // We now collect all candidate rowsets and trim from back at return time via DEFER
322
3.59k
        *compaction_score += rowset->rowset_meta()->get_compaction_score();
323
3.59k
        total_size += rowset->rowset_meta()->total_disk_size();
324
325
3.59k
        transient_size += 1;
326
3.59k
        input_rowsets->push_back(rowset);
327
3.59k
    }
328
68
    DBUG_EXECUTE_IF("SizeBaseCumulativeCompactionPolicy.pick_input_rowsets.return_input_rowsets",
329
68
                    { return transient_size; })
330
331
    // if there is delete version, do compaction directly
332
68
    if (last_delete_version->first != -1) {
333
3
        if (input_rowsets->size() == 1) {
334
0
            auto rs_meta = input_rowsets->front()->rowset_meta();
335
            // if there is only one rowset and not overlapping,
336
            // we do not need to do cumulative compaction
337
0
            if (!rs_meta->is_segments_overlapping()) {
338
0
                input_rowsets->clear();
339
0
                *compaction_score = 0;
340
0
            }
341
0
        }
342
3
        return transient_size;
343
3
    }
344
345
65
    auto rs_begin = input_rowsets->begin();
346
65
    size_t new_compaction_score = *compaction_score;
347
65
    const bool can_handle_exhausted_input =
348
65
            *compaction_score >= static_cast<size_t>(max_compaction_score);
349
92
    while (rs_begin != input_rowsets->end()) {
350
87
        auto& rs_meta = (*rs_begin)->rowset_meta();
351
87
        int64_t current_level = _level_size(rs_meta->total_disk_size());
352
87
        int64_t remain_level = _level_size(total_size - rs_meta->total_disk_size());
353
        // if current level less then remain level, input rowsets contain current rowset
354
        // and process return; otherwise, input rowsets do not contain current rowset.
355
87
        if (current_level <= remain_level) {
356
55
            break;
357
55
        }
358
359
32
        auto next = std::next(rs_begin);
360
        // Keep the last suffix rowset for the singleton checks unless the exhausted-input
361
        // fallback below can select a useful input.
362
32
        if (next == input_rowsets->end() && !can_handle_exhausted_input) {
363
5
            break;
364
5
        }
365
27
        total_size -= rs_meta->total_disk_size();
366
27
        new_compaction_score -= rs_meta->get_compaction_score();
367
27
        rs_begin = next;
368
27
    }
369
65
    if (rs_begin == input_rowsets->end() && *compaction_score >= max_compaction_score) {
370
        // No suitable level size found in `input_rowsets` but score of `input_rowsets` exceed max compaction score,
371
        // which means `input_rowsets` will never change and this tablet will never execute cumulative compaction.
372
        // MUST execute compaction on these `input_rowsets` to reduce compaction score.
373
4
        RowsetSharedPtr rs_with_max_score;
374
4
        uint32_t max_score = 1;
375
9
        for (auto& rs : *input_rowsets) {
376
9
            if (rs->rowset_meta()->get_compaction_score() > max_score) {
377
3
                max_score = rs->rowset_meta()->get_compaction_score();
378
3
                rs_with_max_score = rs;
379
3
            }
380
9
        }
381
4
        if (rs_with_max_score) {
382
3
            input_rowsets->clear();
383
3
            input_rowsets->push_back(std::move(rs_with_max_score));
384
3
            *compaction_score = max_score;
385
3
            return transient_size;
386
3
        }
387
        // no rowset is OVERLAPPING, return all input rowsets (DEFER will trim to max_compaction_score)
388
1
        return transient_size;
389
4
    }
390
61
    input_rowsets->erase(input_rowsets->begin(), rs_begin);
391
61
    *compaction_score = new_compaction_score;
392
393
61
    VLOG_CRITICAL << "cumulative compaction size_based policy, compaction_score = "
394
0
                  << *compaction_score << ", total_size = " << total_size
395
0
                  << ", calc promotion size value = " << promotion_size
396
0
                  << ", tablet = " << tablet->tablet_id() << ", input_rowset size "
397
0
                  << input_rowsets->size();
398
399
    // empty return
400
61
    if (input_rowsets->empty()) {
401
1
        return transient_size;
402
1
    }
403
404
    // if we have a sufficient number of segments, we should process the compaction.
405
    // otherwise, we check number of segments and total_size whether can do compaction.
406
60
    if (total_size < _compaction_min_size && *compaction_score < min_compaction_score) {
407
5
        input_rowsets->clear();
408
5
        *compaction_score = 0;
409
55
    } else if (total_size >= _compaction_min_size && input_rowsets->size() == 1) {
410
4
        auto rs_meta = input_rowsets->front()->rowset_meta();
411
        // if there is only one rowset and not overlapping,
412
        // we do not need to do compaction
413
4
        if (!rs_meta->is_segments_overlapping()) {
414
1
            input_rowsets->clear();
415
1
            *compaction_score = 0;
416
1
        }
417
4
    }
418
60
    return transient_size;
419
61
}
420
421
210
int64_t SizeBasedCumulativeCompactionPolicy::_level_size(const int64_t size) {
422
210
    if (size < 1024) return 0;
423
151
    int64_t max_level = (int64_t)1
424
151
                        << (sizeof(_promotion_size) * 8 - 1 - __builtin_clzl(_promotion_size / 2));
425
151
    if (size >= max_level) return max_level;
426
144
    return (int64_t)1 << (sizeof(size) * 8 - 1 - __builtin_clzl(size));
427
151
}
428
429
std::shared_ptr<CumulativeCompactionPolicy>
430
CumulativeCompactionPolicyFactory::create_cumulative_compaction_policy(
431
594
        const std::string_view& compaction_policy) {
432
594
    if (compaction_policy == CUMULATIVE_TIME_SERIES_POLICY) {
433
17
        return std::make_shared<TimeSeriesCumulativeCompactionPolicy>();
434
577
    } else if (compaction_policy == CUMULATIVE_SIZE_BASED_POLICY) {
435
448
        return std::make_shared<SizeBasedCumulativeCompactionPolicy>();
436
448
    }
437
129
    return std::make_shared<SizeBasedCumulativeCompactionPolicy>();
438
594
}
439
} // namespace doris