Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5839584
[fix](iceberg) Harden write lifecycle and cleanup
Gabriel39 Aug 1, 2026
f299232
[fix](iceberg) Address write safety review findings
Gabriel39 Aug 1, 2026
08b5ac7
[fix](iceberg) Preserve Azure data and deduplicate orphan scans
Gabriel39 Aug 2, 2026
065e5df
[fix](iceberg) Address follow-up write safety review
Gabriel39 Aug 2, 2026
87b3235
[fix](iceberg) Address multipart and snapshot review feedback
Gabriel39 Aug 2, 2026
25f5e23
[fix](iceberg) Address remaining write safety feedback
Gabriel39 Aug 2, 2026
febfd1a
[fix](iceberg) Match object store hash layout
Gabriel39 Aug 3, 2026
5f8b26f
[fix](azure) Keep multipart block IDs rolling-compatible
Gabriel39 Aug 3, 2026
82a09e1
[test](azure) Seed legacy residual block ID
Gabriel39 Aug 3, 2026
2771934
[fix](azure) Fence multipart writers with blob leases
Gabriel39 Aug 3, 2026
b6f4f01
[fix](iceberg) Close async and rolling boundaries
Gabriel39 Aug 3, 2026
630af93
[test](iceberg) Isolate async admission policy
Gabriel39 Aug 3, 2026
40f328b
[fix](iceberg) Close remaining write safety gaps
Gabriel39 Aug 3, 2026
d002553
[chore](be) Fix clang formatting
Gabriel39 Aug 3, 2026
3f1b475
[fix](iceberg) Close final write ownership gaps
Gabriel39 Aug 4, 2026
d01bebc
[fix](iceberg) Gate standalone deletes on report ACK
Gabriel39 Aug 4, 2026
6799e4e
[fix](iceberg) Preserve external write ownership until final report
Gabriel39 Aug 4, 2026
403b77a
[fix](iceberg) Avoid writer test suite collision
Gabriel39 Aug 5, 2026
025bd21
[test](be) Cover deferred upload report lifecycle
Gabriel39 Aug 5, 2026
44cb06d
[fix](be) Declare external report Thrift parameter
Gabriel39 Aug 6, 2026
7dbfd96
[fix](iceberg) Bound partition sorter memory admission
Gabriel39 Aug 6, 2026
78a36ec
[fix](regression) Isolate MTMV job lookup
Gabriel39 Aug 6, 2026
c5f07d4
[fix](test) Model Iceberg report ACK in merge sink UT
Gabriel39 Aug 6, 2026
35a48c0
[fix](test) Preserve rebase safety invariants
Gabriel39 Aug 6, 2026
190ebe5
[fix](iceberg) Close external write review gaps
Gabriel39 Aug 7, 2026
b02a3ff
[fix](fe) Fix Checkstyle import ordering
Gabriel39 Aug 7, 2026
8314c90
[fix](iceberg) Keep scan test compatible with catalog properties
Gabriel39 Aug 7, 2026
71abd40
[fix](azure) Isolate multipart blocks with upload UUIDs
Gabriel39 Aug 10, 2026
64eba05
[fix](iceberg) Preserve EOS memory reservation through close
Gabriel39 Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions be/src/exec/operator/iceberg_sorter_reserve_memory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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.

#pragma once

#include <algorithm>
#include <limits>
#include <vector>

namespace doris {

class Block;

struct IcebergSorterReserveMemory {
size_t retained_growth = 0;
size_t retained_growth_trigger_bytes = 0;
size_t transient_workspace = 0;
};

inline size_t iceberg_saturating_add(size_t lhs, size_t rhs) {
return std::min(std::numeric_limits<size_t>::max() - lhs, rhs) + lhs;
}

inline size_t bounded_iceberg_reserve_size(
const std::vector<IcebergSorterReserveMemory>& per_partition_reservations,
size_t incoming_rows = std::numeric_limits<size_t>::max(),
size_t incoming_bytes = std::numeric_limits<size_t>::max()) {
size_t transient_workspace = 0;
for (const auto& reservation : per_partition_reservations) {
transient_workspace = std::max(transient_workspace, reservation.transient_workspace);
}

std::vector<const IcebergSorterReserveMemory*> growth_candidates;
growth_candidates.reserve(per_partition_reservations.size());
for (const auto& reservation : per_partition_reservations) {
if (reservation.retained_growth > 0) {
growth_candidates.push_back(&reservation);
}
}

std::sort(growth_candidates.begin(), growth_candidates.end(),
[](const auto* lhs, const auto* rhs) {
return lhs->retained_growth > rhs->retained_growth;
});
size_t row_bound = 0;
for (size_t i = 0; i < std::min(incoming_rows, growth_candidates.size()); ++i) {
row_bound = iceberg_saturating_add(row_bound, growth_candidates[i]->retained_growth);
}

size_t byte_bound = 0;
std::vector<const IcebergSorterReserveMemory*> positive_trigger_candidates;
positive_trigger_candidates.reserve(growth_candidates.size());
for (const auto* reservation : growth_candidates) {
if (reservation->retained_growth_trigger_bytes == 0) {
byte_bound = iceberg_saturating_add(byte_bound, reservation->retained_growth);
} else {
positive_trigger_candidates.push_back(reservation);
}
}
std::sort(positive_trigger_candidates.begin(), positive_trigger_candidates.end(),
[](const auto* lhs, const auto* rhs) {
return static_cast<unsigned __int128>(lhs->retained_growth) *
rhs->retained_growth_trigger_bytes >
static_cast<unsigned __int128>(rhs->retained_growth) *
lhs->retained_growth_trigger_bytes;
});
size_t remaining_bytes = incoming_bytes;
for (const auto* reservation : positive_trigger_candidates) {
if (reservation->retained_growth_trigger_bytes <= remaining_bytes) {
byte_bound = iceberg_saturating_add(byte_bound, reservation->retained_growth);
remaining_bytes -= reservation->retained_growth_trigger_bytes;
continue;
}
const auto numerator =
static_cast<unsigned __int128>(reservation->retained_growth) * remaining_bytes +
reservation->retained_growth_trigger_bytes - 1;
const auto fractional_growth =
std::min<unsigned __int128>(numerator / reservation->retained_growth_trigger_bytes,
std::numeric_limits<size_t>::max());
byte_bound = iceberg_saturating_add(byte_bound, static_cast<size_t>(fractional_growth));
break;
}

// A block's rows and bytes are divided across partition sorters. The two fractional-relaxation
// bounds avoid charging the complete input block to every active partition while remaining safe.
const size_t retained_growth = std::min(row_bound, byte_bound);
return iceberg_saturating_add(retained_growth, transient_workspace);
}

inline size_t iceberg_reserve_size(
const std::vector<IcebergSorterReserveMemory>& per_partition_reservations,
size_t incoming_block_reserve, size_t incoming_rows = std::numeric_limits<size_t>::max(),
size_t incoming_bytes = std::numeric_limits<size_t>::max()) {
size_t sorter_reserve =
bounded_iceberg_reserve_size(per_partition_reservations, incoming_rows, incoming_bytes);
// The incoming block creates cold partition writers before they can appear in the published snapshot.
return iceberg_saturating_add(sorter_reserve, incoming_block_reserve);
}

size_t iceberg_cold_writer_reserve_size(const Block& block, size_t writer_workspace_bytes);

inline size_t iceberg_spill_merge_workspace(size_t spill_file_count, size_t spill_buffer_bytes,
size_t merge_limit_bytes) {
if (spill_file_count == 0 || spill_buffer_bytes == 0) {
return 0;
}
const size_t max_fan_in = std::max<size_t>(2, merge_limit_bytes / spill_buffer_bytes);
const size_t input_count = std::min(spill_file_count, max_fan_in);
const size_t max_size = std::numeric_limits<size_t>::max();
const size_t input_bytes = input_count > max_size / spill_buffer_bytes
? max_size
: input_count * spill_buffer_bytes;
// VSortedRunMerger materializes one block per input cursor plus the block being emitted.
return input_bytes > max_size - spill_buffer_bytes ? max_size
: input_bytes + spill_buffer_bytes;
}

} // namespace doris
4 changes: 4 additions & 0 deletions be/src/exec/operator/operator.h
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,10 @@ class DataSinkOperatorXBase : public OperatorBase {
[[nodiscard]] virtual size_t get_reserve_mem_size(RuntimeState* state, bool eos) {
return state->minimum_operator_memory_required_bytes();
}
[[nodiscard]] virtual size_t get_reserve_mem_size(RuntimeState* state, bool eos,
const Block* block) {
return get_reserve_mem_size(state, eos);
}
bool is_blockable(RuntimeState* state) const override {
return state->get_sink_local_state()->is_blockable();
}
Expand Down
98 changes: 70 additions & 28 deletions be/src/exec/operator/spill_iceberg_table_sink_operator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,35 @@
#include "exec/operator/spill_iceberg_table_sink_operator.h"

#include "common/status.h"
#include "core/block/block.h"
#include "exec/operator/iceberg_table_sink_operator.h"
#include "exec/sink/writer/iceberg/viceberg_sort_writer.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"

namespace doris {

size_t iceberg_cold_writer_reserve_size(const Block& block, size_t writer_workspace_bytes) {
const size_t block_bytes = block.allocated_bytes();
const size_t row_index_bytes =
std::min(std::numeric_limits<size_t>::max() / sizeof(size_t), block.rows()) *
sizeof(size_t);
const size_t dispatch_copies = block_bytes > std::numeric_limits<size_t>::max() / 4
? std::numeric_limits<size_t>::max()
: block_bytes * 4;
size_t reserve = iceberg_saturating_add(writer_workspace_bytes, dispatch_copies);
// A transform, selected blocks, and retained sorter copies can coexist; one extra block-sized
// allowance covers allocator fragmentation when a block is split into many tiny partitions.
return iceberg_saturating_add(reserve, row_index_bytes);
}

SpillIcebergTableSinkLocalState::SpillIcebergTableSinkLocalState(DataSinkOperatorXBase* parent,
RuntimeState* state)
: Base(parent, state) {}

Status SpillIcebergTableSinkLocalState::init(RuntimeState* state, LocalSinkStateInfo& info) {
RETURN_IF_ERROR(Base::init(state, info));
// Admission samples async sorter state, so the next block must wait until the prior append publishes it.
_writer->wait_for_processing_before_next_sink();
SCOPED_TIMER(exec_time_counter());
SCOPED_TIMER(_init_timer);

Expand All @@ -51,51 +68,75 @@ bool SpillIcebergTableSinkLocalState::is_blockable() const {
return true;
}

size_t SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState* state, bool eos) {
size_t SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState* state, bool eos,
const Block* block) {
if (!_writer) {
return 0;
}
auto current_writer = _writer->current_writer();
auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(current_writer.get());
if (!sort_writer) {
return 0;
std::vector<IcebergSorterReserveMemory> per_partition_reservations;
const size_t incoming_rows = block == nullptr ? 0 : block->rows();
const size_t incoming_bytes = block == nullptr ? 0 : block->allocated_bytes();
auto active_writers = _writer->active_writers();
Comment thread
Gabriel39 marked this conversation as resolved.
per_partition_reservations.reserve(active_writers->size());
for (const auto& writer : *active_writers) {
if (auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(writer.get())) {
auto reservation = sort_writer->get_reserve_mem_size_components(
state, eos, incoming_rows, incoming_bytes);
per_partition_reservations.push_back(
{.retained_growth = reservation.retained_growth,
.retained_growth_trigger_bytes = reservation.retained_growth_trigger_bytes,
.transient_workspace = reservation.transient_workspace});
}
}

return sort_writer->get_reserve_mem_size(state, eos);
// Column growth remains in every touched sorter, while sorting workspace is reused by serial dispatch.
// The final queued item may contain rows and also owns the reservation used by async finish().
Comment thread
Gabriel39 marked this conversation as resolved.
const size_t incoming_reserve =
block == nullptr ? state->minimum_operator_memory_required_bytes()
: iceberg_cold_writer_reserve_size(
*block, state->minimum_operator_memory_required_bytes());
return iceberg_reserve_size(per_partition_reservations, incoming_reserve, incoming_rows,
incoming_bytes);
}

size_t SpillIcebergTableSinkLocalState::get_revocable_mem_size(RuntimeState* state) const {
if (!_writer) {
return 0;
}
auto current_writer = _writer->current_writer();
auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(current_writer.get());
if (!sort_writer) {
return 0;
size_t revocable_size = 0;
// Retain the published container while the async writer may replace the current snapshot.
auto active_writers = _writer->active_writers();
for (const auto& writer : *active_writers) {
if (auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(writer.get())) {
revocable_size += sort_writer->data_size();
}
}

return sort_writer->data_size();
return revocable_size;
}

Status SpillIcebergTableSinkLocalState::revoke_memory(RuntimeState* state) {
RETURN_IF_CANCELLED(state);
if (!_writer) {
return Status::OK();
}
auto current_writer = _writer->current_writer();
auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(current_writer.get());
if (!sort_writer) {
return Status::OK();
std::shared_ptr<IPartitionWriterBase> largest_writer;
size_t largest_size = 0;
// Retain the published container while the async writer may replace the current snapshot.
auto active_writers = _writer->active_writers();
for (const auto& writer : *active_writers) {
if (auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(writer.get())) {
size_t size = sort_writer->data_size();
if (size > largest_size) {
largest_size = size;
largest_writer = writer;
}
}
}

auto exception_catch_func = [current_writer, sort_writer]() {
auto status = [&]() {
RETURN_IF_CATCH_EXCEPTION({ return sort_writer->trigger_spill(); });
}();
return status;
};

return exception_catch_func();
if (largest_writer != nullptr) {
// Repeated revocation drains the largest partition first without launching O(P) spill jobs at once.
auto* sort_writer = dynamic_cast<VIcebergSortWriter*>(largest_writer.get());
RETURN_IF_CATCH_EXCEPTION({ RETURN_IF_ERROR(sort_writer->trigger_spill()); });
}
return Status::OK();
}

SpillIcebergTableSinkOperatorX::SpillIcebergTableSinkOperatorX(
Expand Down Expand Up @@ -125,9 +166,10 @@ Status SpillIcebergTableSinkOperatorX::sink_impl(RuntimeState* state, Block* in_
return local_state.sink(state, in_block, eos);
}

size_t SpillIcebergTableSinkOperatorX::get_reserve_mem_size(RuntimeState* state, bool eos) {
size_t SpillIcebergTableSinkOperatorX::get_reserve_mem_size(RuntimeState* state, bool eos,
const Block* block) {
auto& local_state = get_local_state(state);
return local_state.get_reserve_mem_size(state, eos);
return local_state.get_reserve_mem_size(state, eos, block);
}

size_t SpillIcebergTableSinkOperatorX::revocable_mem_size(RuntimeState* state) const {
Expand Down
8 changes: 5 additions & 3 deletions be/src/exec/operator/spill_iceberg_table_sink_operator.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#pragma once

#include <memory>
#include <vector>

#include "exec/operator/iceberg_sorter_reserve_memory.h"
#include "exec/operator/operator.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"

Expand All @@ -41,7 +43,7 @@ class SpillIcebergTableSinkLocalState final
Status open(RuntimeState* state) override;

bool is_blockable() const override;
[[nodiscard]] size_t get_reserve_mem_size(RuntimeState* state, bool eos);
[[nodiscard]] size_t get_reserve_mem_size(RuntimeState* state, bool eos, const Block* block);
Status revoke_memory(RuntimeState* state);
size_t get_revocable_mem_size(RuntimeState* state) const;

Expand All @@ -65,7 +67,7 @@ class SpillIcebergTableSinkOperatorX final

Status sink_impl(RuntimeState* state, Block* in_block, bool eos) override;

size_t get_reserve_mem_size(RuntimeState* state, bool eos) override;
size_t get_reserve_mem_size(RuntimeState* state, bool eos, const Block* block) override;

size_t revocable_mem_size(RuntimeState* state) const override;

Expand All @@ -87,4 +89,4 @@ class SpillIcebergTableSinkOperatorX final
ObjectPool* _pool = nullptr;
};

} // namespace doris
} // namespace doris
Loading
Loading