Skip to content

Commit b68b93b

Browse files
authored
feat(compaction): support universal & force level0 compaction strategy for pk table (#152)
1 parent 366e021 commit b68b93b

24 files changed

Lines changed: 1821 additions & 6 deletions

include/paimon/defs.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,32 @@ struct PAIMON_EXPORT Options {
303303
/// "compaction.force-rewrite-all-files" - Whether to force pick all files for a full
304304
/// compaction. Usually seen in a compaction task to external paths. Default value is "false".
305305
static const char COMPACTION_FORCE_REWRITE_ALL_FILES[];
306+
/// "compaction.optimization-interval" - Implying how often to perform an optimization
307+
/// compaction, this configuration is used to ensure the query timeliness of the read-optimized
308+
/// system table. No default value.
309+
static const char COMPACTION_OPTIMIZATION_INTERVAL[];
310+
/// "compaction.total-size-threshold" - When total size is smaller than this threshold, force a
311+
/// full compaction. No default value.
312+
static const char COMPACTION_TOTAL_SIZE_THRESHOLD[];
313+
/// "compaction.incremental-size-threshold" - When incremental size is bigger than this
314+
/// threshold, force a full compaction. No default value.
315+
static const char COMPACTION_INCREMENTAL_SIZE_THRESHOLD[];
316+
/// "compaction.offpeak.start.hour" - The start of off-peak hours, expressed as an integer
317+
/// between 0 and 23, inclusive. Set to -1 to disable off-peak. Default is -1.
318+
static const char COMPACT_OFFPEAK_START_HOUR[];
319+
/// "compaction.offpeak.end.hour" - The end of off-peak hours, expressed as an integer between 0
320+
/// and 23, exclusive. Set to -1 to disable off-peak. Default is -1.
321+
static const char COMPACT_OFFPEAK_END_HOUR[];
322+
/// "compaction.offpeak-ratio" - Allows you to set a different (by default, more aggressive)
323+
/// percentage ratio for determining whether larger sorted run's size are included in
324+
/// compactions during off-peak hours. Works in the same way as compaction.size-ratio. Only
325+
/// applies if offpeak.start.hour and offpeak.end.hour are also enabled.
326+
/// For instance, if your cluster experiences low pressure between 2 AM and 6 PM , you can
327+
/// configure `compaction.offpeak.start.hour=2` and `compaction.offpeak.end.hour=18` to define
328+
/// this period as off-peak hours. During these hours, you can increase the off-peak compaction
329+
/// ratio (e.g. `compaction.offpeak-ratio=20`) to enable more aggressive data compaction.
330+
/// Default is 0.
331+
static const char COMPACTION_OFFPEAK_RATIO[];
306332
};
307333

308334
static constexpr int64_t BATCH_WRITE_COMMIT_IDENTIFIER = std::numeric_limits<int64_t>::max();

src/paimon/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ set(PAIMON_CORE_SRCS
210210
core/manifest/manifest_list.cpp
211211
core/manifest/partition_entry.cpp
212212
core/manifest/index_manifest_file_handler.cpp
213+
core/mergetree/compact/universal_compaction.cpp
214+
core/mergetree/compact/early_full_compaction.cpp
213215
core/mergetree/compact/aggregate/aggregate_merge_function.cpp
214216
core/mergetree/compact/aggregate/field_sum_agg.cpp
215217
core/mergetree/compact/interval_partition.cpp
@@ -527,6 +529,11 @@ if(PAIMON_BUILD_TESTS)
527529
core/mergetree/compact/partial_update_merge_function_test.cpp
528530
core/mergetree/compact/reducer_merge_function_wrapper_test.cpp
529531
core/mergetree/compact/sort_merge_reader_test.cpp
532+
core/mergetree/compact/off_peak_hours_test.cpp
533+
core/mergetree/compact/early_full_compaction_test.cpp
534+
core/mergetree/compact/universal_compaction_test.cpp
535+
core/mergetree/compact/force_up_level0_compaction_test.cpp
536+
core/mergetree/compact/compact_strategy_test.cpp
530537
core/mergetree/drop_delete_reader_test.cpp
531538
core/mergetree/merge_tree_writer_test.cpp
532539
core/mergetree/sorted_run_test.cpp

src/paimon/common/defs.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,11 @@ const char Options::SCAN_TAG_NAME[] = "scan.tag-name";
8686
const char Options::WRITE_ONLY[] = "write-only";
8787
const char Options::COMPACTION_MIN_FILE_NUM[] = "compaction.min.file-num";
8888
const char Options::COMPACTION_FORCE_REWRITE_ALL_FILES[] = "compaction.force-rewrite-all-files";
89-
89+
const char Options::COMPACTION_OPTIMIZATION_INTERVAL[] = "compaction.optimization-interval";
90+
const char Options::COMPACTION_TOTAL_SIZE_THRESHOLD[] = "compaction.total-size-threshold";
91+
const char Options::COMPACTION_INCREMENTAL_SIZE_THRESHOLD[] =
92+
"compaction.incremental-size-threshold";
93+
const char Options::COMPACT_OFFPEAK_START_HOUR[] = "compaction.offpeak.start.hour";
94+
const char Options::COMPACT_OFFPEAK_END_HOUR[] = "compaction.offpeak.end.hour";
95+
const char Options::COMPACTION_OFFPEAK_RATIO[] = "compaction.offpeak-ratio";
9096
} // namespace paimon

src/paimon/common/utils/date_time_utils.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ class DateTimeUtils {
116116
return *(static_cast<const int64_t*>(local_ts_scalar->data()));
117117
}
118118

119+
static inline Result<int32_t> GetCurrentLocalHour() {
120+
PAIMON_ASSIGN_OR_RAISE(uint64_t local_us, GetCurrentLocalTimeUs());
121+
auto local_seconds = static_cast<time_t>(local_us / 1000000);
122+
std::tm local_tm{};
123+
gmtime_r(&local_seconds, &local_tm);
124+
return local_tm.tm_hour;
125+
}
126+
119127
static inline int32_t GetPrecisionFromType(
120128
const std::shared_ptr<arrow::TimestampType>& timestamp_type) {
121129
int32_t precision = Timestamp::MAX_PRECISION;

src/paimon/common/utils/date_time_utils_test.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,14 +273,28 @@ TEST(DateTimeUtilsTest, TestGetLocalTimezoneName) {
273273
ASSERT_EQ(DateTimeUtils::GetLocalTimezoneName(), timezone);
274274
}
275275

276-
TEST(DateTimeUtilsTest, TestGetCurrentTime) {
276+
TEST(DateTimeUtilsTest, TestGetCurrentLocalTimeUs) {
277277
TimezoneGuard guard("Asia/Shanghai");
278278
uint64_t utc_ts = DateTimeUtils::GetCurrentUTCTimeUs();
279279
uint64_t local_ts = DateTimeUtils::GetCurrentLocalTimeUs().value();
280280
ASSERT_GT(local_ts, utc_ts);
281281
ASSERT_GE(local_ts - utc_ts, 28800000000l);
282282
}
283283

284+
TEST(DateTimeUtilsTest, TestGetCurrentLocalHour) {
285+
int32_t shanghai_hour = 0;
286+
int32_t utc_hour = 0;
287+
{
288+
TimezoneGuard guard("Asia/Shanghai");
289+
ASSERT_OK_AND_ASSIGN(shanghai_hour, DateTimeUtils::GetCurrentLocalHour());
290+
}
291+
{
292+
TimezoneGuard guard("UTC");
293+
ASSERT_OK_AND_ASSIGN(utc_hour, DateTimeUtils::GetCurrentLocalHour());
294+
}
295+
ASSERT_EQ((shanghai_hour - utc_hour + 24) % 24, 8);
296+
}
297+
284298
TEST(DateTimeUtilsTest, TestToUTCTimestamp) {
285299
TimezoneGuard guard("Asia/Shanghai");
286300
{
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include "paimon/core/io/data_file_meta.h"
20+
#include "paimon/core/mergetree/level_sorted_run.h"
21+
namespace paimon {
22+
/// A files unit for compaction.
23+
struct CompactUnit {
24+
static CompactUnit FromLevelRuns(int32_t output_level,
25+
const std::vector<LevelSortedRun>& runs) {
26+
std::vector<std::shared_ptr<DataFileMeta>> files;
27+
for (const auto& run : runs) {
28+
const auto& files_in_run = run.run.Files();
29+
files.insert(files.end(), files_in_run.begin(), files_in_run.end());
30+
}
31+
return FromFiles(output_level, files, /*file_rewrite=*/false);
32+
}
33+
34+
static CompactUnit FromFiles(int32_t output_level,
35+
const std::vector<std::shared_ptr<DataFileMeta>>& files,
36+
bool file_rewrite) {
37+
return CompactUnit(output_level, files, file_rewrite);
38+
}
39+
40+
CompactUnit(int32_t _output_level, const std::vector<std::shared_ptr<DataFileMeta>>& _files,
41+
bool _file_rewrite)
42+
: output_level(_output_level), files(_files), file_rewrite(_file_rewrite) {}
43+
44+
int32_t output_level;
45+
std::vector<std::shared_ptr<DataFileMeta>> files;
46+
bool file_rewrite;
47+
};
48+
} // namespace paimon

src/paimon/core/core_options.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ struct CoreOptions::Impl {
324324
std::optional<std::string> global_index_external_path;
325325

326326
std::optional<std::string> scan_tag_name;
327+
std::optional<int64_t> optimized_compaction_interval;
328+
std::optional<int64_t> compaction_total_size_threshold;
329+
std::optional<int64_t> compaction_incremental_size_threshold;
330+
int32_t compact_off_peak_start_hour = -1;
331+
int32_t compact_off_peak_end_hour = -1;
332+
int32_t compact_off_peak_ratio = 0;
327333
};
328334

329335
// Parse configurations from a map and return a populated CoreOptions object
@@ -507,6 +513,7 @@ Result<CoreOptions> CoreOptions::FromMap(
507513
impl->scan_tag_name = scan_tag_name;
508514
}
509515

516+
// Parse compaction options
510517
// Parse commit.force-compact
511518
PAIMON_RETURN_NOT_OK(
512519
parser.Parse<bool>(Options::COMMIT_FORCE_COMPACT, &impl->commit_force_compact));
@@ -519,6 +526,41 @@ Result<CoreOptions> CoreOptions::FromMap(
519526
PAIMON_RETURN_NOT_OK(parser.Parse<bool>(Options::COMPACTION_FORCE_REWRITE_ALL_FILES,
520527
&impl->compaction_force_rewrite_all_files));
521528

529+
// Parse compaction.optimization-interval
530+
std::string optimized_compaction_interval_str;
531+
PAIMON_RETURN_NOT_OK(parser.ParseString(Options::COMPACTION_OPTIMIZATION_INTERVAL,
532+
&optimized_compaction_interval_str));
533+
if (!optimized_compaction_interval_str.empty()) {
534+
PAIMON_ASSIGN_OR_RAISE(impl->optimized_compaction_interval,
535+
TimeDuration::Parse(optimized_compaction_interval_str));
536+
}
537+
// Parse compaction.total-size-threshold
538+
std::string compaction_total_size_threshold_str;
539+
PAIMON_RETURN_NOT_OK(parser.ParseString(Options::COMPACTION_TOTAL_SIZE_THRESHOLD,
540+
&compaction_total_size_threshold_str));
541+
if (!compaction_total_size_threshold_str.empty()) {
542+
PAIMON_ASSIGN_OR_RAISE(impl->compaction_total_size_threshold,
543+
MemorySize::ParseBytes(compaction_total_size_threshold_str));
544+
}
545+
// Parse compaction.incremental-size-threshold
546+
std::string compaction_incremental_size_threshold_str;
547+
PAIMON_RETURN_NOT_OK(parser.ParseString(Options::COMPACTION_INCREMENTAL_SIZE_THRESHOLD,
548+
&compaction_incremental_size_threshold_str));
549+
if (!compaction_incremental_size_threshold_str.empty()) {
550+
PAIMON_ASSIGN_OR_RAISE(impl->compaction_incremental_size_threshold,
551+
MemorySize::ParseBytes(compaction_incremental_size_threshold_str));
552+
}
553+
554+
// Parse compaction.offpeak.start.hour
555+
PAIMON_RETURN_NOT_OK(
556+
parser.Parse(Options::COMPACT_OFFPEAK_START_HOUR, &impl->compact_off_peak_start_hour));
557+
// Parse compaction.offpeak.end.hour
558+
PAIMON_RETURN_NOT_OK(
559+
parser.Parse(Options::COMPACT_OFFPEAK_END_HOUR, &impl->compact_off_peak_end_hour));
560+
// Parse compaction.offpeak-ratio
561+
PAIMON_RETURN_NOT_OK(
562+
parser.Parse(Options::COMPACTION_OFFPEAK_RATIO, &impl->compact_off_peak_ratio));
563+
522564
return options;
523565
}
524566

@@ -847,4 +889,24 @@ std::optional<std::string> CoreOptions::GetScanTagName() const {
847889
return impl_->scan_tag_name;
848890
}
849891

892+
std::optional<int64_t> CoreOptions::GetOptimizedCompactionInterval() const {
893+
return impl_->optimized_compaction_interval;
894+
}
895+
std::optional<int64_t> CoreOptions::GetCompactionTotalSizeThreshold() const {
896+
return impl_->compaction_total_size_threshold;
897+
}
898+
std::optional<int64_t> CoreOptions::GetCompactionIncrementalSizeThreshold() const {
899+
return impl_->compaction_incremental_size_threshold;
900+
}
901+
902+
int32_t CoreOptions::GetCompactOffPeakStartHour() const {
903+
return impl_->compact_off_peak_start_hour;
904+
}
905+
int32_t CoreOptions::GetCompactOffPeakEndHour() const {
906+
return impl_->compact_off_peak_end_hour;
907+
}
908+
int32_t CoreOptions::GetCompactOffPeakRatio() const {
909+
return impl_->compact_off_peak_ratio;
910+
}
911+
850912
} // namespace paimon

src/paimon/core/core_options.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ class PAIMON_EXPORT CoreOptions {
125125

126126
std::optional<std::string> GetScanTagName() const;
127127

128+
std::optional<int64_t> GetOptimizedCompactionInterval() const;
129+
std::optional<int64_t> GetCompactionTotalSizeThreshold() const;
130+
std::optional<int64_t> GetCompactionIncrementalSizeThreshold() const;
131+
132+
int32_t GetCompactOffPeakStartHour() const;
133+
int32_t GetCompactOffPeakEndHour() const;
134+
int32_t GetCompactOffPeakRatio() const;
135+
128136
const std::map<std::string, std::string>& ToMap() const;
129137

130138
private:

src/paimon/core/core_options_test.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ TEST(CoreOptionsTest, TestDefaultValue) {
9797
ASSERT_TRUE(core_options.GlobalIndexEnabled());
9898
ASSERT_FALSE(core_options.GetGlobalIndexExternalPath());
9999
ASSERT_EQ(std::nullopt, core_options.GetScanTagName());
100+
ASSERT_EQ(std::nullopt, core_options.GetOptimizedCompactionInterval());
101+
ASSERT_EQ(std::nullopt, core_options.GetCompactionTotalSizeThreshold());
102+
ASSERT_EQ(std::nullopt, core_options.GetCompactionIncrementalSizeThreshold());
103+
ASSERT_EQ(-1, core_options.GetCompactOffPeakStartHour());
104+
ASSERT_EQ(-1, core_options.GetCompactOffPeakEndHour());
105+
ASSERT_EQ(0, core_options.GetCompactOffPeakRatio());
100106
}
101107

102108
TEST(CoreOptionsTest, TestFromMap) {
@@ -159,7 +165,13 @@ TEST(CoreOptionsTest, TestFromMap) {
159165
{Options::SCAN_TAG_NAME, "test-tag"},
160166
{Options::WRITE_ONLY, "true"},
161167
{Options::COMPACTION_MIN_FILE_NUM, "10"},
162-
{Options::COMPACTION_FORCE_REWRITE_ALL_FILES, "true"}};
168+
{Options::COMPACTION_FORCE_REWRITE_ALL_FILES, "true"},
169+
{Options::COMPACTION_OPTIMIZATION_INTERVAL, "2s"},
170+
{Options::COMPACTION_TOTAL_SIZE_THRESHOLD, "5 GB"},
171+
{Options::COMPACTION_INCREMENTAL_SIZE_THRESHOLD, "12 kB"},
172+
{Options::COMPACT_OFFPEAK_START_HOUR, "3"},
173+
{Options::COMPACT_OFFPEAK_END_HOUR, "16"},
174+
{Options::COMPACTION_OFFPEAK_RATIO, "8"}};
163175

164176
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options));
165177
auto fs = core_options.GetFileSystem();
@@ -238,7 +250,13 @@ TEST(CoreOptionsTest, TestFromMap) {
238250
ASSERT_TRUE(core_options.WriteOnly());
239251
ASSERT_EQ(10, core_options.GetCompactionMinFileNum());
240252
ASSERT_TRUE(core_options.CompactionForceRewriteAllFiles());
241-
} // namespace paimon::test
253+
ASSERT_EQ(2000, core_options.GetOptimizedCompactionInterval().value());
254+
ASSERT_EQ(5l * 1024 * 1024 * 1024, core_options.GetCompactionTotalSizeThreshold().value());
255+
ASSERT_EQ(12l * 1024, core_options.GetCompactionIncrementalSizeThreshold().value());
256+
ASSERT_EQ(3, core_options.GetCompactOffPeakStartHour());
257+
ASSERT_EQ(16, core_options.GetCompactOffPeakEndHour());
258+
ASSERT_EQ(8, core_options.GetCompactOffPeakRatio());
259+
}
242260

243261
TEST(CoreOptionsTest, TestInvalidCase) {
244262
ASSERT_NOK_WITH_MSG(CoreOptions::FromMap({{Options::BUCKET, "3.5"}}),
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include "paimon/core/compact/compact_unit.h"
20+
#include "paimon/core/io/data_file_meta.h"
21+
#include "paimon/core/mergetree/level_sorted_run.h"
22+
namespace paimon {
23+
/// Compact strategy to decide which files to select for compaction.
24+
class CompactStrategy {
25+
public:
26+
virtual ~CompactStrategy() = default;
27+
28+
/// Pick compaction unit from runs.
29+
/// @note Compaction is runs-based, not file-based.
30+
/// Level 0 is special, one run per file; all other levels are one run per level.
31+
/// Compaction is sequential from small level to large level.
32+
virtual Result<std::optional<CompactUnit>> Pick(int32_t num_levels,
33+
const std::vector<LevelSortedRun>& runs) = 0;
34+
/// Pick a compaction unit consisting of all existing files.
35+
// TODO(xinyu.lxy): support RecordLevelExpire and BucketedDvMaintainer
36+
static std::optional<CompactUnit> PickFullCompaction(int32_t num_levels,
37+
const std::vector<LevelSortedRun>& runs,
38+
bool force_rewrite_all_files) {
39+
int32_t max_level = num_levels - 1;
40+
if (runs.empty()) {
41+
// no sorted run, no need to compact
42+
return std::nullopt;
43+
}
44+
// only max level files
45+
if (runs.size() == 1 && runs[0].level == max_level) {
46+
std::vector<std::shared_ptr<DataFileMeta>> files_to_be_compacted;
47+
const auto& run = runs[0];
48+
for (const auto& file : run.run.Files()) {
49+
if (force_rewrite_all_files) {
50+
// add all files when force compacted
51+
files_to_be_compacted.push_back(file);
52+
}
53+
// TODO(xinyu.lxy): support RecordLevelExpire and BucketedDvMaintainer
54+
}
55+
if (files_to_be_compacted.empty()) {
56+
return std::nullopt;
57+
}
58+
return CompactUnit::FromFiles(max_level, files_to_be_compacted, /*file_rewrite=*/true);
59+
}
60+
// full compaction
61+
return CompactUnit::FromLevelRuns(max_level, runs);
62+
}
63+
};
64+
} // namespace paimon

0 commit comments

Comments
 (0)