This is an automated email from the ASF dual-hosted git repository.
jacktengg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new d96a777a7fc [fix](be) Track MemTable row objects and sorting buffers
(#68399)
d96a777a7fc is described below
commit d96a777a7fc5c5ab5978a0b0d1bddb42f99949a0
Author: TengJianPing <[email protected]>
AuthorDate: Fri Oct 9 10:42:05 2026 +0800
[fix](be) Track MemTable row objects and sorting buffers (#68399)
### What problem does this PR solve?
Issue Number: N/A
Related PR: N/A
Problem Summary: MemTable tracks its shared_ptr element array through
DorisVector, but ordinary make_shared allocations for each RowInBlock
and its control block bypass Doris memory accounting. This
underestimates memory_usage() during writes and the additional row
references created for MOW Cluster Key sorting, affecting flush
thresholds and memory limits. Use allocate_shared with
CustomStdAllocator on both paths and track the Tie byte buffer with
DorisVector. Shared ownership and row/LSN ordering remain unchanged, and
deallocation balances accounting in the existing MemTable tracker
scopes, including aggregation and asynchronous cleanup.
### Release note
Correct MemTable memory accounting for row objects, shared_ptr control
blocks and sorting tie buffers so flush thresholds and memory limits
include these allocations.
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter='*MemTable*' -j 24: all 17 tests pass
under ASAN, including 7 new memory tracking cases.
- ./build.sh --be -j 24: ASAN BE build.
- clang-format 16, check-format.sh, check-build-hygiene.sh and git diff
--check pass.
- clang-tidy attempted; no diagnostics on changed lines. Its nonzero
exit is blocked by the pre-existing unmatched NOLINTEND in
be/src/core/types.h:576.
- Behavior changed: Yes, MemTable memory statistics now include the row
objects, control blocks and tie buffers used by flush and memory limits.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [x] 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 -->
---
be/src/load/memtable/memtable.cpp | 63 ++-
be/src/load/memtable/memtable.h | 8 +-
be/src/load/memtable/memtable_writer.cpp | 11 +-
.../memtable/memtable_memory_tracking_test.cpp | 568 +++++++++++++++++++++
4 files changed, 632 insertions(+), 18 deletions(-)
diff --git a/be/src/load/memtable/memtable.cpp
b/be/src/load/memtable/memtable.cpp
index ba950c06db7..53832b3f951 100644
--- a/be/src/load/memtable/memtable.cpp
+++ b/be/src/load/memtable/memtable.cpp
@@ -51,6 +51,21 @@ bvar::Adder<uint64_t>
g_flush_cuz_memtable_full("flush_cuz_memtable_full");
using namespace ErrorCode;
+namespace {
+
+// Share one tracked allocation across a batch. Allocating each row separately
also
+// retains an address record and a stack trace per row when memory diagnostics
are enabled.
+template <typename RowFactory>
+std::shared_ptr<RowInBlock[]> make_row_batch(size_t num_rows, RowFactory&&
make_row) {
+ auto rows =
std::allocate_shared<RowInBlock[]>(CustomStdAllocator<RowInBlock>(), num_rows);
+ for (size_t i = 0; i < num_rows; ++i) {
+ rows[i] = make_row(i);
+ }
+ return rows;
+}
+
+} // namespace
+
MemTable::MemTable(int64_t tablet_id, std::shared_ptr<TabletSchema>
tablet_schema,
const std::vector<SlotDescriptor*>* slot_descs,
TupleDescriptor* tuple_desc,
bool enable_unique_key_mow, PartialUpdateInfo*
partial_update_info,
@@ -274,9 +289,12 @@ Status MemTable::insert(const Block* input_block, const
TabletAddRowsPayload& ro
_input_mutable_block.get_column_by_position(_row_lsn_col_pos)
->replace_column_data_range(*lsn_column, 0, num_rows,
cursor_in_mutableblock);
}
- for (int i = 0; i < num_rows; i++) {
- _row_in_blocks->emplace_back(std::make_shared<RowInBlock>(
- cursor_in_mutableblock + i, _need_lsn ? allocated_lsns[i] :
0));
+ DBUG_EXECUTE_IF("MemTable.insert.row_batch_allocation",
DBUG_RUN_CALLBACK(this));
+ auto row_batch = make_row_batch(num_rows, [&](size_t i) {
+ return RowInBlock(cursor_in_mutableblock + i, _need_lsn ?
allocated_lsns[i] : 0);
+ });
+ for (size_t i = 0; i < num_rows; ++i) {
+ _row_in_blocks->emplace_back(row_batch, &row_batch[i]);
}
_stat.raw_rows += num_rows;
@@ -453,10 +471,11 @@ Status MemTable::_sort_by_cluster_keys() {
if (_need_lsn) {
DCHECK_EQ(_output_allocated_lsns->size(), mutable_block.rows());
}
- for (size_t i = 0; i < mutable_block.rows(); i++) {
- row_in_blocks.emplace_back(
- _need_lsn ? std::make_shared<RowInBlock>(i,
(*_output_allocated_lsns)[i])
- : std::make_shared<RowInBlock>(i));
+ auto rows = make_row_batch(mutable_block.rows(), [&](size_t i) {
+ return RowInBlock(i, _need_lsn ? (*_output_allocated_lsns)[i] : 0);
+ });
+ for (size_t i = 0; i < mutable_block.rows(); ++i) {
+ row_in_blocks.emplace_back(rows, &rows[i]);
}
if (_need_lsn) {
_output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
@@ -563,12 +582,26 @@ void MemTable::_finalize_one_row(RowInBlock* row,
MutableBlock& mutable_block, i
}
void MemTable::_init_row_for_agg(RowInBlock* row, MutableBlock& mutable_block)
{
- row->init_agg_places(_arena.aligned_alloc(_total_size_of_aggregate_states,
16),
- _offsets_of_aggregate_states.data());
- for (auto cid = _tablet_schema->num_key_columns(); cid < _num_columns;
cid++) {
+ auto* agg_mem = _arena.aligned_alloc(_total_size_of_aggregate_states, 16);
+ auto first_value_column = _tablet_schema->num_key_columns();
+ auto cid = first_value_column;
+ try {
+ for (; cid < _num_columns; ++cid) {
+ _agg_functions[cid]->create(agg_mem +
_offsets_of_aggregate_states[cid]);
+ }
+ } catch (...) {
+ // A failed constructor leaves only the completed prefix available for
destruction.
+ while (cid > first_value_column) {
+ --cid;
+ _agg_functions[cid]->destroy(agg_mem +
_offsets_of_aggregate_states[cid]);
+ }
+ throw;
+ }
+ // add() can allocate. Publish the row only after all states can be safely
destroyed.
+ row->init_agg_places(agg_mem, _offsets_of_aggregate_states.data());
+ for (cid = first_value_column; cid < _num_columns; ++cid) {
auto* col_ptr = mutable_block.mutable_columns()[cid].get();
auto* data = row->agg_places(cid);
- _agg_functions[cid]->create(data);
_agg_functions[cid]->add(data, const_cast<const
doris::IColumn**>(&col_ptr), row->_row_pos,
_arena);
}
@@ -650,6 +683,14 @@ void MemTable::_aggregate() {
_output_mutable_block =
MutableBlock::build_mutable_block(std::move(*empty_input_block));
_output_mutable_block.clear_column_data();
_output_allocated_lsns = std::make_shared<std::vector<int64_t>>();
+ // Repack surviving rows so they do not retain batches of merged-away
rows.
+ // Aggregation states belong to the memtable; preserve their pointers
and flags.
+ DBUG_EXECUTE_IF("MemTable.aggregate.row_batch_allocation",
DBUG_RUN_CALLBACK(this));
+ auto rows = make_row_batch(temp_row_in_blocks.size(),
+ [&](size_t i) { return
*temp_row_in_blocks[i]; });
+ for (size_t i = 0; i < temp_row_in_blocks.size(); ++i) {
+ temp_row_in_blocks[i] = {rows, &rows[i]};
+ }
*_row_in_blocks = temp_row_in_blocks;
_last_sorted_pos = _row_in_blocks->size();
}
diff --git a/be/src/load/memtable/memtable.h b/be/src/load/memtable/memtable.h
index 4228da6b928..0e72f1c5574 100644
--- a/be/src/load/memtable/memtable.h
+++ b/be/src/load/memtable/memtable.h
@@ -58,7 +58,7 @@ struct RowInBlock {
size_t* _agg_state_offset = nullptr;
bool _has_init_agg;
- RowInBlock(size_t row) : _row_pos(row), _has_init_agg(false) {}
+ RowInBlock(size_t row = 0) : _row_pos(row), _has_init_agg(false) {}
RowInBlock(size_t row, int64_t allocated_lsn)
: _row_pos(row), _allocated_lsn(allocated_lsn),
_has_init_agg(false) {}
@@ -120,9 +120,7 @@ public:
};
public:
- Tie(size_t begin, size_t end) : _begin(begin), _end(end) {
- _bits = std::vector<uint8_t>(_end - _begin, 1);
- }
+ Tie(size_t begin, size_t end) : _begin(begin), _end(end), _bits(end -
begin, 1) {}
uint8_t operator[](size_t i) const { return _bits[i - _begin]; }
uint8_t& operator[](size_t i) { return _bits[i - _begin]; }
Iter iter() { return Iter(*this); }
@@ -130,7 +128,7 @@ public:
private:
const size_t _begin;
const size_t _end;
- std::vector<uint8_t> _bits;
+ DorisVector<uint8_t> _bits;
};
class RowInBlockComparator {
diff --git a/be/src/load/memtable/memtable_writer.cpp
b/be/src/load/memtable/memtable_writer.cpp
index 63f399fba01..09ab384b847 100644
--- a/be/src/load/memtable/memtable_writer.cpp
+++ b/be/src/load/memtable/memtable_writer.cpp
@@ -165,7 +165,8 @@ Status MemTableWriter::write(const Block* block, const
TabletAddRowsPayload& row
}
_total_received_rows += rows.row_idxs.size();
- auto st = _mem_table->insert(block, rows);
+ Status st;
+ ASSIGN_STATUS_IF_CATCH_EXCEPTION(st = _mem_table->insert(block, rows), st);
// Reset memtable immediately after insert failure to prevent potential
flush operations.
// This is a defensive measure because:
@@ -185,7 +186,13 @@ Status MemTableWriter::write(const Block* block, const
TabletAddRowsPayload& row
}
if (UNLIKELY(_mem_table->need_agg() && config::enable_shrink_memory)) {
- _mem_table->shrink_memtable_by_agg();
+ // Aggregation can replace the columns before row metadata allocation
fails.
+ // Discard the failed memtable while _lock still excludes pressure
flushing.
+ ASSIGN_STATUS_IF_CATCH_EXCEPTION(_mem_table->shrink_memtable_by_agg(),
st);
+ if (!st.ok()) [[unlikely]] {
+ _reset_mem_table();
+ return st;
+ }
}
if (UNLIKELY(_mem_table->need_flush())) {
RETURN_IF_ERROR(_flush_memtable());
diff --git a/be/test/load/memtable/memtable_memory_tracking_test.cpp
b/be/test/load/memtable/memtable_memory_tracking_test.cpp
new file mode 100644
index 00000000000..95d5c5e0451
--- /dev/null
+++ b/be/test/load/memtable/memtable_memory_tracking_test.cpp
@@ -0,0 +1,568 @@
+// 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 <gtest/gtest.h>
+
+#include <functional>
+#include <limits>
+#include <string>
+#include <thread>
+
+#include "common/config.h"
+#include "exprs/aggregate/aggregate_function_min_max.h"
+#include "load/delta_writer/delta_writer_context.h"
+#include "load/memtable/memtable.h"
+#include "load/memtable/memtable_flush_executor.h"
+#include "load/memtable/memtable_writer.h"
+#include "runtime/workload_management/resource_context.h"
+#include "testutil/creators.h"
+#include "util/debug_points.h"
+#include "util/defer_op.h"
+
+namespace doris {
+
+class MemTableMemoryTrackingTest : public testing::TestWithParam<bool> {
+protected:
+ void SetUp() override {
+ _old_inaccurate_detect = config::crash_in_memory_tracker_inaccurate;
+ _old_stack_trace = config::enable_address_sanitizers_with_stack_trace;
+ config::crash_in_memory_tracker_inaccurate = true;
+ config::enable_address_sanitizers_with_stack_trace = true;
+ _thread_context = std::make_unique<ScopedInitThreadContext>();
+ auto resource_ctx = ResourceContext::create_shared();
+
resource_ctx->memory_context()->set_mem_tracker(MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::LOAD, "MemTableMemoryTrackingTest"));
+
+ TabletSchemaPB schema_pb;
+ schema_pb.set_keys_type(UNIQUE_KEYS);
+ testutil::add_column_pb(&schema_pb, 0, "k1", "INT", true, false);
+ testutil::add_column_pb(&schema_pb, 1, "k2", "INT", true, false);
+ testutil::add_column_pb(&schema_pb, 2, "v", "STRING", false, false)
+ ->set_aggregation("REPLACE");
+ schema_pb.add_cluster_key_uids(1);
+ auto schema = std::make_shared<TabletSchema>();
+ schema->init_from_pb(schema_pb);
+
+ auto tdesc = testutil::create_descriptor_table(
+ {{.type = TYPE_INT, .column_name = "k1", .nullable = false},
+ {.type = TYPE_INT, .column_name = "k2", .nullable = false},
+ {.type = TYPE_STRING, .column_name = "v", .nullable =
false}});
+ DescriptorTbl* desc_tbl = nullptr;
+ ASSERT_TRUE(DescriptorTbl::create(&_pool, tdesc, &desc_tbl).ok());
+ auto* tuple_desc = desc_tbl->get_tuple_descriptor(0);
+ _memtable = std::make_unique<MemTable>(1, schema,
&tuple_desc->slots(), tuple_desc, true,
+ nullptr, resource_ctx,
GetParam());
+ for (const auto* slot : tuple_desc->slots()) {
+
_input.insert(ColumnWithTypeAndName(slot->get_empty_mutable_column(),
slot->type(),
+ slot->col_name()));
+ }
+ auto columns = _input.mutate_columns_scoped();
+ for (uint32_t i = 0; i < NUM_ROWS; ++i) {
+ int32_t k1 = i;
+ int32_t k2 = NUM_ROWS - i;
+ columns.mutable_columns()[0]->insert_data(reinterpret_cast<const
char*>(&k1), 0);
+ columns.mutable_columns()[1]->insert_data(reinterpret_cast<const
char*>(&k2), 0);
+ auto value = std::string(64, 'v') + std::to_string(i);
+ columns.mutable_columns()[2]->insert_data(value.data(),
value.size());
+ _rows.row_idxs.push_back(i);
+ if (GetParam()) {
+ _rows.allocated_lsns.push_back(1000 + i);
+ }
+ }
+ }
+
+ void TearDown() override {
+ auto tracker = _memtable->mem_tracker();
+ auto write_tracker =
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker();
+ // Memtables can be destroyed by a flush worker instead of the
inserting thread.
+ std::thread destroyer([memtable = std::move(_memtable)]() mutable {
memtable.reset(); });
+ destroyer.join();
+ EXPECT_EQ(tracker->consumption(), 0);
+ EXPECT_TRUE(write_tracker->_address_sanitizers.empty());
+ EXPECT_TRUE(write_tracker->_error_address_sanitizers.empty());
+ config::crash_in_memory_tracker_inaccurate = _old_inaccurate_detect;
+ config::enable_address_sanitizers_with_stack_trace = _old_stack_trace;
+ }
+
+ void check_sorted_output(const IColumn& primary_key, const IColumn&
cluster_key) {
+ for (uint32_t i = 0; i < NUM_ROWS; ++i) {
+ EXPECT_EQ(primary_key.get_int(i), NUM_ROWS - 1 - i);
+ EXPECT_EQ(cluster_key.get_int(i), i + 1);
+ if (GetParam()) {
+ EXPECT_EQ((*_memtable->_output_allocated_lsns)[i], 1000 +
NUM_ROWS - 1 - i);
+ }
+ }
+ }
+
+ static constexpr uint32_t NUM_ROWS = 1024;
+ bool _old_inaccurate_detect = false;
+ bool _old_stack_trace = false;
+ std::unique_ptr<ScopedInitThreadContext> _thread_context;
+ ObjectPool _pool;
+ Block _input;
+ TabletAddRowsPayload _rows;
+ std::unique_ptr<MemTable> _memtable;
+};
+
+TEST_P(MemTableMemoryTrackingTest, BatchedAllocationDiagnostics) {
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ auto write_tracker =
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker();
+ size_t stack_trace_bytes = 0;
+ for (const auto& [address, allocation] :
write_tracker->_address_sanitizers) {
+ stack_trace_bytes += allocation.stack_trace.capacity();
+ }
+ RecordProperty("allocation_records",
write_tracker->_address_sanitizers.size());
+ RecordProperty("stack_trace_bytes", stack_trace_bytes);
+ // cloud_p0 records an address and a stack trace for every Doris
allocation.
+ // Row storage must use a bounded number of allocations for a batch of
1024 rows.
+ EXPECT_LT(write_tracker->_address_sanitizers.size(), 32);
+}
+
+TEST_P(MemTableMemoryTrackingTest, InsertAndReleaseRows) {
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ const auto num_rows = _memtable->_row_in_blocks->size();
+ ASSERT_EQ(num_rows, 2 * NUM_ROWS);
+ const auto column_and_reference_bytes =
+ _memtable->_input_mutable_block.allocated_bytes() +
+ _memtable->_row_in_blocks->capacity() *
sizeof(std::shared_ptr<RowInBlock>);
+ // Do not assume a standard library's control block layout or allocator
size class.
+ EXPECT_GT(_memtable->memory_usage(),
+ column_and_reference_bytes + num_rows * sizeof(RowInBlock));
+
+ const auto old_adaptive = config::enable_adaptive_write_buffer_size;
+ const auto old_buffer_size = config::write_buffer_size;
+ Defer restore_config {[&] {
+ config::enable_adaptive_write_buffer_size = old_adaptive;
+ config::write_buffer_size = old_buffer_size;
+ }};
+ config::enable_adaptive_write_buffer_size = false;
+ config::write_buffer_size = column_and_reference_bytes + num_rows *
sizeof(RowInBlock);
+ EXPECT_TRUE(_memtable->need_flush());
+
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
+ SCOPED_CONSUME_MEM_TRACKER(_memtable->mem_tracker());
+ auto retained_row = _memtable->_row_in_blocks->front();
+ std::weak_ptr<RowInBlock> weak_row = retained_row;
+ const auto before_clear = _memtable->memory_usage();
+ _memtable->_row_in_blocks->clear();
+ const auto after_clear = _memtable->memory_usage();
+ // The retained row keeps the first batch alive; the second batch is
released.
+ EXPECT_GT(before_clear - after_clear, NUM_ROWS * sizeof(RowInBlock));
+ EXPECT_FALSE(_memtable->need_flush());
+ EXPECT_EQ(retained_row->_row_pos, 0);
+ EXPECT_EQ(retained_row->_allocated_lsn, GetParam() ? 1000 : 0);
+ retained_row.reset();
+ EXPECT_TRUE(weak_row.expired());
+ // allocate_shared keeps the batch and control block allocation until the
+ // final weak reference disappears.
+ EXPECT_EQ(_memtable->memory_usage(), after_clear);
+ weak_row.reset();
+ EXPECT_GT(after_clear - _memtable->memory_usage(), NUM_ROWS *
sizeof(RowInBlock));
+}
+
+TEST_P(MemTableMemoryTrackingTest, ClusterKeySortMemory) {
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
+ SCOPED_CONSUME_MEM_TRACKER(_memtable->mem_tracker());
+ auto input = _memtable->_input_mutable_block.to_block();
+ ASSERT_TRUE(_memtable->_put_into_output(input).ok());
+ const auto before_sort = _memtable->memory_usage();
+ auto sort_tracker = std::make_shared<MemTracker>();
+ {
+ SCOPED_CONSUME_MEM_TRACKER(sort_tracker);
+ ASSERT_TRUE(_memtable->_sort_by_cluster_keys().ok());
+ }
+ // All row objects coexist during sorting, in addition to the reference
array.
+ EXPECT_GT(sort_tracker->peak_consumption(),
+ NUM_ROWS * (sizeof(RowInBlock) +
sizeof(std::shared_ptr<RowInBlock>)));
+ EXPECT_EQ(_memtable->memory_usage(), before_sort);
+ EXPECT_EQ(sort_tracker->consumption(), 0);
+
check_sorted_output(*_memtable->_output_mutable_block.get_column_by_position(0),
+
*_memtable->_output_mutable_block.get_column_by_position(1));
+ // A schema mismatch returns after allocating the temporary rows. Their
memory must
+ // still be released, together with the block that could not be sorted.
+ const auto output_bytes =
_memtable->_output_mutable_block.allocated_bytes();
+ _memtable->_tablet_schema->_cluster_key_uids = {999};
+ auto status = _memtable->_sort_by_cluster_keys();
+ EXPECT_FALSE(status.ok());
+ EXPECT_EQ(_memtable->memory_usage(), before_sort - output_bytes);
+}
+
+TEST_P(MemTableMemoryTrackingTest, AggregateAndFlush) {
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ for (int round = 0; round < 4; ++round) {
+ {
+ auto columns = _input.mutate_columns_scoped();
+ auto& value_column = columns.mutable_columns()[2];
+ value_column->clear();
+ for (uint32_t i = 0; i < NUM_ROWS; ++i) {
+ auto value = std::string(64, 'a' + round) + std::to_string(i);
+ value_column->insert_data(value.data(), value.size());
+ }
+ }
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
+ SCOPED_CONSUME_MEM_TRACKER(_memtable->mem_tracker());
+ std::weak_ptr<RowInBlock> previous_batch =
_memtable->_row_in_blocks->front();
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ std::weak_ptr<RowInBlock> inserted_batch =
_memtable->_row_in_blocks->back();
+ _memtable->shrink_memtable_by_agg();
+ EXPECT_EQ(_memtable->_row_in_blocks->size(), NUM_ROWS);
+ // Surviving rows must not pin batches containing merged-away rows.
+ EXPECT_TRUE(previous_batch.expired());
+ EXPECT_TRUE(inserted_batch.expired());
+ }
+ // Leave duplicate rows for the final aggregation during flush as well.
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
+ SCOPED_CONSUME_MEM_TRACKER(_memtable->mem_tracker());
+ std::unique_ptr<Block> output;
+ ASSERT_TRUE(_memtable->to_block(&output).ok());
+ ASSERT_EQ(output->rows(), NUM_ROWS);
+ check_sorted_output(*output->get_by_position(0).column,
*output->get_by_position(1).column);
+ for (uint32_t i = 0; i < NUM_ROWS; ++i) {
+
EXPECT_EQ(output->get_by_position(2).column->get_data_at(i).to_string(),
+ std::string(64, 'd') + std::to_string(NUM_ROWS - 1 - i));
+ }
+ _memtable->_is_flush_success = true;
+}
+
+TEST_P(MemTableMemoryTrackingTest, SingleRowBatches) {
+ _rows.row_idxs.resize(1);
+ if (GetParam()) {
+ _rows.allocated_lsns.resize(1);
+ }
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ ASSERT_TRUE(_memtable->insert(&_input, _rows).ok());
+ _memtable->shrink_memtable_by_agg();
+ ASSERT_EQ(_memtable->_row_in_blocks->size(), 1);
+
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(
+
_memtable->resource_ctx()->memory_context()->mem_tracker()->write_tracker());
+ SCOPED_CONSUME_MEM_TRACKER(_memtable->mem_tracker());
+ std::unique_ptr<Block> output;
+ ASSERT_TRUE(_memtable->to_block(&output).ok());
+ ASSERT_EQ(output->rows(), 1);
+ EXPECT_EQ(output->get_by_position(0).column->get_int(0), 0);
+ EXPECT_EQ(output->get_by_position(1).column->get_int(0), NUM_ROWS);
+ EXPECT_EQ(output->get_by_position(2).column->get_data_at(0).to_string(),
+ std::string(64, 'v') + "0");
+ if (GetParam()) {
+ EXPECT_EQ((*_memtable->_output_allocated_lsns)[0], 1000);
+ }
+ _memtable->_is_flush_success = true;
+}
+
+INSTANTIATE_TEST_SUITE_P(WithAndWithoutLsn, MemTableMemoryTrackingTest,
testing::Bool());
+
+TEST(MemTableMemoryTrackingAuxTest, TieMemoryTracking) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ auto tracker = std::make_shared<MemTracker>();
+ SCOPED_CONSUME_MEM_TRACKER(tracker);
+ {
+ Tie empty(10, 10);
+ EXPECT_EQ(tracker->consumption(), 0);
+ Tie tie(10, 1034);
+ EXPECT_GE(tracker->consumption(), 1024);
+ EXPECT_EQ(tie[10], 1);
+ EXPECT_EQ(tie[1033], 1);
+ }
+ EXPECT_EQ(tracker->consumption(), 0);
+}
+
+namespace {
+
+template <bool fail_create, bool fail_add>
+struct TestStringMinData : AggregateFunctionMinData<SingleValueDataString> {
+ using Base = AggregateFunctionMinData<SingleValueDataString>;
+ using Base::change_if_better;
+
+ static inline int created = 0;
+ static inline int destroyed = 0;
+ static inline int added = 0;
+
+ TestStringMinData() {
+ if constexpr (fail_create) {
+ throw Exception(ErrorCode::MEM_ALLOC_FAILED, "injected aggregate
constructor failure");
+ }
+ ++created;
+ }
+
+ ~TestStringMinData() { ++destroyed; }
+
+ void change_if_better(const IColumn& column, size_t row_num, Arena& arena)
{
+ ++added;
+ if constexpr (fail_add) {
+ const auto old_probability = config::mem_alloc_fault_probability;
+ Defer restore {[&]() { config::mem_alloc_fault_probability =
old_probability; }};
+ config::mem_alloc_fault_probability = 1.0;
+ // Exercise the native MIN state's allocation of its owning string
buffer.
+ Base::change_if_better(column, row_num, arena);
+ } else {
+ Base::change_if_better(column, row_num, arena);
+ }
+ }
+
+ static void reset_counts() { created = destroyed = added = 0; }
+};
+
+} // namespace
+
+class MemTableWriterFailureTest : public testing::Test {
+protected:
+ void SetUp() override {
+ _old_debug_points = config::enable_debug_points;
+ _old_shrink = config::enable_shrink_memory;
+ _old_adaptive = config::enable_adaptive_write_buffer_size;
+ _old_buffer_size = config::write_buffer_size;
+ _old_agg_buffer_size = config::write_buffer_size_for_agg;
+ config::enable_debug_points = true;
+ config::enable_shrink_memory = false;
+ config::enable_adaptive_write_buffer_size = false;
+ config::write_buffer_size = std::numeric_limits<int64_t>::max();
+ _thread_context = std::make_unique<ScopedInitThreadContext>();
+ }
+
+ void TearDown() override {
+
DebugPoints::instance()->remove("MemTable.insert.row_batch_allocation");
+
DebugPoints::instance()->remove("MemTable.aggregate.row_batch_allocation");
+ _writer.reset();
+ config::enable_debug_points = _old_debug_points;
+ config::enable_shrink_memory = _old_shrink;
+ config::enable_adaptive_write_buffer_size = _old_adaptive;
+ config::write_buffer_size = _old_buffer_size;
+ config::write_buffer_size_for_agg = _old_agg_buffer_size;
+ }
+
+ void init_writer(KeysType keys_type) {
+ TabletSchemaPB schema_pb;
+ schema_pb.set_keys_type(keys_type);
+ testutil::add_column_pb(&schema_pb, 0, "k", "INT", true, false);
+ auto* value = testutil::add_column_pb(&schema_pb, 1, "v", "INT",
false, false);
+ if (keys_type == AGG_KEYS) {
+ value->set_aggregation("SUM");
+ }
+ auto tdesc = testutil::create_descriptor_table(
+ {{.type = TYPE_INT, .column_name = "k", .nullable = false},
+ {.type = TYPE_INT, .column_name = "v", .nullable = false}});
+ init_writer(schema_pb, tdesc);
+ auto columns = _input.mutate_columns_scoped();
+ for (int32_t key : {2, 1, 1}) {
+ int32_t value = 10;
+ columns.mutable_columns()[0]->insert_data(reinterpret_cast<const
char*>(&key), 0);
+ columns.mutable_columns()[1]->insert_data(reinterpret_cast<const
char*>(&value), 0);
+ }
+ _rows.row_idxs = {0, 1, 2};
+ }
+
+ void init_writer(const TabletSchemaPB& schema_pb, const TDescriptorTable&
tdesc) {
+ auto schema = std::make_shared<TabletSchema>();
+ schema->init_from_pb(schema_pb);
+ DescriptorTbl* desc_tbl = nullptr;
+ ASSERT_TRUE(DescriptorTbl::create(&_pool, tdesc, &desc_tbl).ok());
+ auto* tuple_desc = desc_tbl->get_tuple_descriptor(0);
+ WriteRequest req;
+ req.tablet_id = 1;
+ req.tuple_desc = tuple_desc;
+ req.slots = &tuple_desc->slots();
+ _writer = std::make_unique<MemTableWriter>(req);
+ _writer->_tablet_schema = schema;
+ _writer->_resource_ctx = ResourceContext::create_shared();
+
_writer->_resource_ctx->memory_context()->set_mem_tracker(MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::LOAD, "MemTableWriterFailureTest"));
+ _writer->_reset_mem_table();
+ // Only empty replacement memtables reach this token, so no pool or
rowset is needed.
+ _writer->_flush_token = FlushToken::create_shared(nullptr, nullptr);
+ _writer->_is_init = true;
+ for (const auto* slot : tuple_desc->slots()) {
+
_input.insert(ColumnWithTypeAndName(slot->get_empty_mutable_column(),
slot->type(),
+ slot->col_name()));
+ }
+ }
+
+ void check_insert_failure_state(MemTable* memtable, size_t column_bytes,
size_t row_capacity) {
+ EXPECT_EQ(memtable->_input_mutable_block.rows(), 4);
+ // The appended columns and reference vector both fit their existing
capacity.
+ EXPECT_EQ(memtable->_input_mutable_block.allocated_bytes(),
column_bytes);
+ EXPECT_EQ(memtable->_row_in_blocks->size(), 3);
+ EXPECT_EQ(memtable->_row_in_blocks->capacity(), row_capacity);
+ }
+
+ void check_repack_failure_state(MemTable* memtable) {
+ EXPECT_EQ(memtable->_input_mutable_block.rows(), 2);
+ EXPECT_EQ(memtable->_row_in_blocks->size(), 4);
+ EXPECT_TRUE(memtable->_row_in_blocks->front()->has_init_agg());
+ }
+
+ void check_pressure_flush() {
+ EXPECT_TRUE(_writer->_mem_table->empty());
+ EXPECT_TRUE(_writer->_freezed_mem_tables.empty());
+ // A pressure flush after the failed write must only see the empty
replacement.
+ EXPECT_TRUE(_writer->flush_async().ok());
+
EXPECT_EQ(_writer->_flush_token->get_stats().flush_submit_count.load(), 0);
+
EXPECT_EQ(_writer->_flush_token->get_stats().flush_finish_count.load(), 0);
+ }
+
+ void init_string_writer() {
+ TabletSchemaPB schema_pb;
+ schema_pb.set_keys_type(AGG_KEYS);
+ testutil::add_column_pb(&schema_pb, 0, "k", "INT", true, false);
+ testutil::add_column_pb(&schema_pb, 1, "v1", "STRING", false, false)
+ ->set_aggregation("MIN");
+ testutil::add_column_pb(&schema_pb, 2, "v2", "STRING", false, false)
+ ->set_aggregation("MIN");
+ auto tdesc = testutil::create_descriptor_table(
+ {{.type = TYPE_INT, .column_name = "k", .nullable = false},
+ {.type = TYPE_STRING, .column_name = "v1", .nullable = false},
+ {.type = TYPE_STRING, .column_name = "v2", .nullable =
false}});
+ init_writer(schema_pb, tdesc);
+ auto columns = _input.mutate_columns_scoped();
+ int32_t key = 1;
+ columns.mutable_columns()[0]->insert_data(reinterpret_cast<const
char*>(&key), 0);
+ const std::string value(1024, 'v');
+ columns.mutable_columns()[1]->insert_data(value.data(), value.size());
+ columns.mutable_columns()[2]->insert_data(value.data(), value.size());
+ _rows.row_idxs = {0};
+ }
+
+ template <typename Data>
+ void replace_string_min(int cid) {
+ Data::reset_counts();
+ auto function = std::make_shared<AggregateFunctionsSingleValue<Data>>(
+ DataTypes {_input.get_by_position(cid).type});
+ const auto& original = _writer->_mem_table->_agg_functions[cid];
+ ASSERT_EQ(function->size_of_data(), original->size_of_data());
+ ASSERT_EQ(function->align_of_data(), original->align_of_data());
+ _writer->_mem_table->_agg_functions[cid] = std::move(function);
+ }
+
+ void check_aggregate_failure() {
+ std::weak_ptr<MemTable> failed_memtable = _writer->_mem_table;
+ auto tracker = _writer->_mem_table->mem_tracker();
+ // An ordinary add-block worker enters write() without allocation
exception handling.
+ ASSERT_EQ(enable_thread_catch_bad_alloc, 0);
+ config::enable_shrink_memory = true;
+ config::write_buffer_size_for_agg = 0;
+ Status status;
+ ASSERT_NO_THROW(status = _writer->write(&_input, _rows));
+ EXPECT_EQ(status.code(), ErrorCode::MEM_LIMIT_EXCEEDED);
+ EXPECT_EQ(enable_thread_catch_bad_alloc, 0);
+ ASSERT_TRUE(failed_memtable.expired());
+ EXPECT_EQ(tracker->consumption(), 0);
+ check_pressure_flush();
+ }
+
+ void check_failed_write(const std::string& point, bool during_insert, int
error_code) {
+ std::weak_ptr<MemTable> failed_memtable = _writer->_mem_table;
+ auto tracker = _writer->_mem_table->mem_tracker();
+ const auto column_bytes =
_writer->_mem_table->_input_mutable_block.allocated_bytes();
+ const auto row_capacity =
_writer->_mem_table->_row_in_blocks->capacity();
+ bool reached_allocation = false;
+ std::function<void(MemTable*)> fail_allocation = [&](MemTable*
memtable) {
+ reached_allocation = true;
+ if (during_insert) {
+ check_insert_failure_state(memtable, column_bytes,
row_capacity);
+ } else {
+ check_repack_failure_state(memtable);
+ }
+ throw Exception(error_code, "injected row allocation failure");
+ };
+ DebugPoints::instance()->add_with_callback(point, fail_allocation);
+ _rows.row_idxs.resize(1);
+ Status status;
+ ASSERT_NO_THROW(status = _writer->write(&_input, _rows));
+ EXPECT_TRUE(reached_allocation);
+ EXPECT_EQ(status.code(), error_code == ErrorCode::MEM_ALLOC_FAILED
+ ? ErrorCode::MEM_LIMIT_EXCEEDED
+ : error_code);
+ ASSERT_TRUE(failed_memtable.expired());
+ EXPECT_EQ(tracker->consumption(), 0);
+ check_pressure_flush();
+ }
+
+ bool _old_debug_points = false;
+ bool _old_shrink = false;
+ bool _old_adaptive = false;
+ int64_t _old_buffer_size = 0;
+ int64_t _old_agg_buffer_size = 0;
+ std::unique_ptr<ScopedInitThreadContext> _thread_context;
+ ObjectPool _pool;
+ Block _input;
+ TabletAddRowsPayload _rows;
+ std::unique_ptr<MemTableWriter> _writer;
+};
+
+class MemTableWriterAllocationFailureTest : public MemTableWriterFailureTest,
+ public
testing::WithParamInterface<int> {};
+
+TEST_P(MemTableWriterAllocationFailureTest,
InsertFailureDiscardsMemTableBeforePressureFlush) {
+ init_writer(DUP_KEYS);
+ ASSERT_TRUE(_writer->write(&_input, _rows).ok());
+ check_failed_write("MemTable.insert.row_batch_allocation", true,
GetParam());
+}
+
+TEST_P(MemTableWriterAllocationFailureTest,
RepackFailureDiscardsMemTableBeforePressureFlush) {
+ init_writer(AGG_KEYS);
+ ASSERT_TRUE(_writer->write(&_input, _rows).ok());
+ config::enable_shrink_memory = true;
+ config::write_buffer_size_for_agg = 0;
+ check_failed_write("MemTable.aggregate.row_batch_allocation", false,
GetParam());
+}
+
+INSTANTIATE_TEST_SUITE_P(MemoryAndOtherErrors,
MemTableWriterAllocationFailureTest,
+ testing::Values(ErrorCode::MEM_ALLOC_FAILED,
ErrorCode::INTERNAL_ERROR));
+
+TEST_F(MemTableWriterFailureTest,
FirstStringAddFailureDestroysAllConstructedStates) {
+ using First = TestStringMinData<false, true>;
+ using Second = TestStringMinData<false, false>;
+ init_string_writer();
+ ASSERT_TRUE(_writer->write(&_input, _rows).ok());
+ replace_string_min<First>(1);
+ replace_string_min<Second>(2);
+ check_aggregate_failure();
+ EXPECT_EQ(First::created, 1);
+ EXPECT_EQ(First::destroyed, 1);
+ EXPECT_EQ(First::added, 1);
+ EXPECT_EQ(Second::created, 1);
+ EXPECT_EQ(Second::destroyed, 1);
+ EXPECT_EQ(Second::added, 0);
+}
+
+TEST_F(MemTableWriterFailureTest,
SecondStateCreateFailureDestroysOnlyCompletedPrefix) {
+ using First = TestStringMinData<false, false>;
+ using Second = TestStringMinData<true, false>;
+ init_string_writer();
+ ASSERT_TRUE(_writer->write(&_input, _rows).ok());
+ replace_string_min<First>(1);
+ replace_string_min<Second>(2);
+ check_aggregate_failure();
+ EXPECT_EQ(First::created, 1);
+ EXPECT_EQ(First::destroyed, 1);
+ EXPECT_EQ(First::added, 0);
+ EXPECT_EQ(Second::created, 0);
+ EXPECT_EQ(Second::destroyed, 0);
+ EXPECT_EQ(Second::added, 0);
+}
+
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]