Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
977a990
[fix](iceberg) Harden write lifecycle and cleanup
Gabriel39 Aug 1, 2026
597bb5d
[fix](iceberg) Address write safety review findings
Gabriel39 Aug 1, 2026
35f4c36
[fix](iceberg) Preserve Azure data and deduplicate orphan scans
Gabriel39 Aug 2, 2026
fe2d712
[fix](iceberg) Address follow-up write safety review
Gabriel39 Aug 2, 2026
17e683f
[fix](iceberg) Address multipart and snapshot review feedback
Gabriel39 Aug 2, 2026
846dbd1
[fix](iceberg) Address remaining write safety feedback
Gabriel39 Aug 2, 2026
a1e6863
[fix](iceberg) Match object store hash layout
Gabriel39 Aug 3, 2026
247b785
[fix](azure) Keep multipart block IDs rolling-compatible
Gabriel39 Aug 3, 2026
e45a499
[test](azure) Seed legacy residual block ID
Gabriel39 Aug 3, 2026
64372ea
[fix](azure) Fence multipart writers with blob leases
Gabriel39 Aug 3, 2026
f3b4f54
[fix](iceberg) Close async and rolling boundaries
Gabriel39 Aug 3, 2026
d2afe9b
[test](iceberg) Isolate async admission policy
Gabriel39 Aug 3, 2026
9da1347
[fix](iceberg) Close remaining write safety gaps
Gabriel39 Aug 3, 2026
185ab1d
[chore](be) Fix clang formatting
Gabriel39 Aug 3, 2026
36c03aa
[fix](iceberg) Close final write ownership gaps
Gabriel39 Aug 4, 2026
885e724
[fix](iceberg) Gate standalone deletes on report ACK
Gabriel39 Aug 4, 2026
4d00595
[fix](iceberg) Preserve external write ownership until final report
Gabriel39 Aug 4, 2026
082fb2a
[fix](iceberg) Avoid writer test suite collision
Gabriel39 Aug 5, 2026
ea3bd58
[test](be) Cover deferred upload report lifecycle
Gabriel39 Aug 5, 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
74 changes: 74 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,74 @@
// 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 transient_workspace = 0;
};

inline size_t bounded_iceberg_reserve_size(
const std::vector<IcebergSorterReserveMemory>& per_partition_reservations) {
size_t retained_growth = 0;
size_t transient_workspace = 0;
for (const auto& reservation : per_partition_reservations) {
retained_growth = std::min(std::numeric_limits<size_t>::max() - retained_growth,
reservation.retained_growth) +
retained_growth;
transient_workspace = std::max(transient_workspace, reservation.transient_workspace);
}
return std::min(std::numeric_limits<size_t>::max() - retained_growth, transient_workspace) +
retained_growth;
}

inline size_t iceberg_reserve_size(
const std::vector<IcebergSorterReserveMemory>& per_partition_reservations,
size_t incoming_block_bytes) {
size_t sorter_reserve = bounded_iceberg_reserve_size(per_partition_reservations);
// The incoming block creates cold partition writers before they can appear in the published snapshot.
return std::min(std::numeric_limits<size_t>::max() - sorter_reserve, incoming_block_bytes) +
sorter_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
93 changes: 65 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 selected_and_retained_bytes =
std::min(std::numeric_limits<size_t>::max() / 2, block_bytes) * 2;
size_t reserve = std::min(std::numeric_limits<size_t>::max() - writer_workspace_bytes,
selected_and_retained_bytes) +
writer_workspace_bytes;
// Cold dispatch may allocate a selected block and a retained sorter copy before publication.
return std::min(std::numeric_limits<size_t>::max() - reserve, row_index_bytes) + reserve;
}

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,70 @@ 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;
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);
Comment thread
Gabriel39 marked this conversation as resolved.
per_partition_reservations.push_back(
{.retained_growth = reservation.retained_growth,
.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);
}

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 +161,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