This is an automated email from the ASF dual-hosted git repository.
zclllyybb 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 8be719da4fa [fix](be) Publish nullable hash state after creation
(#66102)
8be719da4fa is described below
commit 8be719da4fa8eaf17a40a059fdcfa984bc152abd
Author: HappenLee <[email protected]>
AuthorDate: Thu Jul 30 12:30:58 2026 +0800
[fix](be) Publish nullable hash state after creation (#66102)
Nullable aggregation hash tables marked the null-key entry as present
before its aggregate-state creator completed. If allocation or
aggregate-state construction threw, cleanup treated uninitialized
null-key storage as a valid state and could crash while destroying it.
Publishing the flag only after the creator returns fixes that case, but
nullable LIMIT creators perform additional potentially throwing work
after aggregate-state construction. If top-N column insertion or heap
maintenance throws, the flag remains unpublished while a live
resource-owning aggregate state would otherwise be left behind.
This change preserves delayed flag publication and gives the LIMIT
null-key creators a strong exception guarantee: construct into a local
pointer, perform post-construction LIMIT maintenance, destroy the
completed state if that work throws, and publish the mapped pointer only
on success. Null-key storage is value-initialized, and the same
publication ordering is applied to StringHashMap batch paths. Unit tests
cover immediate creator failure, construction followed by post-create
failure and rollback, and successful retry for ordinary nullable and
StringHashMap paths.
---
be/src/exec/common/agg_utils.h | 13 ++
be/src/exec/common/columns_hashing.h | 9 +-
be/src/exec/common/hash_table/hash_map_context.h | 12 +-
be/src/exec/operator/aggregation_sink_operator.cpp | 20 ++-
.../operator/streaming_aggregation_operator.cpp | 9 +-
be/test/exec/hash_map/hash_table_method_test.cpp | 141 +++++++++++++++++++++
6 files changed, 182 insertions(+), 22 deletions(-)
diff --git a/be/src/exec/common/agg_utils.h b/be/src/exec/common/agg_utils.h
index b613a68a5ab..6c89684d436 100644
--- a/be/src/exec/common/agg_utils.h
+++ b/be/src/exec/common/agg_utils.h
@@ -17,6 +17,7 @@
#pragma once
+#include <utility>
#include <variant>
#include <vector>
@@ -28,6 +29,18 @@
namespace doris {
+template <typename PostCreate, typename Rollback>
+void commit_aggregate_state(AggregateDataPtr& mapped, AggregateDataPtr
new_state,
+ PostCreate&& post_create, Rollback&& rollback) {
+ try {
+ std::forward<PostCreate>(post_create)();
+ } catch (...) {
+ std::forward<Rollback>(rollback)(new_state);
+ throw;
+ }
+ mapped = new_state;
+}
+
template <typename T>
using AggData = PHHashMap<T, AggregateDataPtr, HashCRC32<T>>;
template <typename T>
diff --git a/be/src/exec/common/columns_hashing.h
b/be/src/exec/common/columns_hashing.h
index ee6a76445df..dc346097dc3 100644
--- a/be/src/exec/common/columns_hashing.h
+++ b/be/src/exec/common/columns_hashing.h
@@ -128,18 +128,17 @@ struct HashMethodSingleLowNullableColumn : public
SingleColumnMethod {
size_t hash_value, Func&& f,
CreatorForNull&& null_creator) {
if (key_column->is_null_at(row)) {
- bool has_null_key = data.has_null_key_data();
- data.has_null_key_data() = true;
-
if constexpr (std::is_same_v<Mapped, void>) {
- if (!has_null_key) {
+ if (!data.has_null_key_data()) {
std::forward<CreatorForNull>(null_creator)();
+ data.has_null_key_data() = true;
}
return nullptr;
} else {
- if (!has_null_key) {
+ if (!data.has_null_key_data()) {
std::forward<CreatorForNull>(null_creator)(
data.template get_null_key_data<Mapped>());
+ data.has_null_key_data() = true;
}
return &data.template get_null_key_data<Mapped>();
}
diff --git a/be/src/exec/common/hash_table/hash_map_context.h
b/be/src/exec/common/hash_table/hash_map_context.h
index 804d2e48020..fb8184f9fea 100644
--- a/be/src/exec/common/hash_table/hash_map_context.h
+++ b/be/src/exec/common/hash_table/hash_map_context.h
@@ -506,11 +506,10 @@ void process_submap_emplace(Submap& submap, const
uint32_t* indices, size_t coun
pre_handler(row);
if constexpr (is_nullable) {
if (state.key_column->is_null_at(row)) {
- bool has_null_key = hash_table.has_null_key_data();
- hash_table.has_null_key_data() = true;
- if (!has_null_key) {
+ if (!hash_table.has_null_key_data()) {
std::forward<FF>(creator_for_null_key)(
hash_table.template get_null_key_data<Mapped>());
+ hash_table.has_null_key_data() = true;
}
result_handler(row, hash_table.template
get_null_key_data<Mapped>());
continue;
@@ -552,10 +551,9 @@ void process_submap_emplace_void(Submap& submap, const
uint32_t* indices, size_t
pre_handler(row);
if constexpr (is_nullable) {
if (state.key_column->is_null_at(row)) {
- bool has_null_key = hash_table.has_null_key_data();
- hash_table.has_null_key_data() = true;
- if (!has_null_key) {
+ if (!hash_table.has_null_key_data()) {
std::forward<FF>(creator_for_null_key)();
+ hash_table.has_null_key_data() = true;
}
continue;
}
@@ -1164,7 +1162,7 @@ struct DataWithNullKey : public Base {
protected:
bool has_null_key = false;
- Base::Value null_key_data;
+ Base::Value null_key_data {};
};
/// Single low cardinality column.
diff --git a/be/src/exec/operator/aggregation_sink_operator.cpp
b/be/src/exec/operator/aggregation_sink_operator.cpp
index c77563e9751..f4e74be9995 100644
--- a/be/src/exec/operator/aggregation_sink_operator.cpp
+++ b/be/src/exec/operator/aggregation_sink_operator.cpp
@@ -749,16 +749,22 @@ bool
AggSinkLocalState::_emplace_into_hash_table_limit(AggregateDataPtr* places,
};
auto creator_for_null_key = [&](auto& mapped) {
- mapped =
Base::_shared_state->agg_arena_pool.aligned_alloc(
- Base::_parent->template
cast<AggSinkOperatorX>()
-
._total_size_of_aggregate_states,
- Base::_parent->template
cast<AggSinkOperatorX>()
- ._align_aggregate_states);
- auto st = _create_agg_status(mapped);
+ auto* new_state =
+
Base::_shared_state->agg_arena_pool.aligned_alloc(
+ Base::_parent->template
cast<AggSinkOperatorX>()
+
._total_size_of_aggregate_states,
+ Base::_parent->template
cast<AggSinkOperatorX>()
+
._align_aggregate_states);
+ auto st = _create_agg_status(new_state);
if (!st) {
throw Exception(st.code(),
st.to_string());
}
- _shared_state->refresh_top_limit(i,
key_columns);
+ commit_aggregate_state(
+ mapped, new_state,
+ [&] {
_shared_state->refresh_top_limit(i, key_columns); },
+ [&](auto* state) {
+
static_cast<void>(_destroy_agg_status(state));
+ });
};
SCOPED_TIMER(_hash_table_emplace_timer);
diff --git a/be/src/exec/operator/streaming_aggregation_operator.cpp
b/be/src/exec/operator/streaming_aggregation_operator.cpp
index 28fc383bace..31ae0d843e4 100644
--- a/be/src/exec/operator/streaming_aggregation_operator.cpp
+++ b/be/src/exec/operator/streaming_aggregation_operator.cpp
@@ -763,16 +763,19 @@ bool
StreamingAggLocalState::_emplace_into_hash_table_limit(AggregateDataPtr* pl
};
auto creator_for_null_key = [&](auto& mapped) {
- mapped = _agg_arena_pool.aligned_alloc(
+ auto* new_state =
_agg_arena_pool.aligned_alloc(
Base::_parent->template
cast<StreamingAggOperatorX>()
._total_size_of_aggregate_states,
Base::_parent->template
cast<StreamingAggOperatorX>()
._align_aggregate_states);
- auto st = _create_agg_status(mapped);
+ auto st = _create_agg_status(new_state);
if (!st) {
throw Exception(st.code(),
st.to_string());
}
- _refresh_limit_heap(i, key_columns);
+ commit_aggregate_state(
+ mapped, new_state,
+ [&] { _refresh_limit_heap(i,
key_columns); },
+ [&](auto* state) {
_destroy_agg_status(state); });
};
SCOPED_TIMER(_hash_table_emplace_timer);
diff --git a/be/test/exec/hash_map/hash_table_method_test.cpp
b/be/test/exec/hash_map/hash_table_method_test.cpp
index de2f0a7ac63..0fcab89040d 100644
--- a/be/test/exec/hash_map/hash_table_method_test.cpp
+++ b/be/test/exec/hash_map/hash_table_method_test.cpp
@@ -17,6 +17,7 @@
#include <gtest/gtest.h>
+#include <new>
#include <set>
#include "core/data_type/data_type_number.h"
@@ -25,6 +26,7 @@
#include "exec/common/hash_table/hash.h"
#include "exec/common/hash_table/hash_map_context.h"
#include "exec/common/hash_table/ph_hash_map.h"
+#include "exec/common/hash_table/ph_hash_set.h"
#include "testutil/column_helper.h"
namespace doris {
@@ -254,6 +256,145 @@ static AggregateDataPtr make_mapped(size_t val) {
return reinterpret_cast<AggregateDataPtr>(val);
}
+struct TrackedNullAggregateState {
+ explicit TrackedNullAggregateState(size_t& destroy_count_) :
destroy_count(destroy_count_) {}
+ ~TrackedNullAggregateState() { ++destroy_count; }
+
+ size_t& destroy_count;
+};
+
+static void create_null_state_then_fail(AggregateDataPtr& mapped, void*
storage,
+ size_t& destroy_count) {
+ auto* new_state = new (storage) TrackedNullAggregateState(destroy_count);
+ commit_aggregate_state(
+ mapped, reinterpret_cast<AggregateDataPtr>(new_state),
+ [] {
+ throw Exception(ErrorCode::INTERNAL_ERROR,
+ "post-construction null key creation failed");
+ },
+ [](AggregateDataPtr state) {
+
reinterpret_cast<TrackedNullAggregateState*>(state)->~TrackedNullAggregateState();
+ });
+}
+
+TEST(HashTableMethodTest, testNullableNullKeyCreationExceptionSafety) {
+ using NullableMethod =
+ MethodSingleNullableColumn<MethodOneNumber<UInt32,
AggDataNullable<UInt32>>>;
+ NullableMethod method;
+ using State = NullableMethod::State;
+
+ auto col = ColumnHelper::create_nullable_column<DataTypeInt32>({0}, {1});
+ ColumnRawPtrs key_columns = {col.get()};
+ State state(key_columns);
+ method.init_serialized_keys(key_columns, 1);
+
+ EXPECT_THROW(method.lazy_emplace(
+ state, 0, [](const auto&, auto&, auto&) {},
+ [](auto&) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "null
key creation failed");
+ }),
+ Exception);
+ EXPECT_FALSE(method.hash_table->has_null_key_data());
+ EXPECT_TRUE(method.hash_table->empty());
+ EXPECT_FALSE(method.find(state, 0).is_found());
+
+ alignas(TrackedNullAggregateState) char
state_storage[sizeof(TrackedNullAggregateState)];
+ size_t destroy_count = 0;
+ EXPECT_THROW(method.lazy_emplace(
+ state, 0, [](const auto&, auto&, auto&) {},
+ [&](auto& null_mapped) {
+ create_null_state_then_fail(null_mapped,
state_storage, destroy_count);
+ }),
+ Exception);
+ EXPECT_EQ(destroy_count, 1);
+ EXPECT_FALSE(method.hash_table->has_null_key_data());
+ EXPECT_EQ(method.hash_table->get_null_key_data<AggregateDataPtr>(),
nullptr);
+ EXPECT_TRUE(method.hash_table->empty());
+ EXPECT_FALSE(method.find(state, 0).is_found());
+
+ auto* mapped = method.lazy_emplace(
+ state, 0, [](const auto&, auto&, auto&) {},
+ [](auto& null_mapped) { null_mapped = make_mapped(123); });
+ ASSERT_NE(mapped, nullptr);
+ EXPECT_EQ(*mapped, make_mapped(123));
+ EXPECT_TRUE(method.hash_table->has_null_key_data());
+ EXPECT_EQ(method.hash_table->size(), 1);
+}
+
+TEST(HashTableMethodTest, testNullableVoidNullKeyCreationExceptionSafety) {
+ using NullableMethod = MethodSingleNullableColumn<
+ MethodOneNumber<UInt32, DataWithNullKey<PHHashSet<UInt32,
HashCRC32<UInt32>>>>>;
+ NullableMethod method;
+ using State = NullableMethod::State;
+
+ auto col = ColumnHelper::create_nullable_column<DataTypeInt32>({0}, {1});
+ ColumnRawPtrs key_columns = {col.get()};
+ State state(key_columns);
+ method.init_serialized_keys(key_columns, 1);
+
+ EXPECT_THROW(
+ method.lazy_emplace(
+ state, 0, [](const auto&, auto&, auto&) {},
+ [] { throw Exception(ErrorCode::INTERNAL_ERROR, "null key
creation failed"); }),
+ Exception);
+ EXPECT_FALSE(method.hash_table->has_null_key_data());
+ EXPECT_TRUE(method.hash_table->empty());
+
+ method.lazy_emplace(
+ state, 0, [](const auto&, auto&, auto&) {}, [] {});
+ EXPECT_TRUE(method.hash_table->has_null_key_data());
+ EXPECT_EQ(method.hash_table->size(), 1);
+}
+
+TEST(HashTableMethodTest,
testNullableStringBatchNullKeyCreationExceptionSafety) {
+ using NullableMethod = MethodSingleNullableColumn<
+ MethodStringNoCache<AggregatedDataWithNullableShortStringKey>>;
+ NullableMethod method;
+ using State = NullableMethod::State;
+
+ auto col = ColumnHelper::create_nullable_column<DataTypeString>({""}, {1});
+ ColumnRawPtrs key_columns = {col.get()};
+ State state(key_columns);
+ method.init_serialized_keys(key_columns, 1);
+
+ EXPECT_THROW(lazy_emplace_batch(
+ method, state, 1, [](const auto&, auto&, auto&) {},
+ [](auto&) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "null
key creation failed");
+ },
+ [](uint32_t, auto&) {}),
+ Exception);
+ EXPECT_FALSE(method.hash_table->has_null_key_data());
+ EXPECT_TRUE(method.hash_table->empty());
+
+ alignas(TrackedNullAggregateState) char
state_storage[sizeof(TrackedNullAggregateState)];
+ size_t destroy_count = 0;
+ EXPECT_THROW(lazy_emplace_batch(
+ method, state, 1, [](const auto&, auto&, auto&) {},
+ [&](auto& null_mapped) {
+ create_null_state_then_fail(null_mapped,
state_storage, destroy_count);
+ },
+ [](uint32_t, auto&) {}),
+ Exception);
+ EXPECT_EQ(destroy_count, 1);
+ EXPECT_FALSE(method.hash_table->has_null_key_data());
+ EXPECT_EQ(method.hash_table->get_null_key_data<AggregateDataPtr>(),
nullptr);
+ EXPECT_TRUE(method.hash_table->empty());
+
+ bool result_handled = false;
+ lazy_emplace_batch(
+ method, state, 1, [](const auto&, auto&, auto&) {},
+ [](auto& null_mapped) { null_mapped = make_mapped(456); },
+ [&](uint32_t row, auto& mapped) {
+ EXPECT_EQ(row, 0);
+ EXPECT_EQ(mapped, make_mapped(456));
+ result_handled = true;
+ });
+ EXPECT_TRUE(result_handled);
+ EXPECT_TRUE(method.hash_table->has_null_key_data());
+ EXPECT_EQ(method.hash_table->size(), 1);
+}
+
// ========== MethodOneNumber<UInt32, AggData<UInt32>> ==========
// AggData<UInt32> = PHHashMap<UInt32, AggregateDataPtr, HashCRC32<UInt32>>
TEST(HashTableMethodTest, testMethodOneNumberAggInsertFindForEach) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]