Skip to content

Commit b76a5ff

Browse files
committed
feat(spill): add SpillWriter, SpillReader and SpillChannelManager for spill-to-disk support
1 parent 3b444c7 commit b76a5ff

30 files changed

Lines changed: 1337 additions & 156 deletions

include/paimon/disk/io_manager.h

Lines changed: 0 additions & 38 deletions
This file was deleted.

include/paimon/write_context.h

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
namespace paimon {
3131
class Executor;
32-
class IOManager;
3332
class MemoryPool;
3433

3534
/// `WriteContext` is some configuration for write operations.
@@ -44,8 +43,7 @@ class PAIMON_EXPORT WriteContext {
4443
const std::optional<int32_t>& write_id, const std::string& branch,
4544
const std::vector<std::string>& write_schema,
4645
const std::shared_ptr<MemoryPool>& memory_pool,
47-
const std::shared_ptr<Executor>& executor,
48-
const std::shared_ptr<IOManager>& io_manager,
46+
const std::shared_ptr<Executor>& executor, const std::string& temp_directory,
4947
const std::shared_ptr<FileSystem>& specific_file_system,
5048
const std::map<std::string, std::string>& fs_scheme_to_identifier_map,
5149
const std::map<std::string, std::string>& options);
@@ -100,8 +98,8 @@ class PAIMON_EXPORT WriteContext {
10098
return executor_;
10199
}
102100

103-
std::shared_ptr<IOManager> GetIOManager() const {
104-
return io_manager_;
101+
const std::string& GetTempDirectory() const {
102+
return temp_directory_;
105103
}
106104

107105
std::shared_ptr<FileSystem> GetSpecificFileSystem() const {
@@ -119,7 +117,7 @@ class PAIMON_EXPORT WriteContext {
119117
std::vector<std::string> write_schema_;
120118
std::shared_ptr<MemoryPool> memory_pool_;
121119
std::shared_ptr<Executor> executor_;
122-
std::shared_ptr<IOManager> io_manager_;
120+
std::string temp_directory_;
123121
std::shared_ptr<FileSystem> specific_file_system_;
124122
std::map<std::string, std::string> fs_scheme_to_identifier_map_;
125123
std::map<std::string, std::string> options_;
@@ -170,10 +168,10 @@ class PAIMON_EXPORT WriteContextBuilder {
170168
/// @return Reference to this builder for method chaining.
171169
WriteContextBuilder& WithExecutor(const std::shared_ptr<Executor>& executor);
172170

173-
/// Set custom IO manager for lookup and external disk spill operations.
174-
/// @param io_manager The IO manager to use.
171+
/// Set the temporary directory path for IO operations (lookup and external disk spill).
172+
/// @param temp_dir The temporary directory path.
175173
/// @return Reference to this builder for method chaining.
176-
WriteContextBuilder& WithIOManager(const std::shared_ptr<IOManager>& io_manager);
174+
WriteContextBuilder& WithTempDirectory(const std::string& temp_dir);
177175

178176
/// For postpone bucket mode in pk table, `WithWriteId()` supposed to be used.
179177
///

src/paimon/CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ set(PAIMON_COMMON_SRCS
146146
common/utils/string_utils.cpp)
147147

148148
set(PAIMON_CORE_SRCS
149-
core/disk/io_manager.cpp
149+
core/disk/file_io_channel.cpp
150+
core/mergetree/spill_reader.cpp
151+
core/mergetree/spill_writer.cpp
150152
core/append/append_only_writer.cpp
151153
core/append/bucketed_append_compact_manager.cpp
152154
core/casting/binary_to_string_cast_executor.cpp
@@ -605,6 +607,8 @@ if(PAIMON_BUILD_TESTS)
605607
core/mergetree/merge_tree_writer_test.cpp
606608
core/mergetree/write_buffer_test.cpp
607609
core/mergetree/sorted_run_test.cpp
610+
core/mergetree/spill_channel_manager_test.cpp
611+
core/mergetree/spill_reader_writer_test.cpp
608612
core/migrate/file_meta_utils_test.cpp
609613
core/operation/metrics/compaction_metrics_test.cpp
610614
core/operation/data_evolution_file_store_scan_test.cpp

src/paimon/common/utils/arrow/arrow_utils.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
#include "arrow/array/array_base.h"
2020
#include "arrow/array/array_nested.h"
21+
#include "arrow/util/compression.h"
2122
#include "paimon/common/utils/arrow/status_utils.h"
23+
#include "paimon/common/utils/string_utils.h"
2224

2325
namespace paimon {
2426
Result<std::shared_ptr<arrow::Schema>> ArrowUtils::DataTypeToSchema(
@@ -161,4 +163,14 @@ Result<std::shared_ptr<arrow::StructArray>> ArrowUtils::RemoveFieldFromStructArr
161163
return array;
162164
}
163165

166+
Result<arrow::Compression::type> ArrowUtils::GetCompressionType(const std::string& compression) {
167+
std::string normalized = StringUtils::ToLowerCase(compression);
168+
if (normalized.empty() || normalized == "none") {
169+
normalized = "uncompressed";
170+
}
171+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::Compression::type compression_type,
172+
arrow::util::Codec::GetCompressionType(normalized));
173+
return compression_type;
174+
}
175+
164176
} // namespace paimon

src/paimon/common/utils/arrow/arrow_utils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <vector>
2020

2121
#include "arrow/api.h"
22+
#include "arrow/util/type_fwd.h"
2223
#include "fmt/format.h"
2324
#include "paimon/result.h"
2425

@@ -49,6 +50,10 @@ class PAIMON_EXPORT ArrowUtils {
4950
static bool EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>& type,
5051
const std::shared_ptr<arrow::DataType>& other_type);
5152

53+
/// Normalize and resolve a compression string to an Arrow compression type.
54+
/// Handles "none" and empty string by mapping them to "uncompressed".
55+
static Result<arrow::Compression::type> GetCompressionType(const std::string& compression);
56+
5257
private:
5358
static Status InnerCheckNullabilityMatch(const std::shared_ptr<arrow::Field>& field,
5459
const std::shared_ptr<arrow::Array>& data);

src/paimon/common/utils/arrow/arrow_utils_test.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,4 +445,43 @@ TEST(ArrowUtilsTest, TestEqualsIgnoreNullable) {
445445
ASSERT_TRUE(ArrowUtils::EqualsIgnoreNullable(struct_type1, struct_type2));
446446
}
447447
}
448+
449+
TEST(ArrowUtilsTest, TestGetCompressionType) {
450+
{
451+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType(""));
452+
ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
453+
}
454+
{
455+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("none"));
456+
ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
457+
}
458+
{
459+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("uncompressed"));
460+
ASSERT_EQ(type, arrow::Compression::UNCOMPRESSED);
461+
}
462+
{
463+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("zstd"));
464+
ASSERT_EQ(type, arrow::Compression::ZSTD);
465+
}
466+
{
467+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("ZSTD"));
468+
ASSERT_EQ(type, arrow::Compression::ZSTD);
469+
}
470+
{
471+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("lz4"));
472+
ASSERT_EQ(type, arrow::Compression::LZ4_FRAME);
473+
}
474+
{
475+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("snappy"));
476+
ASSERT_EQ(type, arrow::Compression::SNAPPY);
477+
}
478+
{
479+
ASSERT_OK_AND_ASSIGN(auto type, ArrowUtils::GetCompressionType("gzip"));
480+
ASSERT_EQ(type, arrow::Compression::GZIP);
481+
}
482+
{
483+
ASSERT_NOK(ArrowUtils::GetCompressionType("invalid_codec"));
484+
}
485+
}
486+
448487
} // namespace paimon::test
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 <memory>
20+
#include <mutex>
21+
#include <random>
22+
#include <string>
23+
24+
#include "paimon/common/utils/path_util.h"
25+
#include "paimon/common/utils/uuid.h"
26+
#include "paimon/core/disk/file_io_channel.h"
27+
#include "paimon/fs/local/local_file_system.h"
28+
#include "paimon/result.h"
29+
30+
namespace paimon {
31+
class FileChannelManager {
32+
public:
33+
static Result<std::unique_ptr<FileChannelManager>> Create(
34+
const std::string& tmp_dir, const std::string& prefix,
35+
const std::shared_ptr<FileSystem>& file_system) {
36+
std::string uuid;
37+
if (!UUID::Generate(&uuid)) {
38+
return Status::Invalid("Failed to generate UUID for FileChannelManager.");
39+
}
40+
std::string spill_dir = PathUtil::JoinPath(tmp_dir, "paimon-" + prefix + "-" + uuid);
41+
42+
PAIMON_RETURN_NOT_OK(file_system->Mkdirs(spill_dir));
43+
44+
std::random_device rd;
45+
std::mt19937 random(rd());
46+
47+
return std::unique_ptr<FileChannelManager>(
48+
new FileChannelManager(spill_dir, std::move(random), file_system));
49+
}
50+
51+
~FileChannelManager() {
52+
if (!spill_dir_.empty() && fs_ != nullptr) {
53+
[[maybe_unused]] auto status = fs_->Delete(spill_dir_, /*recursive=*/true);
54+
}
55+
}
56+
57+
FileChannelManager(const FileChannelManager&) = delete;
58+
FileChannelManager& operator=(const FileChannelManager&) = delete;
59+
60+
FileIOChannel::ID CreateChannel() {
61+
std::lock_guard<std::mutex> lock(mutex_);
62+
return FileIOChannel::ID(spill_dir_, &random_);
63+
}
64+
65+
FileIOChannel::ID CreateChannel(const std::string& prefix) {
66+
std::lock_guard<std::mutex> lock(mutex_);
67+
return FileIOChannel::ID(spill_dir_, prefix, &random_);
68+
}
69+
70+
std::shared_ptr<FileIOChannel::Enumerator> CreateChannelEnumerator() {
71+
std::lock_guard<std::mutex> lock(mutex_);
72+
return std::make_shared<FileIOChannel::Enumerator>(spill_dir_, &random_);
73+
}
74+
75+
const std::string& GetSpillDir() const {
76+
return spill_dir_;
77+
}
78+
79+
private:
80+
FileChannelManager(const std::string& spill_dir, std::mt19937&& random,
81+
const std::shared_ptr<FileSystem>& fs)
82+
: spill_dir_(spill_dir), random_(std::move(random)), fs_(fs) {}
83+
std::string spill_dir_;
84+
std::mutex mutex_;
85+
std::mt19937 random_;
86+
std::shared_ptr<FileSystem> fs_;
87+
};
88+
89+
} // namespace paimon
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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/disk/file_io_channel.h"
18+
19+
#include <iomanip>
20+
#include <sstream>
21+
#include <utility>
22+
23+
#include "paimon/common/utils/path_util.h"
24+
25+
namespace paimon {
26+
std::string FileIOChannel::GenerateRandomHexString(std::mt19937* random) {
27+
std::uniform_int_distribution<int32_t> dist(0, 255);
28+
std::ostringstream hex_stream;
29+
hex_stream << std::hex << std::setfill('0');
30+
for (int32_t i = 0; i < kRandomBytesLength; ++i) {
31+
hex_stream << std::setw(2) << dist(*random);
32+
}
33+
return hex_stream.str();
34+
}
35+
36+
FileIOChannel::ID::ID(const std::string& path) : path_(path) {}
37+
38+
FileIOChannel::ID::ID(const std::string& base_path, std::mt19937* random)
39+
: path_(PathUtil::JoinPath(base_path, GenerateRandomHexString(random) + ".channel")) {}
40+
41+
FileIOChannel::ID::ID(const std::string& base_path, const std::string& prefix, std::mt19937* random)
42+
: path_(PathUtil::JoinPath(base_path,
43+
prefix + "-" + GenerateRandomHexString(random) + ".channel")) {}
44+
45+
const std::string& FileIOChannel::ID::GetPath() const {
46+
return path_;
47+
}
48+
49+
bool FileIOChannel::ID::operator==(const ID& other) const {
50+
return path_ == other.path_;
51+
}
52+
53+
bool FileIOChannel::ID::operator!=(const ID& other) const {
54+
return !(*this == other);
55+
}
56+
57+
size_t FileIOChannel::ID::Hash::operator()(const ID& id) const {
58+
return std::hash<std::string>{}(id.path_);
59+
}
60+
61+
FileIOChannel::Enumerator::Enumerator(const std::string& base_path, std::mt19937* random)
62+
: path_(base_path), name_prefix_(GenerateRandomHexString(random)) {}
63+
64+
FileIOChannel::ID FileIOChannel::Enumerator::Next() {
65+
std::ostringstream filename;
66+
filename << name_prefix_ << "." << std::setfill('0') << std::setw(6) << (local_counter_++)
67+
<< ".channel";
68+
69+
std::string full_path = PathUtil::JoinPath(path_, filename.str());
70+
return ID(full_path);
71+
}
72+
73+
} // namespace paimon

0 commit comments

Comments
 (0)