Skip to content

Commit c9d86ff

Browse files
committed
feat(spill): add SpillWriter, SpillReader and SpillChannelManager for spill-to-disk support
1 parent 15ad16c commit c9d86ff

11 files changed

Lines changed: 990 additions & 1 deletion
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 <cstdint>
20+
#include <iomanip>
21+
#include <memory>
22+
#include <random>
23+
#include <sstream>
24+
#include <string>
25+
26+
#include "paimon/result.h"
27+
#include "paimon/status.h"
28+
#include "paimon/visibility.h"
29+
30+
namespace paimon {
31+
32+
/// A FileIOChannel represents a collection of files that belong logically to the same resource.
33+
/// An example is a collection of files that contain sorted runs of data from the same stream,
34+
/// that will later on be merged together.
35+
class PAIMON_EXPORT FileIOChannel {
36+
private:
37+
static constexpr int RANDOM_BYTES_LENGTH = 16;
38+
static std::string GenerateRandomHexString(std::mt19937& random) {
39+
std::uniform_int_distribution<int> dist(0, 255);
40+
std::ostringstream hex_stream;
41+
hex_stream << std::hex << std::setfill('0');
42+
for (int i = 0; i < RANDOM_BYTES_LENGTH; ++i) {
43+
hex_stream << std::setw(2) << dist(random);
44+
}
45+
return hex_stream.str();
46+
}
47+
48+
public:
49+
class PAIMON_EXPORT ID {
50+
public:
51+
explicit ID(std::string path) : path_(std::move(path)) {}
52+
53+
ID(const std::string& base_path, std::mt19937& random)
54+
: path_(base_path + "/" + GenerateRandomHexString(random) + ".channel") {}
55+
56+
ID(const std::string& base_path, const std::string& prefix, std::mt19937& random)
57+
: path_(base_path + "/" + prefix + "-" + GenerateRandomHexString(random) + ".channel") {
58+
}
59+
60+
const std::string& GetPath() const {
61+
return path_;
62+
}
63+
64+
bool operator==(const ID& other) const {
65+
return path_ == other.path_;
66+
}
67+
68+
bool operator!=(const ID& other) const {
69+
return !(*this == other);
70+
}
71+
72+
struct Hash {
73+
size_t operator()(const ID& id) const {
74+
return std::hash<std::string>{}(id.path_);
75+
}
76+
};
77+
78+
private:
79+
std::string path_;
80+
};
81+
82+
class PAIMON_EXPORT Enumerator {
83+
public:
84+
Enumerator(std::string base_path, std::mt19937& random)
85+
: path_(std::move(base_path)), name_prefix_(GenerateRandomHexString(random)) {}
86+
std::shared_ptr<ID> Next() {
87+
std::ostringstream filename;
88+
filename << name_prefix_ << "." << std::setfill('0') << std::setw(6)
89+
<< (local_counter_++) << ".channel";
90+
91+
std::string full_path = path_ + "/" + filename.str();
92+
return std::make_shared<ID>(std::move(full_path));
93+
}
94+
95+
private:
96+
std::string path_;
97+
std::string name_prefix_;
98+
uint64_t local_counter_{0};
99+
};
100+
};
101+
102+
} // namespace paimon

include/paimon/disk/io_manager.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <memory>
2121
#include <string>
2222

23+
#include "paimon/disk/file_io_channel.h"
2324
#include "paimon/result.h"
2425
#include "paimon/visibility.h"
2526

@@ -34,5 +35,11 @@ class PAIMON_EXPORT IOManager {
3435
virtual const std::string& GetTempDir() const = 0;
3536

3637
virtual Result<std::string> GenerateTempFilePath(const std::string& prefix) const = 0;
38+
39+
virtual Result<std::shared_ptr<FileIOChannel::ID>> CreateChannel() = 0;
40+
41+
virtual Result<std::shared_ptr<FileIOChannel::ID>> CreateChannel(const std::string& prefix) = 0;
42+
43+
virtual Result<std::shared_ptr<FileIOChannel::Enumerator>> CreateChannelEnumerator() = 0;
3744
};
3845
} // namespace paimon

src/paimon/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,9 @@ if(PAIMON_BUILD_TESTS)
598598
core/mergetree/merge_tree_writer_test.cpp
599599
core/mergetree/write_buffer_test.cpp
600600
core/mergetree/sorted_run_test.cpp
601+
core/mergetree/spill_channel_manager_test.cpp
602+
core/mergetree/spill_reader_test.cpp
603+
core/mergetree/spill_writer_test.cpp
601604
core/migrate/file_meta_utils_test.cpp
602605
core/operation/metrics/compaction_metrics_test.cpp
603606
core/operation/data_evolution_file_store_scan_test.cpp

src/paimon/core/disk/io_manager.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
*/
1616
#include "paimon/disk/io_manager.h"
1717

18+
#include <mutex>
19+
1820
#include "paimon/common/utils/path_util.h"
1921
#include "paimon/common/utils/uuid.h"
2022

2123
namespace paimon {
2224
class IOManagerImpl : public IOManager {
2325
public:
24-
explicit IOManagerImpl(const std::string& tmp_dir) : tmp_dir_(tmp_dir) {}
26+
explicit IOManagerImpl(const std::string& tmp_dir) : tmp_dir_(tmp_dir) {
27+
random_ = std::mt19937(std::random_device{}());
28+
}
2529

2630
const std::string& GetTempDir() const override {
2731
return tmp_dir_;
@@ -35,9 +39,26 @@ class IOManagerImpl : public IOManager {
3539
return PathUtil::JoinPath(tmp_dir_, prefix + "-" + uuid + std::string(kSuffix));
3640
}
3741

42+
Result<std::shared_ptr<FileIOChannel::ID>> CreateChannel() override {
43+
std::lock_guard<std::mutex> lock(mutex_);
44+
return std::make_shared<FileIOChannel::ID>(tmp_dir_, random_);
45+
}
46+
47+
Result<std::shared_ptr<FileIOChannel::ID>> CreateChannel(const std::string& prefix) override {
48+
std::lock_guard<std::mutex> lock(mutex_);
49+
return std::make_shared<FileIOChannel::ID>(tmp_dir_, prefix, random_);
50+
}
51+
52+
Result<std::shared_ptr<FileIOChannel::Enumerator>> CreateChannelEnumerator() override {
53+
std::lock_guard<std::mutex> lock(mutex_);
54+
return std::make_shared<FileIOChannel::Enumerator>(tmp_dir_, random_);
55+
}
56+
3857
private:
3958
static constexpr char kSuffix[] = ".channel";
4059
std::string tmp_dir_;
60+
std::mutex mutex_;
61+
std::mt19937 random_;
4162
};
4263

4364
std::unique_ptr<IOManager> IOManager::Create(const std::string& tmp_dir) {

src/paimon/core/disk/io_manager_test.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,34 @@ TEST(IOManagerTest, GenerateTempFilePathShouldBeDifferentAcrossCalls) {
6363
ASSERT_NE(path1, path2);
6464
}
6565

66+
TEST(IOManagerTest, CreateChannelShouldReturnValidAndUniquePaths) {
67+
auto tmp_dir = UniqueTestDirectory::Create();
68+
std::unique_ptr<IOManager> manager = IOManager::Create(tmp_dir->Str());
69+
const std::string prefix = "spill";
70+
71+
ASSERT_OK_AND_ASSIGN(auto channel1, manager->CreateChannel());
72+
ASSERT_TRUE(StringUtils::StartsWith(channel1->GetPath(), tmp_dir->Str() + "/"));
73+
ASSERT_TRUE(StringUtils::EndsWith(channel1->GetPath(), ".channel"));
74+
ASSERT_EQ(PathUtil::GetName(channel1->GetPath()).size(), 32 + std::string(".channel").size());
75+
76+
ASSERT_OK_AND_ASSIGN(auto channel2, manager->CreateChannel(prefix));
77+
ASSERT_TRUE(StringUtils::StartsWith(PathUtil::GetName(channel2->GetPath()), prefix + "-"));
78+
}
79+
80+
TEST(IOManagerTest, CreateChannelEnumeratorShouldReturnSequentialAndUniquePaths) {
81+
auto tmp_dir = UniqueTestDirectory::Create();
82+
std::unique_ptr<IOManager> manager = IOManager::Create(tmp_dir->Str());
83+
84+
ASSERT_OK_AND_ASSIGN(auto enumerator, manager->CreateChannelEnumerator());
85+
86+
for (int i = 0; i < 10; ++i) {
87+
auto channel_id = enumerator->Next();
88+
ASSERT_TRUE(StringUtils::StartsWith(channel_id->GetPath(), tmp_dir->Str() + "/"));
89+
std::string counter_str = std::to_string(i);
90+
std::string padded_counter = std::string(6 - counter_str.size(), '0') + counter_str;
91+
ASSERT_TRUE(
92+
StringUtils::EndsWith(channel_id->GetPath(), "." + padded_counter + ".channel"));
93+
}
94+
}
95+
6696
} // namespace paimon::test
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 <unordered_set>
20+
21+
#include "paimon/disk/file_io_channel.h"
22+
#include "paimon/fs/file_system.h"
23+
#include "paimon/status.h"
24+
25+
namespace paimon {
26+
27+
class SpillChannelManager {
28+
public:
29+
explicit SpillChannelManager(const std::shared_ptr<FileSystem>& fs,
30+
size_t initial_capacity = 128)
31+
: fs_(fs) {
32+
channels_.reserve(initial_capacity);
33+
}
34+
35+
void AddChannel(const std::shared_ptr<FileIOChannel::ID>& channel_id) {
36+
channels_.emplace(*channel_id);
37+
}
38+
39+
Status DeleteChannel(const std::shared_ptr<FileIOChannel::ID>& channel_id) {
40+
PAIMON_RETURN_NOT_OK(fs_->Delete(channel_id->GetPath()));
41+
channels_.erase(*channel_id);
42+
return Status::OK();
43+
}
44+
45+
Status Reset() {
46+
Status first_error;
47+
for (const auto& channel : channels_) {
48+
auto status = fs_->Delete(channel.GetPath());
49+
if (!status.ok() && first_error.ok()) {
50+
first_error = std::move(status);
51+
}
52+
}
53+
channels_.clear();
54+
return first_error;
55+
}
56+
57+
const std::unordered_set<FileIOChannel::ID, FileIOChannel::ID::Hash>& GetChannels() const {
58+
return channels_;
59+
}
60+
61+
private:
62+
std::unordered_set<FileIOChannel::ID, FileIOChannel::ID::Hash> channels_;
63+
std::shared_ptr<FileSystem> fs_;
64+
};
65+
66+
} // namespace paimon
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
#include "paimon/core/mergetree/spill_channel_manager.h"
18+
19+
#include <memory>
20+
#include <string>
21+
22+
#include "gtest/gtest.h"
23+
#include "paimon/disk/io_manager.h"
24+
#include "paimon/fs/local/local_file_system.h"
25+
#include "paimon/testing/utils/testharness.h"
26+
27+
namespace paimon::test {
28+
29+
class SpillChannelManagerTest : public ::testing::Test {
30+
public:
31+
void SetUp() override {
32+
file_system_ = std::make_shared<LocalFileSystem>();
33+
test_dir_ = UniqueTestDirectory::Create();
34+
io_manager_ = IOManager::Create(test_dir_->Str());
35+
}
36+
37+
std::shared_ptr<FileIOChannel::ID> CreateTempFile() {
38+
auto result = io_manager_->CreateChannel();
39+
EXPECT_TRUE(result.ok());
40+
auto channel_id = result.value();
41+
// Create the actual file on disk
42+
auto out = file_system_->Create(channel_id->GetPath(), /*overwrite=*/false);
43+
EXPECT_TRUE(out.ok());
44+
EXPECT_OK(out.value()->Close());
45+
return channel_id;
46+
}
47+
48+
protected:
49+
std::shared_ptr<LocalFileSystem> file_system_;
50+
std::unique_ptr<UniqueTestDirectory> test_dir_;
51+
std::shared_ptr<IOManager> io_manager_;
52+
};
53+
54+
TEST_F(SpillChannelManagerTest, AddAndGetChannels) {
55+
SpillChannelManager manager(file_system_);
56+
57+
auto channel1 = CreateTempFile();
58+
auto channel2 = CreateTempFile();
59+
60+
manager.AddChannel(channel1);
61+
manager.AddChannel(channel2);
62+
63+
const auto& channels = manager.GetChannels();
64+
ASSERT_EQ(channels.size(), 2);
65+
ASSERT_GT(channels.count(*channel1), 0);
66+
ASSERT_GT(channels.count(*channel2), 0);
67+
}
68+
69+
TEST_F(SpillChannelManagerTest, DeleteChannelRemovesFileAndEntry) {
70+
SpillChannelManager manager(file_system_);
71+
72+
auto channel = CreateTempFile();
73+
manager.AddChannel(channel);
74+
75+
ASSERT_OK_AND_ASSIGN(bool exists_before, file_system_->Exists(channel->GetPath()));
76+
ASSERT_TRUE(exists_before);
77+
78+
ASSERT_OK(manager.DeleteChannel(channel));
79+
ASSERT_EQ(manager.GetChannels().size(), 0);
80+
ASSERT_OK_AND_ASSIGN(bool exists_after, file_system_->Exists(channel->GetPath()));
81+
ASSERT_FALSE(exists_after);
82+
}
83+
84+
TEST_F(SpillChannelManagerTest, ResetDeletesAllFiles) {
85+
SpillChannelManager manager(file_system_);
86+
87+
auto channel1 = CreateTempFile();
88+
auto channel2 = CreateTempFile();
89+
auto channel3 = CreateTempFile();
90+
91+
manager.AddChannel(channel1);
92+
manager.AddChannel(channel2);
93+
manager.AddChannel(channel3);
94+
95+
ASSERT_OK_AND_ASSIGN(bool e1, file_system_->Exists(channel1->GetPath()));
96+
ASSERT_OK_AND_ASSIGN(bool e2, file_system_->Exists(channel2->GetPath()));
97+
ASSERT_OK_AND_ASSIGN(bool e3, file_system_->Exists(channel3->GetPath()));
98+
ASSERT_TRUE(e1);
99+
ASSERT_TRUE(e2);
100+
ASSERT_TRUE(e3);
101+
102+
ASSERT_OK(manager.Reset());
103+
104+
ASSERT_OK_AND_ASSIGN(bool a1, file_system_->Exists(channel1->GetPath()));
105+
ASSERT_OK_AND_ASSIGN(bool a2, file_system_->Exists(channel2->GetPath()));
106+
ASSERT_OK_AND_ASSIGN(bool a3, file_system_->Exists(channel3->GetPath()));
107+
ASSERT_FALSE(a1);
108+
ASSERT_FALSE(a2);
109+
ASSERT_FALSE(a3);
110+
}
111+
112+
} // namespace paimon::test

0 commit comments

Comments
 (0)