Skip to content

Commit 21228d9

Browse files
authored
feat(realtime): support spilling sealed segments (#327)
1 parent bafe557 commit 21228d9

30 files changed

Lines changed: 1421 additions & 301 deletions

‎include/paimon/defs.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,10 @@ struct PAIMON_EXPORT Options {
612612
/// Default value is "false".
613613
static const char REALTIME_ENABLED[];
614614

615+
/// "realtime.spill-enabled" - Whether the default real-time store spills sealed segments to
616+
/// the local temporary directory. Default value is "false".
617+
static const char REALTIME_SPILL_ENABLED[];
618+
615619
/// "realtime.read-view-ttl" - Lifetime of a real-time memory view pinned by scan planning
616620
/// before reader creation. Default value is "5 min".
617621
static const char REALTIME_READ_VIEW_TTL[];

‎include/paimon/file_store_write.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ class PAIMON_EXPORT FileStoreWrite {
6161
virtual Status Write(std::unique_ptr<RecordBatch>&& batch) = 0;
6262

6363
/// Slices the current in-memory real-time data into a sealed segment so it can be reclaimed
64-
/// independently. A future implementation will support spilling sealed segments to a
65-
/// temporary directory; currently this method only creates the in-memory segment boundary.
64+
/// independently. When `realtime.spill-enabled` is true and the write context has a temporary
65+
/// directory, the default real-time store synchronously spills each non-empty sealed segment
66+
/// to a local Arrow IPC file.
6667
/// Calling this method on a non-real-time writer returns an error.
6768
/// If sealing fails, the caller must recreate both the `RealtimeContext` and writer. The
6869
/// upstream must then recover input from the durable recovery offset persisted in the

‎include/paimon/realtime/realtime_store.h‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct ArrowSchema;
3939

4040
namespace paimon {
4141

42+
class FileSystem;
4243
class MemoryPool;
4344
class Predicate;
4445

@@ -62,6 +63,12 @@ struct PAIMON_EXPORT RealtimeStoreCreateRequest {
6263
RealtimeStoreMode mode = RealtimeStoreMode::APPEND_ONLY;
6364
/// Statistics collected by the store for query pruning.
6465
StatisticsMode statistics_mode = StatisticsMode::NONE;
66+
/// Directory used by the default store for immutable spill files when
67+
/// `realtime.spill-enabled` is true. Custom stores may ignore this hint.
68+
std::string temp_directory = "";
69+
/// File system used to access `temp_directory`. The default store retains shared ownership
70+
/// for its lifetime. Custom stores may ignore this hint.
71+
std::shared_ptr<FileSystem> file_system = nullptr;
6572
};
6673

6774
/// A record batch and its application-assigned offset bounds.
@@ -131,6 +138,8 @@ struct PAIMON_EXPORT RealtimeQueryContext {
131138
/// Optional predicate using field indexes from `read_schema`. A non-null predicate allows the
132139
/// plugin to prune candidate rows. Exact filtering is applied by the Paimon read framework.
133140
std::shared_ptr<Predicate> predicate;
141+
/// Maximum number of rows in a returned query batch.
142+
int32_t read_batch_size = 1024;
134143
};
135144

136145
/// Customizable plugin interface for storing and querying real-time rows before Paimon data-file
@@ -162,6 +171,8 @@ class PAIMON_EXPORT RealtimeStore {
162171
/// preserve write order and contain `_REALTIME_OFFSET` followed by the table write fields.
163172
/// Primary-key readers contain the real-time primary-key store fields; each reader's complete
164173
/// stream is sorted by full primary key then sequence number.
174+
/// Returned readers have independent mutable read state and may be operated concurrently with
175+
/// one another without external synchronization.
165176
virtual Result<std::vector<std::unique_ptr<BatchReader>>> CreateCommitReaders(
166177
const std::shared_ptr<RealtimeSegmentHandle>& segment) = 0;
167178

@@ -173,7 +184,10 @@ class PAIMON_EXPORT RealtimeStore {
173184

174185
/// Creates readers over rows in `view`. The readers collectively expose every candidate row
175186
/// exactly once. Primary-key reader streams are sorted by full primary key then sequence
176-
/// number. Paimon retains `view` for the lifetime of the resulting framework reader.
187+
/// number. Every returned batch contains at most `context.read_batch_size` rows. Paimon retains
188+
/// `view` for the lifetime of the resulting framework reader.
189+
/// Returned readers have independent mutable read state and may be operated concurrently with
190+
/// one another without external synchronization.
177191
virtual Result<std::vector<std::unique_ptr<BatchReader>>> CreateQueryReaders(
178192
const std::shared_ptr<RealtimeReadView>& view, const RealtimeQueryContext& context) = 0;
179193

‎include/paimon/write_context.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ class PAIMON_EXPORT WriteContextBuilder {
222222
/// @return Reference to this builder for method chaining.
223223
WriteContextBuilder& WithExecutor(const std::shared_ptr<Executor>& executor);
224224

225-
/// Set the temporary directory path for IO operations (lookup and external disk spill).
225+
/// Set the temporary directory path for IO operations (lookup, external disk spill, and
226+
/// real-time spill).
226227
/// @param temp_dir The temporary directory path.
227228
/// @return Reference to this builder for method chaining.
228229
WriteContextBuilder& WithTempDirectory(const std::string& temp_dir);

‎src/paimon/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ endif()
212212

213213
set(PAIMON_CORE_SRCS
214214
core/disk/file_io_channel.cpp
215+
core/io/arrow_ipc_file.cpp
215216
core/mergetree/spill_reader.cpp
216217
core/mergetree/spill_writer.cpp
217218
core/append/append_only_writer.cpp

‎src/paimon/common/defs.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ const char Options::TABLE_READ_SEQUENCE_NUMBER_ENABLED[] = "table-read.sequence-
164164
const char Options::KEY_VALUE_SEQUENCE_NUMBER_ENABLED[] = "key-value.sequence_number.enabled";
165165
const char Options::SCAN_TIMESTAMP_MILLIS[] = "scan.timestamp-millis";
166166
const char Options::REALTIME_ENABLED[] = "realtime.enabled";
167+
const char Options::REALTIME_SPILL_ENABLED[] = "realtime.spill-enabled";
167168
const char Options::REALTIME_READ_VIEW_TTL[] = "realtime.read-view-ttl";
168169
const char Options::REALTIME_STORE_STATS_MODE[] = "realtime.store.stats-mode";
169170
const char Options::SCAN_TIMESTAMP[] = "scan.timestamp";

‎src/paimon/core/core_options.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ struct CoreOptions::Impl {
435435
std::optional<int32_t> global_index_thread_num;
436436

437437
bool realtime_enabled = false;
438+
bool realtime_spill_enabled = false;
438439
bool scan_manifest_entry_lazy_decode_enabled = true;
439440
bool ignore_delete = false;
440441
bool manifest_delete_file_drop_stats = false;
@@ -847,6 +848,8 @@ struct CoreOptions::Impl {
847848
// Parse real-time write and read configurations.
848849
Status ParseRealtimeOptions(const ConfigParser& parser) {
849850
PAIMON_RETURN_NOT_OK(parser.Parse<bool>(Options::REALTIME_ENABLED, &realtime_enabled));
851+
PAIMON_RETURN_NOT_OK(
852+
parser.Parse<bool>(Options::REALTIME_SPILL_ENABLED, &realtime_spill_enabled));
850853
PAIMON_RETURN_NOT_OK(parser.ParseTimeDuration(Options::REALTIME_READ_VIEW_TTL,
851854
&realtime_read_view_ttl_millis));
852855
if (realtime_read_view_ttl_millis <= 0) {
@@ -1186,6 +1189,10 @@ bool CoreOptions::RealtimeEnabled() const {
11861189
return impl_->realtime_enabled;
11871190
}
11881191

1192+
bool CoreOptions::RealtimeSpillEnabled() const {
1193+
return impl_->realtime_spill_enabled;
1194+
}
1195+
11891196
int64_t CoreOptions::GetRealtimeReadViewTtlMillis() const {
11901197
return impl_->realtime_read_view_ttl_millis;
11911198
}

‎src/paimon/core/core_options.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class PAIMON_EXPORT CoreOptions {
108108
std::optional<int64_t> GetScanSnapshotId() const;
109109
std::optional<int64_t> GetScanTimestampMillis() const;
110110
bool RealtimeEnabled() const;
111+
bool RealtimeSpillEnabled() const;
111112
int64_t GetRealtimeReadViewTtlMillis() const;
112113
/// Returns the statistics mode used by the real-time store.
113114
StatisticsMode GetRealtimeStoreStatisticsMode() const;

‎src/paimon/core/core_options_test.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
5858
ASSERT_EQ(std::nullopt, core_options.GetScanSnapshotId());
5959
ASSERT_EQ(5 * 60 * 1000, core_options.GetRealtimeReadViewTtlMillis());
6060
ASSERT_FALSE(core_options.RealtimeEnabled());
61+
ASSERT_FALSE(core_options.RealtimeSpillEnabled());
6162
ASSERT_EQ(StatisticsMode::NONE, core_options.GetRealtimeStoreStatisticsMode());
6263
ASSERT_EQ("zstd", core_options.GetFileCompression());
6364
ASSERT_EQ(std::nullopt, core_options.GetChangelogFileCompression());
@@ -325,6 +326,7 @@ TEST(CoreOptionsTest, TestFromMap) {
325326
{Options::TABLE_READ_SEQUENCE_NUMBER_ENABLED, "true"},
326327
{Options::KEY_VALUE_SEQUENCE_NUMBER_ENABLED, "true"},
327328
{Options::REALTIME_ENABLED, "true"},
329+
{Options::REALTIME_SPILL_ENABLED, "true"},
328330
{Options::BUCKET_FUNCTION_TYPE, "mod"},
329331
{"fields.metrics.map.storage-layout", "shared-shredding"},
330332
{"fields.metrics.map.shared-shredding.max-columns", "128"},
@@ -494,6 +496,7 @@ TEST(CoreOptionsTest, TestFromMap) {
494496
ASSERT_TRUE(core_options.TableReadSequenceNumberEnabled());
495497
ASSERT_TRUE(core_options.KeyValueSequenceNumberEnabled());
496498
ASSERT_TRUE(core_options.RealtimeEnabled());
499+
ASSERT_TRUE(core_options.RealtimeSpillEnabled());
497500
ASSERT_TRUE(core_options.LookupRemoteFileEnabled());
498501
ASSERT_EQ(core_options.GetLookupRemoteLevelThreshold(), 2);
499502
ASSERT_EQ(BucketFunctionType::MOD, core_options.GetBucketFunctionType());
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#include "paimon/core/io/arrow_ipc_file.h"
20+
21+
#include <utility>
22+
23+
#include "arrow/ipc/api.h"
24+
#include "arrow/util/compression.h"
25+
#include "paimon/common/utils/arrow/arrow_input_stream_adapter.h"
26+
#include "paimon/common/utils/arrow/arrow_output_stream_adapter.h"
27+
#include "paimon/common/utils/arrow/arrow_utils.h"
28+
#include "paimon/common/utils/arrow/status_utils.h"
29+
#include "paimon/common/utils/scope_guard.h"
30+
#include "paimon/fs/file_system.h"
31+
#include "paimon/macros.h"
32+
33+
namespace paimon {
34+
35+
ArrowIpcFileWriter::ArrowIpcFileWriter(const std::shared_ptr<FileSystem>& fs, std::string path,
36+
const std::shared_ptr<arrow::Schema>& schema,
37+
std::string compression, int32_t compression_level,
38+
bool use_threads,
39+
const std::shared_ptr<arrow::MemoryPool>& arrow_pool)
40+
: fs_(fs),
41+
path_(std::move(path)),
42+
schema_(schema),
43+
compression_(std::move(compression)),
44+
compression_level_(compression_level),
45+
use_threads_(use_threads),
46+
arrow_pool_(arrow_pool) {}
47+
48+
ArrowIpcFileWriter::~ArrowIpcFileWriter() {
49+
[[maybe_unused]] Status status = Close();
50+
}
51+
52+
Result<std::unique_ptr<ArrowIpcFileWriter>> ArrowIpcFileWriter::Create(
53+
const std::shared_ptr<FileSystem>& fs, const std::string& path,
54+
const std::shared_ptr<arrow::Schema>& schema, const std::string& compression,
55+
int32_t compression_level, bool use_threads,
56+
const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
57+
if (!fs || !schema || !arrow_pool || path.empty()) {
58+
return Status::Invalid("invalid Arrow IPC file writer arguments");
59+
}
60+
std::unique_ptr<ArrowIpcFileWriter> writer(new ArrowIpcFileWriter(
61+
fs, path, schema, compression, compression_level, use_threads, arrow_pool));
62+
PAIMON_RETURN_NOT_OK(writer->Open());
63+
return writer;
64+
}
65+
66+
Status ArrowIpcFileWriter::Open() {
67+
auto cleanup_guard = ScopeGuard([&]() {
68+
arrow_writer_.reset();
69+
arrow_output_stream_adapter_.reset();
70+
if (out_stream_) {
71+
[[maybe_unused]] Status status = out_stream_->Close();
72+
out_stream_.reset();
73+
}
74+
[[maybe_unused]] Status status = fs_->Delete(path_);
75+
});
76+
auto write_options = arrow::ipc::IpcWriteOptions::Defaults();
77+
write_options.memory_pool = arrow_pool_.get();
78+
write_options.use_threads = use_threads_;
79+
PAIMON_ASSIGN_OR_RAISE(arrow::Compression::type arrow_compression,
80+
ArrowUtils::GetCompressionType(compression_));
81+
if (!arrow::util::Codec::SupportsCompressionLevel(arrow_compression)) {
82+
compression_level_ = arrow::util::Codec::UseDefaultCompressionLevel();
83+
}
84+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
85+
write_options.codec, arrow::util::Codec::Create(arrow_compression, compression_level_));
86+
PAIMON_ASSIGN_OR_RAISE(out_stream_, fs_->Create(path_, /*overwrite=*/false));
87+
arrow_output_stream_adapter_ = std::make_shared<ArrowOutputStreamAdapter>(out_stream_);
88+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
89+
arrow_writer_,
90+
arrow::ipc::MakeFileWriter(arrow_output_stream_adapter_, schema_, write_options));
91+
cleanup_guard.Release();
92+
return Status::OK();
93+
}
94+
95+
Status ArrowIpcFileWriter::WriteBatch(const std::shared_ptr<arrow::RecordBatch>& batch) {
96+
if (closed_ || !arrow_writer_) {
97+
return Status::Invalid("Arrow IPC file writer is closed");
98+
}
99+
if (!batch) {
100+
return Status::Invalid("Arrow IPC record batch is null");
101+
}
102+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->WriteRecordBatch(*batch));
103+
return Status::OK();
104+
}
105+
106+
Status ArrowIpcFileWriter::Close() {
107+
if (closed_) {
108+
return Status::OK();
109+
}
110+
if (arrow_writer_) {
111+
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow_writer_->Close());
112+
arrow_writer_.reset();
113+
}
114+
if (out_stream_) {
115+
PAIMON_RETURN_NOT_OK(out_stream_->Close());
116+
out_stream_.reset();
117+
}
118+
closed_ = true;
119+
return Status::OK();
120+
}
121+
122+
Result<int64_t> ArrowIpcFileWriter::GetFileSize() const {
123+
if (!closed_ && arrow_output_stream_adapter_) {
124+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(int64_t size, arrow_output_stream_adapter_->Tell());
125+
return size;
126+
}
127+
PAIMON_ASSIGN_OR_RAISE(FileStatus status, fs_->GetFileStatus(path_));
128+
return status.GetLen();
129+
}
130+
131+
ArrowIpcFileReader::ArrowIpcFileReader(const std::shared_ptr<FileSystem>& fs, std::string path,
132+
bool use_threads,
133+
const std::shared_ptr<arrow::MemoryPool>& arrow_pool)
134+
: fs_(fs), path_(std::move(path)), use_threads_(use_threads), arrow_pool_(arrow_pool) {}
135+
136+
ArrowIpcFileReader::~ArrowIpcFileReader() {
137+
Close();
138+
}
139+
140+
Result<std::unique_ptr<ArrowIpcFileReader>> ArrowIpcFileReader::Open(
141+
const std::shared_ptr<FileSystem>& fs, const std::string& path, bool use_threads,
142+
const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
143+
if (!fs || !arrow_pool || path.empty()) {
144+
return Status::Invalid("invalid Arrow IPC file reader arguments");
145+
}
146+
std::unique_ptr<ArrowIpcFileReader> reader(
147+
new ArrowIpcFileReader(fs, path, use_threads, arrow_pool));
148+
PAIMON_RETURN_NOT_OK(reader->DoOpen());
149+
return reader;
150+
}
151+
152+
Status ArrowIpcFileReader::DoOpen() {
153+
PAIMON_ASSIGN_OR_RAISE(in_stream_, fs_->Open(path_));
154+
PAIMON_ASSIGN_OR_RAISE(FileStatus status, fs_->GetFileStatus(path_));
155+
arrow_input_stream_adapter_ =
156+
std::make_shared<ArrowInputStreamAdapter>(in_stream_, status.GetLen(), arrow_pool_);
157+
auto read_options = arrow::ipc::IpcReadOptions::Defaults();
158+
read_options.memory_pool = arrow_pool_.get();
159+
read_options.use_threads = use_threads_;
160+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
161+
arrow_reader_,
162+
arrow::ipc::RecordBatchFileReader::Open(arrow_input_stream_adapter_, read_options));
163+
record_batch_count_ = arrow_reader_->num_record_batches();
164+
return Status::OK();
165+
}
166+
167+
int32_t ArrowIpcFileReader::GetRecordBatchCount() const {
168+
return record_batch_count_;
169+
}
170+
171+
Result<std::shared_ptr<arrow::RecordBatch>> ArrowIpcFileReader::ReadRecordBatch(
172+
int32_t batch_index) {
173+
if (closed_ || !arrow_reader_) {
174+
return Status::Invalid("Arrow IPC file reader is closed");
175+
}
176+
if (batch_index < 0 || batch_index >= record_batch_count_) {
177+
return Status::Invalid("Arrow IPC record batch index out of range: ", batch_index);
178+
}
179+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::RecordBatch> batch,
180+
arrow_reader_->ReadRecordBatch(batch_index));
181+
return batch;
182+
}
183+
184+
void ArrowIpcFileReader::Close() {
185+
if (closed_) {
186+
return;
187+
}
188+
arrow_reader_.reset();
189+
arrow_input_stream_adapter_.reset();
190+
if (in_stream_) {
191+
[[maybe_unused]] Status status = in_stream_->Close();
192+
in_stream_.reset();
193+
}
194+
closed_ = true;
195+
}
196+
197+
} // namespace paimon

0 commit comments

Comments
 (0)