This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new e409a25a914 branch-4.1:[improvement](be) Make Iceberg data file writes
synchronous (#67087)
e409a25a914 is described below
commit e409a25a9141c939b7ffd2e024329846f14a7973
Author: daidai <[email protected]>
AuthorDate: Wed Aug 26 10:35:41 2026 +0800
branch-4.1:[improvement](be) Make Iceberg data file writes synchronous
(#67087)
### What problem does this PR solve?
Problem Summary: Iceberg data-file sinks inherited AsyncResultWriter,
retaining queued Blocks and dispatching file I/O on a separate writer
thread. Make the normal and spill table sinks own VIcebergTableWriter
directly and run its lifecycle synchronously on the blocking pipeline
scheduler. Preserve spill/revoke behavior and the nested table-writer
compatibility used by MERGE; DELETE and MERGE top-level async paths
remain unchanged.
### Release note
Iceberg data-file writes now run synchronously on the blocking pipeline
scheduler without an intermediate async Block queue.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
.../exec/operator/iceberg_table_sink_operator.cpp | 59 ++-
be/src/exec/operator/iceberg_table_sink_operator.h | 26 +-
be/src/exec/operator/operator.cpp | 2 -
.../operator/spill_iceberg_table_sink_operator.cpp | 151 +++++--
.../operator/spill_iceberg_table_sink_operator.h | 24 +-
be/src/exec/sink/viceberg_merge_sink.cpp | 3 +-
.../sink/writer/iceberg/viceberg_sort_writer.cpp | 27 +-
.../sink/writer/iceberg/viceberg_sort_writer.h | 8 +-
.../sink/writer/iceberg/viceberg_table_writer.cpp | 35 +-
.../sink/writer/iceberg/viceberg_table_writer.h | 40 +-
be/src/exec/sort/sorter.cpp | 3 +-
.../operator/iceberg_table_sink_operator_test.cpp | 450 +++++++++++++++++++++
be/test/exec/sort/full_sort_test.cpp | 6 +-
13 files changed, 757 insertions(+), 77 deletions(-)
diff --git a/be/src/exec/operator/iceberg_table_sink_operator.cpp
b/be/src/exec/operator/iceberg_table_sink_operator.cpp
index 22b7ddd1531..b241ae71ac1 100644
--- a/be/src/exec/operator/iceberg_table_sink_operator.cpp
+++ b/be/src/exec/operator/iceberg_table_sink_operator.cpp
@@ -25,9 +25,64 @@ Status IcebergTableSinkLocalState::init(RuntimeState* state,
LocalSinkStateInfo&
RETURN_IF_ERROR(Base::init(state, info));
SCOPED_TIMER(exec_time_counter());
SCOPED_TIMER(_init_timer);
- auto& p = _parent->cast<Parent>();
- RETURN_IF_ERROR(_writer->init_properties(p._pool, p._row_desc));
+ _writer = std::make_unique<VIcebergTableWriter>(info.tsink,
_output_vexpr_ctxs);
+ _writer->defer_file_cleanup_until_outer_close();
+ auto& parent = _parent->cast<Parent>();
+ RETURN_IF_ERROR(_writer->init_properties(parent._pool, parent._row_desc));
return Status::OK();
}
+Status IcebergTableSinkLocalState::open(RuntimeState* state) {
+ SCOPED_TIMER(exec_time_counter());
+ SCOPED_TIMER(_open_timer);
+ RETURN_IF_ERROR(Base::open(state));
+
+ auto& parent = _parent->cast<Parent>();
+ _output_vexpr_ctxs.resize(parent._output_vexpr_ctxs.size());
+ for (size_t i = 0; i < _output_vexpr_ctxs.size(); ++i) {
+ RETURN_IF_ERROR(parent._output_vexpr_ctxs[i]->clone(state,
_output_vexpr_ctxs[i]));
+ }
+ return _writer->open(state, operator_profile());
+}
+
+Status IcebergTableSinkLocalState::sink(RuntimeState* state, Block* block,
bool /*eos*/) {
+ if (block->rows() == 0) {
+ return Status::OK();
+ }
+ DCHECK(_writer);
+ return _writer->write(state, *block);
+}
+
+Status IcebergTableSinkLocalState::close(RuntimeState* state, Status
exec_status) {
+ if (_closed) {
+ return Status::OK();
+ }
+
+ SCOPED_TIMER(exec_time_counter());
+ SCOPED_TIMER(_close_timer);
+
+ DCHECK(_writer);
+ Status final_status = exec_status;
+ // Observe cancellation before close so a known-cancelled write is not
finalized as successful.
+ if (final_status.ok() && state->is_cancelled()) {
+ final_status = state->cancel_reason();
+ }
+ Status writer_status = _writer->close(final_status);
+ if (final_status.ok() && !writer_status.ok()) {
+ final_status = writer_status;
+ }
+ // close() may block on sort/merge/file I/O, so cancellation can arrive
while it is running.
+ if (final_status.ok() && state->is_cancelled()) {
+ final_status = state->cancel_reason();
+ }
+ _writer->finish_deferred_file_cleanup(final_status);
+ _writer.reset();
+
+ Status base_status = Base::close(state, final_status);
+ if (final_status.ok() && !base_status.ok()) {
+ final_status = base_status;
+ }
+ return final_status;
+}
+
} // namespace doris
diff --git a/be/src/exec/operator/iceberg_table_sink_operator.h
b/be/src/exec/operator/iceberg_table_sink_operator.h
index 1d5cfc9c25f..3a4aa07b2d5 100644
--- a/be/src/exec/operator/iceberg_table_sink_operator.h
+++ b/be/src/exec/operator/iceberg_table_sink_operator.h
@@ -17,6 +17,8 @@
#pragma once
+#include <memory>
+
#include "exec/operator/operator.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
@@ -25,21 +27,26 @@ namespace doris {
class IcebergTableSinkOperatorX;
-class IcebergTableSinkLocalState final
- : public AsyncWriterSink<VIcebergTableWriter,
IcebergTableSinkOperatorX> {
+class IcebergTableSinkLocalState final : public
PipelineXSinkLocalState<FakeSharedState> {
public:
- using Base = AsyncWriterSink<VIcebergTableWriter,
IcebergTableSinkOperatorX>;
+ using Base = PipelineXSinkLocalState<FakeSharedState>;
using Parent = IcebergTableSinkOperatorX;
ENABLE_FACTORY_CREATOR(IcebergTableSinkLocalState);
IcebergTableSinkLocalState(DataSinkOperatorXBase* parent, RuntimeState*
state)
: Base(parent, state) {};
Status init(RuntimeState* state, LocalSinkStateInfo& info) override;
- Status open(RuntimeState* state) override {
- SCOPED_TIMER(exec_time_counter());
- SCOPED_TIMER(_open_timer);
- return Base::open(state);
- }
+ Status open(RuntimeState* state) override;
+ Status sink(RuntimeState* state, Block* block, bool eos);
+ Status close(RuntimeState* state, Status exec_status) override;
+
+ [[nodiscard]] bool is_blockable() const override { return true; }
+
+private:
friend class IcebergTableSinkOperatorX;
+ friend class IcebergTableSinkOperatorTest;
+
+ VExprContextSPtrs _output_vexpr_ctxs;
+ std::unique_ptr<VIcebergTableWriter> _writer;
};
class IcebergTableSinkOperatorX final : public
DataSinkOperatorX<IcebergTableSinkLocalState> {
@@ -74,9 +81,6 @@ public:
private:
friend class IcebergTableSinkLocalState;
- template <typename Writer, typename Parent>
- requires(std::is_base_of_v<AsyncResultWriter, Writer>)
- friend class AsyncWriterSink;
const RowDescriptor& _row_desc;
VExprContextSPtrs _output_vexpr_ctxs;
const std::vector<TExpr>& _t_output_expr;
diff --git a/be/src/exec/operator/operator.cpp
b/be/src/exec/operator/operator.cpp
index e078f4417fb..36449664875 100644
--- a/be/src/exec/operator/operator.cpp
+++ b/be/src/exec/operator/operator.cpp
@@ -938,8 +938,6 @@ template class AsyncWriterSink<doris::VJdbcTableWriter,
JdbcTableSinkOperatorX>;
template class AsyncWriterSink<doris::VTabletWriter, OlapTableSinkOperatorX>;
template class AsyncWriterSink<doris::VTabletWriterV2,
OlapTableSinkV2OperatorX>;
template class AsyncWriterSink<doris::VHiveTableWriter,
HiveTableSinkOperatorX>;
-template class AsyncWriterSink<doris::VIcebergTableWriter,
IcebergTableSinkOperatorX>;
-template class AsyncWriterSink<doris::VIcebergTableWriter,
SpillIcebergTableSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergDeleteSink,
IcebergDeleteSinkOperatorX>;
template class AsyncWriterSink<doris::VIcebergMergeSink,
IcebergMergeSinkOperatorX>;
template class AsyncWriterSink<doris::VMCTableWriter, MCTableSinkOperatorX>;
diff --git a/be/src/exec/operator/spill_iceberg_table_sink_operator.cpp
b/be/src/exec/operator/spill_iceberg_table_sink_operator.cpp
index 20b4eea954a..3177933da31 100644
--- a/be/src/exec/operator/spill_iceberg_table_sink_operator.cpp
+++ b/be/src/exec/operator/spill_iceberg_table_sink_operator.cpp
@@ -22,6 +22,7 @@
#include "exec/operator/spill_utils.h"
#include "exec/sink/writer/iceberg/viceberg_sort_writer.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
+#include "exec/spill/spill_file.h"
namespace doris {
#include "common/compile_check_begin.h"
@@ -36,9 +37,11 @@ Status SpillIcebergTableSinkLocalState::init(RuntimeState*
state, LocalSinkState
SCOPED_TIMER(_init_timer);
_init_spill_counters();
+ _writer = std::make_unique<VIcebergTableWriter>(info.tsink,
_output_vexpr_ctxs);
+ _writer->defer_file_cleanup_until_outer_close();
- auto& p = _parent->cast<Parent>();
- RETURN_IF_ERROR(_writer->init_properties(p._pool, p._row_desc));
+ auto& parent = _parent->cast<Parent>();
+ RETURN_IF_ERROR(_writer->init_properties(parent._pool, parent._row_desc));
return Status::OK();
}
@@ -46,53 +49,149 @@ Status SpillIcebergTableSinkLocalState::open(RuntimeState*
state) {
SCOPED_TIMER(Base::exec_time_counter());
SCOPED_TIMER(Base::_open_timer);
RETURN_IF_ERROR(Base::open(state));
- return Status::OK();
+
+ auto& parent = _parent->cast<Parent>();
+ _output_vexpr_ctxs.resize(parent._output_vexpr_ctxs.size());
+ for (size_t i = 0; i < _output_vexpr_ctxs.size(); ++i) {
+ RETURN_IF_ERROR(parent._output_vexpr_ctxs[i]->clone(state,
_output_vexpr_ctxs[i]));
+ }
+ return _writer->open(state, operator_profile());
}
-bool SpillIcebergTableSinkLocalState::is_blockable() const {
- return true;
+Status SpillIcebergTableSinkLocalState::sink(RuntimeState* state, Block*
block, bool eos) {
+ if (block->rows() > 0) {
+ DCHECK(_writer);
+ RETURN_IF_ERROR(_writer->write(state, *block));
+ }
+ if (!eos) {
+ return Status::OK();
+ }
+
+ Status close_status = Status::OK();
+ if (state->is_cancelled()) {
+ close_status = state->cancel_reason();
+ }
+ return _close_writer(close_status);
}
-size_t SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState*
state, bool eos) {
- if (!_writer) {
- return 0;
+Status SpillIcebergTableSinkLocalState::_close_writer(Status close_status) {
+ DCHECK(_writer);
+ if (!_writer_closed) {
+ _writer_close_status = _writer->close(close_status);
+ _writer_closed = true;
}
- auto current_writer = _writer->current_writer();
- auto* sort_writer =
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
- if (!sort_writer) {
- return 0;
+ if (close_status.ok() && !_writer_close_status.ok()) {
+ close_status = _writer_close_status;
+ }
+ return close_status;
+}
+
+Status SpillIcebergTableSinkLocalState::close(RuntimeState* state, Status
exec_status) {
+ if (_closed) {
+ return Status::OK();
+ }
+
+ SCOPED_TIMER(exec_time_counter());
+ SCOPED_TIMER(_close_timer);
+
+ Status final_status = exec_status;
+ if (final_status.ok() && state->is_cancelled()) {
+ final_status = state->cancel_reason();
+ }
+ final_status = _close_writer(final_status);
+ DCHECK(_writer);
+ _writer->finish_deferred_file_cleanup(final_status);
+ {
+ std::lock_guard lock(_writer_mutex);
+ _writer.reset();
}
- return sort_writer->get_reserve_mem_size(state, eos);
+ Status base_status = Base::close(state, final_status);
+ if (final_status.ok() && !base_status.ok()) {
+ final_status = base_status;
+ }
+ return final_status;
+}
+
+bool SpillIcebergTableSinkLocalState::is_blockable() const {
+ return true;
+}
+
+size_t SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState*
state, bool eos) {
+ DCHECK(_writer);
+ return _writer->get_reserve_mem_size(state, eos);
}
size_t SpillIcebergTableSinkLocalState::get_revocable_mem_size(RuntimeState*
state) const {
- if (!_writer) {
- return 0;
+ std::shared_ptr<const VIcebergTableWriter::PartitionWriterSnapshot>
partition_writers;
+ std::shared_ptr<IPartitionWriterBase> current_writer;
+ {
+ std::lock_guard lock(_writer_mutex);
+ if (!_writer) {
+ return 0;
+ }
+ partition_writers = _writer->partition_writers_snapshot();
+ if (!partition_writers) {
+ current_writer = _writer->current_writer();
+ }
}
- auto current_writer = _writer->current_writer();
- auto* sort_writer =
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
- if (!sort_writer) {
- return 0;
+
+ if (partition_writers) {
+ size_t revocable_size = 0;
+ for (const auto& sort_writer : *partition_writers) {
+ size_t writer_size = sort_writer->data_size();
+ if (writer_size >= SpillFile::MIN_SPILL_WRITE_BATCH_MEM) {
+ revocable_size += writer_size;
+ }
+ }
+ return revocable_size;
}
+ if (!current_writer) {
+ return 0;
+ }
+ auto* sort_writer =
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
+ DORIS_CHECK(sort_writer != nullptr);
return sort_writer->data_size();
}
Status SpillIcebergTableSinkLocalState::revoke_memory(RuntimeState* state) {
RETURN_IF_CANCELLED(state);
- if (!_writer) {
- return Status::OK();
+ std::shared_ptr<const VIcebergTableWriter::PartitionWriterSnapshot>
partition_writers;
+ std::shared_ptr<IPartitionWriterBase> current_writer;
+ {
+ std::lock_guard lock(_writer_mutex);
+ if (!_writer) {
+ return Status::OK();
+ }
+ partition_writers = _writer->partition_writers_snapshot();
+ if (!partition_writers) {
+ current_writer = _writer->current_writer();
+ }
}
- auto current_writer = _writer->current_writer();
- auto* sort_writer =
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
- if (!sort_writer) {
+
+ std::vector<std::shared_ptr<VIcebergSortWriter>> writers_to_spill;
+ if (partition_writers) {
+ for (const auto& sort_writer : *partition_writers) {
+ if (sort_writer->data_size() >=
SpillFile::MIN_SPILL_WRITE_BATCH_MEM) {
+ writers_to_spill.emplace_back(sort_writer);
+ }
+ }
+ } else if (current_writer) {
+ auto sort_writer =
std::dynamic_pointer_cast<VIcebergSortWriter>(current_writer);
+ DORIS_CHECK(sort_writer != nullptr);
+ writers_to_spill.emplace_back(std::move(sort_writer));
+ }
+ if (writers_to_spill.empty()) {
return Status::OK();
}
- auto exception_catch_func = [current_writer, sort_writer]() {
+ auto exception_catch_func = [writers = std::move(writers_to_spill)]() {
auto status = [&]() {
- RETURN_IF_CATCH_EXCEPTION({ return sort_writer->trigger_spill();
});
+ for (const auto& sort_writer : writers) {
+ RETURN_IF_CATCH_EXCEPTION({
RETURN_IF_ERROR(sort_writer->trigger_spill()); });
+ }
+ return Status::OK();
}();
return status;
};
diff --git a/be/src/exec/operator/spill_iceberg_table_sink_operator.h
b/be/src/exec/operator/spill_iceberg_table_sink_operator.h
index 7e6a037d2f5..693a7f33005 100644
--- a/be/src/exec/operator/spill_iceberg_table_sink_operator.h
+++ b/be/src/exec/operator/spill_iceberg_table_sink_operator.h
@@ -18,6 +18,7 @@
#pragma once
#include <memory>
+#include <mutex>
#include "exec/operator/operator.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
@@ -28,10 +29,9 @@ namespace doris {
class SpillIcebergTableSinkLocalState;
class SpillIcebergTableSinkOperatorX;
-class SpillIcebergTableSinkLocalState final
- : public AsyncWriterSink<VIcebergTableWriter,
SpillIcebergTableSinkOperatorX> {
+class SpillIcebergTableSinkLocalState final : public
PipelineXSinkLocalState<FakeSharedState> {
public:
- using Base = AsyncWriterSink<VIcebergTableWriter,
SpillIcebergTableSinkOperatorX>;
+ using Base = PipelineXSinkLocalState<FakeSharedState>;
using Parent = SpillIcebergTableSinkOperatorX;
ENABLE_FACTORY_CREATOR(SpillIcebergTableSinkLocalState);
@@ -40,6 +40,8 @@ public:
Status init(RuntimeState* state, LocalSinkStateInfo& info) override;
Status open(RuntimeState* state) override;
+ Status sink(RuntimeState* state, Block* block, bool eos);
+ Status close(RuntimeState* state, Status exec_status) override;
bool is_blockable() const override;
[[nodiscard]] size_t get_reserve_mem_size(RuntimeState* state, bool eos);
@@ -47,8 +49,19 @@ public:
size_t get_revocable_mem_size(RuntimeState* state) const;
private:
+ Status _close_writer(Status close_status);
void _init_spill_counters();
friend class SpillIcebergTableSinkOperatorX;
+ friend class IcebergTableSinkOperatorTest;
+
+ VExprContextSPtrs _output_vexpr_ctxs;
+ // Protects table-writer pointer access/reset against concurrent
workload-memory callbacks.
+ // Blocking I/O stays outside this lock; a callback keeps a child-writer
shared_ptr whose
+ // sorter mutex serializes it with write/close.
+ mutable std::mutex _writer_mutex;
+ std::unique_ptr<VIcebergTableWriter> _writer;
+ bool _writer_closed = false;
+ Status _writer_close_status;
};
class SpillIcebergTableSinkOperatorX final
@@ -78,9 +91,6 @@ public:
private:
friend class SpillIcebergTableSinkLocalState;
- template <typename Writer, typename Parent>
- requires(std::is_base_of_v<AsyncResultWriter, Writer>)
- friend class AsyncWriterSink;
const RowDescriptor& _row_desc;
VExprContextSPtrs _output_vexpr_ctxs;
@@ -89,4 +99,4 @@ private:
};
#include "common/compile_check_end.h"
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/src/exec/sink/viceberg_merge_sink.cpp
b/be/src/exec/sink/viceberg_merge_sink.cpp
index 5008b217228..0023b37e6d4 100644
--- a/be/src/exec/sink/viceberg_merge_sink.cpp
+++ b/be/src/exec/sink/viceberg_merge_sink.cpp
@@ -56,8 +56,7 @@ Status VIcebergMergeSink::init_properties(ObjectPool* pool,
const RowDescriptor&
RETURN_IF_ERROR(_build_inner_sinks());
if (_writes_data_files) {
- _table_writer = std::make_unique<VIcebergTableWriter>(_table_sink,
_table_output_expr_ctxs,
- nullptr,
nullptr);
+ _table_writer = std::make_unique<VIcebergTableWriter>(_table_sink,
_table_output_expr_ctxs);
_table_writer->defer_file_cleanup_until_outer_close();
RETURN_IF_ERROR(_table_writer->init_properties(pool, row_desc));
}
diff --git a/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.cpp
b/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.cpp
index db453c511fa..b296fa9c504 100644
--- a/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.cpp
+++ b/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.cpp
@@ -58,9 +58,10 @@ Status VIcebergSortWriter::open(RuntimeState* state,
RuntimeProfile* profile,
Status VIcebergSortWriter::write(Block& block) {
std::lock_guard<std::mutex> lock(_sorter_mutex);
+ // FullSorter consumes the input block, so derive the spill batch size
before append_block().
+ _update_spill_block_batch_row_count(block);
// Append incoming block data to the sorter's internal buffer
RETURN_IF_ERROR(_sorter->append_block(&block));
- _update_spill_block_batch_row_count(block);
// When accumulated data size reaches the target file size threshold,
// sort the data in memory and flush it directly to a Parquet/ORC file.
@@ -82,7 +83,21 @@ size_t VIcebergSortWriter::data_size() const {
size_t VIcebergSortWriter::get_reserve_mem_size(RuntimeState* state, bool eos)
const {
std::lock_guard<std::mutex> lock(_sorter_mutex);
- return _sorter == nullptr ? 0 : _sorter->get_reserve_mem_size(state, eos);
+ size_t reserve_size = _sorter == nullptr ? 0 :
_sorter->get_reserve_mem_size(state, eos);
+ if (eos && !_sorted_spill_files.empty()) {
+ const size_t buffer_size =
static_cast<size_t>(state->spill_buffer_size_bytes());
+ const size_t merge_limit =
static_cast<size_t>(state->spill_sort_merge_mem_limit_bytes());
+ const size_t max_fan_in = std::max<size_t>(2, merge_limit /
buffer_size);
+ // Reservation is computed before sink(), so a non-empty EOS can add
one final spill run.
+ const size_t selected_streams = std::min(_sorted_spill_files.size(),
max_fan_in - 1) + 1;
+ // Every selected cursor eagerly owns one input block. A multiway
merge also builds a
+ // separate output block while those inputs remain live.
+ const size_t merge_buffer_count = selected_streams + (selected_streams
> 1 ? 1 : 0);
+ DORIS_CHECK(buffer_size <= std::numeric_limits<size_t>::max() /
merge_buffer_count);
+ const size_t merge_reserve_size = merge_buffer_count * buffer_size;
+ reserve_size = std::max({reserve_size, merge_limit,
merge_reserve_size});
+ }
+ return reserve_size;
}
Status VIcebergSortWriter::trigger_spill() {
@@ -390,8 +405,12 @@ Status VIcebergSortWriter::_create_merger(bool
is_final_merge, size_t batch_size
}
Status VIcebergSortWriter::_create_final_merger() {
- // Final merger uses the runtime batch size and merges all remaining
streams
- return _create_merger(true, _runtime_state->batch_size(), 1);
+ // Keep the final output within both the normal runtime batch and the
spill-buffer estimate.
+ return _create_merger(true, _final_merge_batch_row_count(), 1);
+}
+
+size_t VIcebergSortWriter::_final_merge_batch_row_count() const {
+ return std::min<size_t>(_runtime_state->batch_size(),
_spill_block_batch_row_count);
}
void VIcebergSortWriter::_cleanup_spill_streams() {
diff --git a/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.h
b/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.h
index e1e512f0a0c..9222aed9c82 100644
--- a/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.h
+++ b/be/src/exec/sink/writer/iceberg/viceberg_sort_writer.h
@@ -101,14 +101,16 @@ public:
inline size_t written_len() const override { return
_iceberg_partition_writer->written_len(); }
- size_t data_size() const;
+ virtual size_t data_size() const;
size_t get_reserve_mem_size(RuntimeState* state, bool eos) const;
// Called by the memory management system to trigger spilling data to disk
- Status trigger_spill();
+ virtual Status trigger_spill();
private:
+ friend class IcebergTableSinkOperatorTest;
+
// Calculate average row size from the first non-empty block to determine
// the optimal batch row count for spill operations
void _update_spill_block_batch_row_count(const Block& block);
@@ -152,6 +154,8 @@ private:
// Create the final merger that merges all remaining spill streams
Status _create_final_merger();
+ size_t _final_merge_batch_row_count() const;
+
// Release all spill stream resources (both pending and currently merging)
void _cleanup_spill_streams();
diff --git a/be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp
b/be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp
index b9eeeac38a7..1c8a9523f05 100644
--- a/be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp
+++ b/be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp
@@ -40,10 +40,8 @@ namespace doris {
#include "common/compile_check_begin.h"
VIcebergTableWriter::VIcebergTableWriter(const TDataSink& t_sink,
- const VExprContextSPtrs&
output_expr_ctxs,
- std::shared_ptr<Dependency> dep,
- std::shared_ptr<Dependency> fin_dep)
- : AsyncResultWriter(output_expr_ctxs, dep, fin_dep), _t_sink(t_sink) {
+ const VExprContextSPtrs&
output_expr_ctxs)
+ : _vec_output_expr_ctxs(output_expr_ctxs), _t_sink(t_sink) {
DCHECK(_t_sink.__isset.iceberg_table_sink);
}
@@ -221,6 +219,34 @@ Status VIcebergTableWriter::write_prepared_block(Block&
block) {
return _write_prepared_block(block);
}
+size_t VIcebergTableWriter::get_reserve_mem_size(RuntimeState* state, bool
eos) const {
+ size_t reserve_size = state->minimum_operator_memory_required_bytes();
+ if (!eos) {
+ auto current_writer = _current_writer.load();
+ if (!current_writer) {
+ return reserve_size;
+ }
+ auto* sort_writer =
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
+ DORIS_CHECK(sort_writer != nullptr);
+ return std::max(reserve_size, sort_writer->get_reserve_mem_size(state,
false));
+ }
+
+ auto partition_writer_snapshot =
std::make_shared<PartitionWriterSnapshot>();
+ partition_writer_snapshot->reserve(_partitions_to_writers.size());
+ // close() finalizes partition writers sequentially, so reserve the
largest close peak rather
+ // than the sum. Publish the same owning set for workload
accounting/revoke if reservation
+ // fails. The admission floor covers a first non-empty EOS before its
first writer exists.
+ for (const auto& [_, writer] : _partitions_to_writers) {
+ auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(writer.get());
+ DORIS_CHECK(sort_writer != nullptr);
+ partition_writer_snapshot->emplace_back(
+ std::static_pointer_cast<VIcebergSortWriter>(writer));
+ reserve_size = std::max(reserve_size,
sort_writer->get_reserve_mem_size(state, true));
+ }
+ _partition_writer_snapshot.store(std::move(partition_writer_snapshot));
+ return reserve_size;
+}
+
Status VIcebergTableWriter::_process_row_lineage_columns(Block& block) {
if (_write_type != TIcebergWriteType::INSERT) {
return Status::OK();
@@ -475,6 +501,7 @@ Status VIcebergTableWriter::close(Status status) {
}
}
_partitions_to_writers.clear();
+
_partition_writer_snapshot.store(std::make_shared<PartitionWriterSnapshot>());
}
if (status.ok()) {
SCOPED_TIMER(_operator_profile->total_time_counter());
diff --git a/be/src/exec/sink/writer/iceberg/viceberg_table_writer.h
b/be/src/exec/sink/writer/iceberg/viceberg_table_writer.h
index 070019d85db..b618a949412 100644
--- a/be/src/exec/sink/writer/iceberg/viceberg_table_writer.h
+++ b/be/src/exec/sink/writer/iceberg/viceberg_table_writer.h
@@ -20,9 +20,9 @@
#include <gen_cpp/DataSinks_types.h>
#include "common/atomic_shared_ptr.h"
+#include "common/status.h"
#include "core/block/block.h"
#include "core/column/column.h"
-#include "exec/sink/writer/async_result_writer.h"
#include "exec/sink/writer/iceberg/partition_data.h"
#include "exec/sink/writer/iceberg/partition_transformers.h"
#include "exprs/vexpr_fwd.h"
@@ -44,48 +44,58 @@ class IPartitionWriterBase;
class VIcebergSortWriter;
struct ColumnWithTypeAndName;
-class VIcebergTableWriter final : public AsyncResultWriter {
+class VIcebergTableWriter {
public:
- VIcebergTableWriter(const TDataSink& t_sink, const VExprContextSPtrs&
output_exprs,
- std::shared_ptr<Dependency> dep,
std::shared_ptr<Dependency> fin_dep);
+ using PartitionWriterSnapshot =
std::vector<std::shared_ptr<VIcebergSortWriter>>;
- ~VIcebergTableWriter() = default;
+ VIcebergTableWriter(const TDataSink& t_sink, const VExprContextSPtrs&
output_exprs);
+
+ virtual ~VIcebergTableWriter() = default;
Status init_properties(ObjectPool* pool, const RowDescriptor& row_desc) {
_row_desc = &row_desc;
return Status::OK();
}
- Status open(RuntimeState* state, RuntimeProfile* profile) override;
+ virtual Status open(RuntimeState* state, RuntimeProfile* profile);
- Status write(RuntimeState* state, Block& block) override;
+ virtual Status write(RuntimeState* state, Block& block);
Status write_prepared_block(Block& block);
- Status close(Status) override;
+ virtual Status close(Status);
void defer_file_cleanup_until_outer_close() {
_defer_file_cleanup_until_outer_close = true; }
- void finish_deferred_file_cleanup(Status outer_status);
+ virtual void finish_deferred_file_cleanup(Status outer_status);
bool is_rewrite_compaction() const { return _write_type ==
TIcebergWriteType::REWRITE; }
TIcebergWriteType::type write_type() const { return _write_type; }
+ size_t get_reserve_mem_size(RuntimeState* state, bool eos) const;
+
+ std::shared_ptr<const PartitionWriterSnapshot>
partition_writers_snapshot() const {
+ return _partition_writer_snapshot.load();
+ }
+
// Getter for the current partition writer.
// Used by SpillIcebergTableSinkLocalState to access the current writer for
// memory management operations (get_reserve_mem_size, revocable_mem_size,
etc.).
- // Returns a snapshot by value: the async writer thread updates
_current_writer
- // concurrently with the spill/revoke path, so callers must hold their own
copy
- // while operating on it instead of dereferencing the underlying member
directly.
+ // Returns a snapshot by value. The spill/revoke path may inspect the
current writer
+ // independently of the pipeline task, so callers must hold their own copy
while operating
+ // on it instead of dereferencing the underlying member directly.
std::shared_ptr<IPartitionWriterBase> current_writer() const { return
_current_writer.load(); }
private:
+ friend class IcebergTableSinkOperatorTest;
+
// The currently active partition writer (may be VIcebergPartitionWriter
or VIcebergSortWriter).
// Updated during write() to track which writer received the most recent
data.
- // Wrapped in atomic_shared_ptr because revoke_memory /
get_revocable_mem_size run on
- // a different thread than the async writer that assigns to it.
+ // Wrapped in atomic_shared_ptr because revoke_memory /
get_revocable_mem_size may run
+ // independently of the pipeline task that assigns to it.
doris::atomic_shared_ptr<IPartitionWriterBase> _current_writer;
+ mutable doris::atomic_shared_ptr<const PartitionWriterSnapshot>
_partition_writer_snapshot;
class IcebergPartitionColumn {
public:
IcebergPartitionColumn(const iceberg::PartitionField& field,
@@ -148,8 +158,10 @@ private:
void _cleanup_closed_files();
// Currently it is a copy, maybe it is better to use move semantics to
eliminate it.
+ const VExprContextSPtrs& _vec_output_expr_ctxs;
TDataSink _t_sink;
RuntimeState* _state = nullptr;
+ RuntimeProfile* _operator_profile = nullptr;
// Target file size in bytes for controlling when to split files
int64_t _target_file_size_bytes = 0;
diff --git a/be/src/exec/sort/sorter.cpp b/be/src/exec/sort/sorter.cpp
index 26deeef3a85..b06c7426cb9 100644
--- a/be/src/exec/sort/sorter.cpp
+++ b/be/src/exec/sort/sorter.cpp
@@ -197,7 +197,8 @@ size_t FullSorter::get_reserve_mem_size(RuntimeState*
state, bool eos) const {
if ((new_block_bytes * 100 / allocated_bytes) >= 85) {
size_to_reserve += (size_t)(allocated_bytes * 1.15);
}
- auto sort = new_rows > _buffered_block_size || new_block_bytes >
_buffered_block_bytes;
+ auto sort =
+ eos || new_rows > _buffered_block_size || new_block_bytes >
_buffered_block_bytes;
if (sort) {
// new column is created when doing sort, reserve average size of
one column
// for estimation
diff --git a/be/test/exec/operator/iceberg_table_sink_operator_test.cpp
b/be/test/exec/operator/iceberg_table_sink_operator_test.cpp
new file mode 100644
index 00000000000..6c3e48d4576
--- /dev/null
+++ b/be/test/exec/operator/iceberg_table_sink_operator_test.cpp
@@ -0,0 +1,450 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "exec/operator/iceberg_table_sink_operator.h"
+
+#include <gtest/gtest.h>
+
+#include <functional>
+#include <limits>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "common/object_pool.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "exec/operator/spill_iceberg_table_sink_operator.h"
+#include "exec/sink/writer/async_result_writer.h"
+#include "exec/sink/writer/iceberg/viceberg_sort_writer.h"
+#include "runtime/runtime_profile.h"
+#include "testutil/column_helper.h"
+#include "testutil/mock/mock_descriptors.h"
+#include "testutil/mock/mock_runtime_state.h"
+#include "testutil/mock/mock_slot_ref.h"
+
+namespace doris {
+
+static_assert(!std::is_base_of_v<AsyncResultWriter, VIcebergTableWriter>);
+
+namespace {
+
+TDataSink make_iceberg_table_sink() {
+ TIcebergTableSink iceberg_sink;
+ TDataSink sink;
+ sink.__set_type(TDataSinkType::ICEBERG_TABLE_SINK);
+ sink.__set_iceberg_table_sink(iceberg_sink);
+ return sink;
+}
+
+struct FakeWriterState {
+ Status open_result;
+ Status write_result;
+ Status close_result;
+ int open_count = 0;
+ int write_count = 0;
+ int close_count = 0;
+ int cleanup_count = 0;
+ size_t written_rows = 0;
+ std::vector<Status> close_inputs;
+ Status cleanup_status;
+ std::function<void()> close_hook;
+};
+
+class FakeIcebergTableWriter final : public VIcebergTableWriter {
+public:
+ FakeIcebergTableWriter(const VExprContextSPtrs& output_exprs,
+ std::shared_ptr<FakeWriterState> state)
+ : VIcebergTableWriter(make_iceberg_table_sink(), output_exprs),
+ _fake_state(std::move(state)) {}
+
+ Status open(RuntimeState*, RuntimeProfile*) override {
+ ++_fake_state->open_count;
+ return _fake_state->open_result;
+ }
+
+ Status write(RuntimeState*, Block& block) override {
+ ++_fake_state->write_count;
+ _fake_state->written_rows += block.rows();
+ return _fake_state->write_result;
+ }
+
+ Status close(Status status) override {
+ ++_fake_state->close_count;
+ _fake_state->close_inputs.emplace_back(status);
+ if (_fake_state->close_hook) {
+ _fake_state->close_hook();
+ }
+ return _fake_state->close_result;
+ }
+
+ void finish_deferred_file_cleanup(Status status) override {
+ ++_fake_state->cleanup_count;
+ _fake_state->cleanup_status = status;
+ }
+
+private:
+ std::shared_ptr<FakeWriterState> _fake_state;
+};
+
+struct FakeSortWriterState {
+ size_t data_size = 0;
+ int spill_count = 0;
+};
+
+class FakeRevocableIcebergSortWriter final : public VIcebergSortWriter {
+public:
+ explicit
FakeRevocableIcebergSortWriter(std::shared_ptr<FakeSortWriterState> state)
+ : VIcebergSortWriter(nullptr, TSortInfo {}, 0),
_fake_state(std::move(state)) {}
+
+ size_t data_size() const override { return _fake_state->data_size; }
+
+ Status trigger_spill() override {
+ ++_fake_state->spill_count;
+ _fake_state->data_size = 0;
+ return Status::OK();
+ }
+
+private:
+ std::shared_ptr<FakeSortWriterState> _fake_state;
+};
+
+} // namespace
+
+class IcebergTableSinkOperatorTest : public testing::Test {
+protected:
+ template <typename Parent, typename LocalState>
+ void initialize_local_state(Parent* parent, LocalState* local_state,
MockRuntimeState* state,
+ std::shared_ptr<FakeWriterState>* fake_state) {
+ TDataSink sink = make_iceberg_table_sink();
+ ASSERT_TRUE(parent->init(sink).ok());
+
+ auto shared_state = parent->create_shared_state();
+ LocalSinkStateInfo info {.task_idx = 0,
+ .parent_profile = &_parent_profile,
+ .sender_id = 0,
+ .shared_state = shared_state.get(),
+ .shared_state_map = {},
+ .tsink = sink};
+ ASSERT_TRUE(local_state->init(state, info).ok());
+
+ *fake_state = std::make_shared<FakeWriterState>();
+ auto writer = std::make_unique<FakeIcebergTableWriter>(_output_exprs,
*fake_state);
+ local_state->_writer = std::move(writer);
+ ASSERT_TRUE(local_state->open(state).ok());
+ }
+
+ void set_partition_writers(SpillIcebergTableSinkLocalState* local_state,
+ std::shared_ptr<VIcebergSortWriter> first,
+ std::shared_ptr<VIcebergSortWriter> second) {
+ auto current_writer = second;
+ local_state->_writer->_partitions_to_writers.emplace("first",
std::move(first));
+ local_state->_writer->_partitions_to_writers.emplace("second",
std::move(second));
+ local_state->_writer->_current_writer.store(std::move(current_writer));
+ }
+
+ void mark_spilled(VIcebergSortWriter* writer, size_t count = 1) {
+ for (size_t i = 0; i < count; ++i) {
+ writer->_sorted_spill_files.emplace_back(nullptr);
+ }
+ }
+
+ void initialize_sort_writer_for_write(VIcebergSortWriter* writer,
RuntimeState* state,
+ const DataTypePtr& data_type) {
+ _sort_row_desc =
+ std::make_unique<MockRowDescriptor>(std::vector<DataTypePtr>
{data_type}, &_pool);
+ writer->_runtime_state = state;
+ writer->_ordering_expr_ctxs = MockSlotRef::create_mock_contexts(0,
data_type);
+ writer->_sort_info.is_asc_order = {true};
+ writer->_sort_info.nulls_first = {false};
+ writer->_sorter = FullSorter::create_unique(
+ writer->_ordering_expr_ctxs, -1, 0, &writer->_pool,
writer->_sort_info.is_asc_order,
+ writer->_sort_info.nulls_first, *_sort_row_desc, state,
nullptr);
+ writer->_sorter->set_enable_spill();
+ writer->_target_file_size_bytes = std::numeric_limits<int64_t>::max();
+ }
+
+ std::pair<size_t, size_t> spill_batch_state(const VIcebergSortWriter&
writer) const {
+ return {writer._avg_row_bytes, writer._spill_block_batch_row_count};
+ }
+
+ size_t final_merge_batch_row_count(const VIcebergSortWriter& writer) const
{
+ return writer._final_merge_batch_row_count();
+ }
+
+ ObjectPool _pool;
+ RowDescriptor _row_desc;
+ std::unique_ptr<MockRowDescriptor> _sort_row_desc;
+ VExprContextSPtrs _output_exprs;
+ RuntimeProfile _parent_profile {"IcebergTableSinkOperatorTest"};
+};
+
+TEST_F(IcebergTableSinkOperatorTest,
SyncWritersUseBlockingSchedulerWithoutDependencies) {
+ IcebergTableSinkLocalState table_sink(nullptr, nullptr);
+ SpillIcebergTableSinkLocalState spill_sink(nullptr, nullptr);
+
+ EXPECT_TRUE(table_sink.is_blockable());
+ EXPECT_TRUE(spill_sink.is_blockable());
+ EXPECT_TRUE(table_sink.dependencies().empty());
+ EXPECT_TRUE(spill_sink.dependencies().empty());
+ EXPECT_EQ(table_sink.finishdependency(), nullptr);
+ EXPECT_EQ(spill_sink.finishdependency(), nullptr);
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
NormalSinkWritesDataAndClosesAfterEmptyEos) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ IcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ IcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ Block data = ColumnHelper::create_block<DataTypeInt32>({1, 2, 3});
+ Block empty = ColumnHelper::create_block<DataTypeInt32>({});
+ ASSERT_TRUE(local_state.sink(&state, &data, false).ok());
+ ASSERT_TRUE(local_state.sink(&state, &empty, true).ok());
+ EXPECT_EQ(fake_state->write_count, 1);
+ EXPECT_EQ(fake_state->written_rows, 3);
+ EXPECT_EQ(fake_state->close_count, 0);
+
+ ASSERT_TRUE(local_state.close(&state, Status::OK()).ok());
+ EXPECT_EQ(fake_state->close_count, 1);
+ ASSERT_EQ(fake_state->close_inputs.size(), 1);
+ EXPECT_TRUE(fake_state->close_inputs.front().ok());
+}
+
+TEST_F(IcebergTableSinkOperatorTest, NormalSinkConvertsOkCloseToCancellation) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ IcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ IcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ state.cancel(Status::Cancelled("cancel normal Iceberg sink"));
+ Status close_status = local_state.close(&state, Status::OK());
+ EXPECT_TRUE(close_status.is<ErrorCode::CANCELLED>()) <<
close_status.to_string();
+ ASSERT_EQ(fake_state->close_inputs.size(), 1);
+ EXPECT_TRUE(fake_state->close_inputs.front().is<ErrorCode::CANCELLED>());
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
NormalSinkObservesCancellationDuringWriterClose) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ IcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ IcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+ fake_state->close_hook = [&state]() {
+ state.cancel(Status::Cancelled("cancel during normal writer close"));
+ };
+
+ Status close_status = local_state.close(&state, Status::OK());
+ EXPECT_TRUE(close_status.is<ErrorCode::CANCELLED>()) <<
close_status.to_string();
+ ASSERT_EQ(fake_state->close_inputs.size(), 1);
+ EXPECT_TRUE(fake_state->close_inputs.front().ok());
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+ EXPECT_TRUE(fake_state->cleanup_status.is<ErrorCode::CANCELLED>());
+}
+
+TEST_F(IcebergTableSinkOperatorTest, SpillSinkFinalizesNonEmptyEos) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ Block data = ColumnHelper::create_block<DataTypeInt32>({1, 2, 3});
+ ASSERT_TRUE(local_state.sink(&state, &data, true).ok());
+ EXPECT_EQ(fake_state->write_count, 1);
+ EXPECT_EQ(fake_state->close_count, 1);
+ ASSERT_TRUE(local_state.close(&state, Status::OK()).ok());
+ EXPECT_EQ(fake_state->close_count, 1);
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+ EXPECT_TRUE(fake_state->cleanup_status.ok());
+}
+
+TEST_F(IcebergTableSinkOperatorTest, SpillSinkFinalizesEmptyEosAfterData) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ Block data = ColumnHelper::create_block<DataTypeInt32>({1, 2, 3});
+ Block empty = ColumnHelper::create_block<DataTypeInt32>({});
+ ASSERT_TRUE(local_state.sink(&state, &data, false).ok());
+ EXPECT_EQ(fake_state->close_count, 0);
+ ASSERT_TRUE(local_state.sink(&state, &empty, true).ok());
+ EXPECT_EQ(fake_state->close_count, 1);
+
+ ASSERT_TRUE(local_state.close(&state, Status::OK()).ok());
+ EXPECT_EQ(fake_state->close_count, 1);
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
SpillSinkReservesEosAdmissionAndLargestPartitionMerge) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ EXPECT_EQ(local_state.get_reserve_mem_size(&state, true),
+ state.minimum_operator_memory_required_bytes());
+
+ auto empty_writer = std::make_shared<VIcebergSortWriter>(nullptr,
TSortInfo {}, 0);
+ auto spilled_writer = std::make_shared<VIcebergSortWriter>(nullptr,
TSortInfo {}, 0);
+ auto* spilled_writer_ptr = spilled_writer.get();
+ mark_spilled(spilled_writer.get());
+ EXPECT_EQ(spilled_writer->get_reserve_mem_size(&state, true),
+ static_cast<size_t>(state.spill_sort_merge_mem_limit_bytes()));
+ set_partition_writers(&local_state, std::move(empty_writer),
std::move(spilled_writer));
+ EXPECT_EQ(local_state.get_reserve_mem_size(&state, true),
+ static_cast<size_t>(state.spill_sort_merge_mem_limit_bytes()));
+
+ mark_spilled(spilled_writer_ptr, 7);
+ EXPECT_EQ(local_state.get_reserve_mem_size(&state, true), 72 * 1024 *
1024);
+
+ TQueryOptions query_options = state.query_options();
+ query_options.__set_spill_buffer_size_bytes(256 * 1024 * 1024);
+ query_options.__set_spill_sort_merge_mem_limit_bytes(1024 * 1024);
+ state.set_query_options(query_options);
+ EXPECT_EQ(local_state.get_reserve_mem_size(&state, true), 768 * 1024 *
1024);
+ ASSERT_TRUE(local_state.close(&state, Status::OK()).ok());
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
SortWriterSamplesSpillBatchBeforeConsumingInput) {
+ MockRuntimeState state;
+ TQueryOptions query_options = state.query_options();
+ query_options.__set_spill_buffer_size_bytes(1024 * 1024);
+ state.set_query_options(query_options);
+
+ auto data_type = std::make_shared<DataTypeString>();
+ VIcebergSortWriter writer(nullptr, TSortInfo {},
std::numeric_limits<int64_t>::max());
+ initialize_sort_writer_for_write(&writer, &state, data_type);
+
+ std::string wide_value(64 * 1024, 'x');
+ Block block = ColumnHelper::create_block<DataTypeString>(
+ {wide_value, wide_value, wide_value, wide_value});
+ const size_t expected_avg_row_bytes = block.bytes() / block.rows();
+ const size_t expected_batch_rows =
+ (state.spill_buffer_size_bytes() + expected_avg_row_bytes - 1) /
expected_avg_row_bytes;
+
+ ASSERT_TRUE(writer.write(block).ok());
+ EXPECT_EQ(block.rows(), 0);
+ EXPECT_EQ(spill_batch_state(writer),
+ std::make_pair(expected_avg_row_bytes, expected_batch_rows));
+ EXPECT_EQ(final_merge_batch_row_count(writer),
+ std::min<size_t>(state.batch_size(), expected_batch_rows));
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
SpillSinkAccountsAndRevokesAllEosPartitions) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ auto large_state = std::make_shared<FakeSortWriterState>();
+ large_state->data_size = 1024 * 1024;
+ auto small_state = std::make_shared<FakeSortWriterState>();
+ small_state->data_size = 128 * 1024;
+ set_partition_writers(&local_state,
+
std::make_shared<FakeRevocableIcebergSortWriter>(large_state),
+
std::make_shared<FakeRevocableIcebergSortWriter>(small_state));
+ static_cast<void>(local_state.get_reserve_mem_size(&state, true));
+
+ EXPECT_EQ(local_state.get_revocable_mem_size(&state),
large_state->data_size);
+ ASSERT_TRUE(local_state.revoke_memory(&state).ok());
+ EXPECT_EQ(large_state->spill_count, 1);
+ EXPECT_EQ(small_state->spill_count, 0);
+ EXPECT_EQ(local_state.get_revocable_mem_size(&state), 0);
+ ASSERT_TRUE(local_state.close(&state, Status::OK()).ok());
+}
+
+TEST_F(IcebergTableSinkOperatorTest, SpillSinkWriteErrorClosesWithError) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+ fake_state->write_result = Status::InternalError("injected write failure");
+
+ Block data = ColumnHelper::create_block<DataTypeInt32>({1});
+ Status sink_status = local_state.sink(&state, &data, true);
+ EXPECT_TRUE(sink_status.is<ErrorCode::INTERNAL_ERROR>()) <<
sink_status.to_string();
+ EXPECT_EQ(fake_state->close_count, 0);
+
+ Status close_status = local_state.close(&state, sink_status);
+ EXPECT_TRUE(close_status.is<ErrorCode::INTERNAL_ERROR>()) <<
close_status.to_string();
+ EXPECT_EQ(fake_state->close_count, 1);
+ ASSERT_EQ(fake_state->close_inputs.size(), 1);
+
EXPECT_TRUE(fake_state->close_inputs.front().is<ErrorCode::INTERNAL_ERROR>());
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+ EXPECT_TRUE(fake_state->cleanup_status.is<ErrorCode::INTERNAL_ERROR>());
+}
+
+TEST_F(IcebergTableSinkOperatorTest, SpillSinkCloseErrorPropagatesAndCleansUp)
{
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+ fake_state->close_result = Status::InternalError("injected close failure");
+
+ Block empty = ColumnHelper::create_block<DataTypeInt32>({});
+ Status sink_status = local_state.sink(&state, &empty, true);
+ EXPECT_TRUE(sink_status.is<ErrorCode::INTERNAL_ERROR>()) <<
sink_status.to_string();
+ EXPECT_EQ(fake_state->close_count, 1);
+
+ Status close_status = local_state.close(&state, sink_status);
+ EXPECT_TRUE(close_status.is<ErrorCode::INTERNAL_ERROR>()) <<
close_status.to_string();
+ EXPECT_EQ(fake_state->close_count, 1);
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+ EXPECT_TRUE(fake_state->cleanup_status.is<ErrorCode::INTERNAL_ERROR>());
+}
+
+TEST_F(IcebergTableSinkOperatorTest,
SpillSinkCancellationAfterEosDeletesDeferredFiles) {
+ MockRuntimeState state;
+ std::vector<TExpr> thrift_exprs;
+ SpillIcebergTableSinkOperatorX parent(&_pool, 1, _row_desc, thrift_exprs);
+ SpillIcebergTableSinkLocalState local_state(&parent, &state);
+ std::shared_ptr<FakeWriterState> fake_state;
+ initialize_local_state(&parent, &local_state, &state, &fake_state);
+
+ Block empty = ColumnHelper::create_block<DataTypeInt32>({});
+ ASSERT_TRUE(local_state.sink(&state, &empty, true).ok());
+ EXPECT_EQ(fake_state->close_count, 1);
+
+ state.cancel(Status::Cancelled("cancel spill Iceberg sink after EOS"));
+ Status close_status = local_state.close(&state, Status::OK());
+ EXPECT_TRUE(close_status.is<ErrorCode::CANCELLED>()) <<
close_status.to_string();
+ EXPECT_EQ(fake_state->close_count, 1);
+ EXPECT_EQ(fake_state->cleanup_count, 1);
+ EXPECT_TRUE(fake_state->cleanup_status.is<ErrorCode::CANCELLED>());
+}
+
+} // namespace doris
diff --git a/be/test/exec/sort/full_sort_test.cpp
b/be/test/exec/sort/full_sort_test.cpp
index e182048c807..b7b695fa278 100644
--- a/be/test/exec/sort/full_sort_test.cpp
+++ b/be/test/exec/sort/full_sort_test.cpp
@@ -91,7 +91,9 @@ TEST_F(FullSorterTest, test_full_sorter2) {
EXPECT_TRUE(sorter->append_block(&block).ok());
}
- std::cout << sorter->get_reserve_mem_size(&_state, false) << std::endl;
+ size_t non_eos_reserve_size = sorter->get_reserve_mem_size(&_state, false);
+ size_t eos_reserve_size = sorter->get_reserve_mem_size(&_state, true);
+ EXPECT_GT(eos_reserve_size, non_eos_reserve_size);
}
TEST_F(FullSorterTest, test_full_sorter3) {
@@ -113,4 +115,4 @@ TEST_F(FullSorterTest, test_full_sorter3) {
EXPECT_EQ(sorter->_state->get_sorted_block()[1]->rows(), 4);
}
-} // namespace doris
\ No newline at end of file
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]