This is an automated email from the ASF dual-hosted git repository.

eldenmoon 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 0c78027c868 [refactor](variant) Make Variant metadata key registration 
allocation-safe (#70487)
0c78027c868 is described below

commit 0c78027c8688456a2ff6b02ab7336b3a53856316
Author: lihangyu <[email protected]>
AuthorDate: Thu Nov 12 22:17:01 2026 -0800

    [refactor](variant) Make Variant metadata key registration allocation-safe 
(#70487)
---
 .../core/value/variant/variant_batch_builder.cpp   | 118 +++++++---
 .../segment/variant/v2/variant_path_builder.cpp    |  20 +-
 .../variant/variant_column_writer_reader_test.cpp  |  31 +++
 .../util/variant/variant_batch_builder_test.cpp    | 258 +++++++++++++++++++++
 4 files changed, 391 insertions(+), 36 deletions(-)

diff --git a/be/src/core/value/variant/variant_batch_builder.cpp 
b/be/src/core/value/variant/variant_batch_builder.cpp
index 6b2e77484e2..d3bc1e1c65f 100644
--- a/be/src/core/value/variant/variant_batch_builder.cpp
+++ b/be/src/core/value/variant/variant_batch_builder.cpp
@@ -22,11 +22,11 @@
 #include <algorithm>
 #include <cstdint>
 #include <cstring>
-#include <deque>
 #include <functional>
 #include <limits>
 #include <ranges>
 #include <string>
+#include <type_traits>
 #include <utility>
 
 #include "common/exception.h"
@@ -36,6 +36,10 @@
 #include "core/value/variant/variant_parquet_encoding.h"
 #include "util/utf8_check.h"
 
+#ifdef BE_TEST
+#include "util/debug_points.h"
+#endif
+
 namespace doris {
 namespace {
 
@@ -130,9 +134,30 @@ void append_unsigned(String& output, uint64_t value, 
uint8_t width) {
     }
 }
 
-template <typename Container>
-void release_batch_container(Container& container) noexcept {
-    Container().swap(container);
+template <typename T>
+void release_batch_vector(DorisVector<T>& buffer) noexcept {
+    static_assert(noexcept(DorisVector<T>()));
+    static_assert(noexcept(std::declval<DorisVector<T>&>().swap(buffer)));
+    DorisVector<T>().swap(buffer);
+}
+
+using MetadataKeyBytes = DorisVector<char>;
+using MetadataKeys = DorisVector<MetadataKeyBytes>;
+
+// A key map entry points at the bytes owned by one MetadataKeyBytes. Growing 
MetadataKeys moves
+// the inner vectors, so that move must only transfer the byte buffer. For 
example, reallocating
+// 1024 keys to 2048 entries must not copy any key bytes or leave a map entry 
pointing at freed
+// storage.
+static_assert(std::is_nothrow_move_constructible_v<MetadataKeyBytes>,
+              "Variant metadata key bytes must move without allocating");
+
+// An empty key owns no bytes, but its map entry still needs a stable, 
non-null address for both
+// hashing and comparison. One sentinel byte serves every empty key.
+constexpr char EMPTY_METADATA_KEY = '\0';
+
+StringRef metadata_key_ref(const MetadataKeyBytes& bytes) noexcept {
+    DCHECK(bytes.empty() || bytes.data() != nullptr);
+    return {bytes.empty() ? &EMPTY_METADATA_KEY : bytes.data(), bytes.size()};
 }
 
 } // namespace
@@ -141,9 +166,13 @@ struct VariantMetadataBuilder::Impl {
     using KeyMapValue = std::pair<const StringRef, uint32_t>;
     using KeyMap = phmap::flat_hash_map<StringRef, uint32_t, StringRefHash, 
std::equal_to<>,
                                         CustomStdAllocator<KeyMapValue>>;
-    using Keys = std::deque<PaddedPODArray<char>, 
CustomStdAllocator<PaddedPODArray<char>>>;
+    // register_key() publishes a map entry before appending the key, so a 
failed map allocation
+    // must not leave a half-inserted entry behind. phmap only moves entries 
after allocating a
+    // larger table, and moving this value type cannot throw, so a rejected 
growth leaves the map
+    // unchanged.
+    static_assert(std::is_nothrow_move_constructible_v<KeyMapValue>);
 
-    Keys keys;
+    MetadataKeys keys;
     KeyMap key_to_temporary_id;
     DorisVector<uint32_t> temporary_to_final_id;
     PaddedPODArray<char> encoded;
@@ -177,16 +206,33 @@ uint32_t VariantMetadataBuilder::register_key(StringRef 
key) {
     }
 
     const auto temporary_id = static_cast<uint32_t>(_impl->keys.size());
-    _impl->keys.emplace_back();
+
+    // Prepare every fallible step first: copy the bytes into a local owner 
and make room in the
+    // outer key vector. Only then publish the map entry and append the owner, 
so a rejected
+    // allocation leaves both containers untouched instead of needing a catch 
block.
+    MetadataKeyBytes owned_key;
     if (key.size != 0) {
-        _impl->keys.back().insert(key.data, key.data + key.size);
+        owned_key.insert(owned_key.end(), key.data, key.data + key.size);
+    }
+    // reserve() allocates exactly the requested capacity, so reserving size+1 
every time would
+    // reallocate and move all previous keys on every registration. Grow 
geometrically only when
+    // the vector is full; the append below then needs no allocation.
+    if (_impl->keys.size() == _impl->keys.capacity()) {
+        _impl->keys.reserve(_impl->keys.capacity() == 0 ? 1 : 
_impl->keys.capacity() * 2);
     }
-    const StringRef owned_key {_impl->keys.back().data(), 
_impl->keys.back().size()};
+
     const size_t old_capacity = _impl->key_to_temporary_id.capacity();
-    const auto [unused, inserted] = 
_impl->key_to_temporary_id.emplace(owned_key, temporary_id);
-    _impl->key_capacity_growths += old_capacity != 
_impl->key_to_temporary_id.capacity();
+    // The entry is visible while `owned_key` still owns the bytes; the move 
below transfers that
+    // same buffer into `keys`, so the stored StringRef stays valid.
+    const auto [unused, inserted] =
+            _impl->key_to_temporary_id.emplace(metadata_key_ref(owned_key), 
temporary_id);
     static_cast<void>(unused);
     DCHECK(inserted);
+
+    // Commit: the outer vector has capacity for one more key and the move 
only transfers the byte
+    // buffer, so nothing below this point can allocate or throw.
+    _impl->keys.emplace_back(std::move(owned_key));
+    _impl->key_capacity_growths += old_capacity != 
_impl->key_to_temporary_id.capacity();
     return temporary_id;
 }
 
@@ -211,8 +257,9 @@ void VariantMetadataBuilder::_abort_row() noexcept {
     DCHECK(_impl->row_active);
     DCHECK_LE(_impl->row_start_key_count, _impl->keys.size());
     while (_impl->keys.size() > _impl->row_start_key_count) {
-        const StringRef key {_impl->keys.back().data(), 
_impl->keys.back().size()};
-        const size_t erased = _impl->key_to_temporary_id.erase(key);
+        // Erase the map entry before releasing the bytes it points at; 
neither step allocates.
+        const size_t erased =
+                
_impl->key_to_temporary_id.erase(metadata_key_ref(_impl->keys.back()));
         DCHECK_EQ(erased, 1);
         _impl->keys.pop_back();
     }
@@ -224,12 +271,13 @@ void VariantMetadataBuilder::_reserve_keys(size_t count) {
         throw Exception(ErrorCode::INVALID_ARGUMENT,
                         "Cannot reserve Variant metadata keys after seal");
     }
+    _impl->keys.reserve(count);
     _impl->key_to_temporary_id.reserve(count);
 }
 
 StringRef VariantMetadataBuilder::_temporary_key(uint32_t temporary_id) const 
noexcept {
     DCHECK_LT(temporary_id, _impl->keys.size());
-    return {_impl->keys[temporary_id].data(), 
_impl->keys[temporary_id].size()};
+    return metadata_key_ref(_impl->keys[temporary_id]);
 }
 
 size_t VariantMetadataBuilder::_key_capacity() const noexcept {
@@ -255,8 +303,8 @@ void VariantMetadataBuilder::seal() {
         sorted_temporary_ids.push_back(temporary_id);
     }
     std::ranges::sort(sorted_temporary_ids, [this](uint32_t left, uint32_t 
right) {
-        const StringRef left_key {_impl->keys[left].data(), 
_impl->keys[left].size()};
-        const StringRef right_key {_impl->keys[right].data(), 
_impl->keys[right].size()};
+        const StringRef left_key = metadata_key_ref(_impl->keys[left]);
+        const StringRef right_key = metadata_key_ref(_impl->keys[right]);
         return left_key.compare(right_key) < 0;
     });
 
@@ -297,8 +345,12 @@ void VariantMetadataBuilder::seal() {
         append_unsigned(_impl->encoded, offset, offset_width);
     }
     for (uint32_t temporary_id : sorted_temporary_ids) {
-        const auto& key = _impl->keys[temporary_id];
-        _impl->encoded.insert(key.data(), key.data() + key.size());
+        const StringRef key = metadata_key_ref(_impl->keys[temporary_id]);
+        // PaddedPODArray::insert must not receive a null source pointer, and 
an empty key has no
+        // bytes to append.
+        if (key.size != 0) {
+            _impl->encoded.insert(key.data, key.data + key.size);
+        }
     }
     DCHECK_EQ(_impl->encoded.size(), encoded_size);
     _impl->sealed_key_count = count;
@@ -342,9 +394,10 @@ StringRef VariantMetadataBuilder::encoded_metadata() const 
{
 PaddedPODArray<char> VariantMetadataBuilder::_take_encoded_metadata() noexcept 
{
     DCHECK(_impl->sealed);
     PaddedPODArray<char> encoded = std::move(_impl->encoded);
-    release_batch_container(_impl->keys);
-    release_batch_container(_impl->key_to_temporary_id);
-    release_batch_container(_impl->temporary_to_final_id);
+    // Destroy the scratch state directly: the encoded bytes were moved out 
first, and destroying
+    // the key map and key vectors only releases memory. Rebuilding empty 
containers here would
+    // allocate inside a noexcept cleanup path.
+    _impl.reset();
     return encoded;
 }
 
@@ -1237,14 +1290,14 @@ public:
     }
 
     void release_scratch() noexcept {
-        release_batch_container(scalar_bytes);
-        release_batch_container(nodes);
-        release_batch_container(containers);
-        release_batch_container(children);
-        release_batch_container(scope_stack);
-        release_batch_container(object_id_scratch);
-        release_batch_container(container_plans);
-        release_batch_container(planned_object_children);
+        release_batch_vector(scalar_bytes);
+        release_batch_vector(nodes);
+        release_batch_vector(containers);
+        release_batch_vector(children);
+        release_batch_vector(scope_stack);
+        release_batch_vector(object_id_scratch);
+        release_batch_vector(container_plans);
+        release_batch_vector(planned_object_children);
         previous_object_tokens = nullptr;
         pending_object_tokens = nullptr;
         active_container = INVALID_INDEX;
@@ -1575,12 +1628,13 @@ VariantBatchBuilder VariantBatchBuilder::finish_batch() 
{
     _impl->refresh_counters(_impl->metadata._key_capacity(),
                             _impl->metadata._key_capacity_growths(), 
_impl->metadata.num_keys());
     _impl->counters_finalized = true;
+    DBUG_EXECUTE_IF("VariantBatchBuilder.before_release_scratch", 
DBUG_RUN_CALLBACK());
 #endif
     PaddedPODArray<char> metadata = _impl->metadata._take_encoded_metadata();
     _impl->collection.release_scratch();
-    release_batch_container(_impl->roots);
-    release_batch_container(_impl->previous_object_tokens);
-    release_batch_container(_impl->pending_object_tokens);
+    release_batch_vector(_impl->roots);
+    release_batch_vector(_impl->previous_object_tokens);
+    release_batch_vector(_impl->pending_object_tokens);
     return {std::move(metadata), std::move(values), std::move(offsets)};
 }
 
diff --git a/be/src/storage/segment/variant/v2/variant_path_builder.cpp 
b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
index 77757244d6c..3c12192b164 100644
--- a/be/src/storage/segment/variant/v2/variant_path_builder.cpp
+++ b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
@@ -56,6 +56,9 @@
 #include "core/value/vdatetime_value.h"
 #include "exec/common/variant_util.h"
 #include "exprs/function/parse/variant_jsonb_parse.h"
+#ifdef BE_TEST
+#include "util/debug_points.h"
+#endif
 #include "util/jsonb_document.h"
 #include "util/jsonb_writer.h"
 
@@ -1225,18 +1228,27 @@ Status VariantPathBuilder::append(VariantRef value, 
size_t row,
         }
 
         try {
+#ifdef BE_TEST
+            DBUG_EXECUTE_IF("VariantPathBuilder.before_append_value", 
DBUG_RUN_CALLBACK());
+#endif
             auto& nullable = assert_cast<ColumnNullable&>(*_impl->column);
             append_value(value, _impl->type, &nullable.get_nested_column());
-            nullable.get_null_map_data().push_back(0);
-        } catch (const Exception&) {
-            if (is_array || _impl->type->get_primitive_type() == TYPE_JSONB) {
+        } catch (const Exception& exception) {
+            if (exception.code() == ErrorCode::MEM_ALLOC_FAILED ||
+                exception.code() == ErrorCode::MEM_LIMIT_EXCEEDED || is_array 
||
+                _impl->type->get_primitive_type() == TYPE_JSONB) {
                 throw;
             }
             RETURN_IF_ERROR(_impl->promote(jsonb_type(), false, on_promotion));
             auto& nullable = assert_cast<ColumnNullable&>(*_impl->column);
             append_value(value, _impl->type, &nullable.get_nested_column());
-            nullable.get_null_map_data().push_back(0);
         }
+
+#ifdef BE_TEST
+        DBUG_EXECUTE_IF("VariantPathBuilder.before_append_null_map", 
DBUG_RUN_CALLBACK());
+#endif
+        auto& nullable = assert_cast<ColumnNullable&>(*_impl->column);
+        nullable.get_null_map_data().push_back(0);
         _impl->rowids.push_back(static_cast<uint32_t>(row));
         _impl->logical_rows = row + 1;
 #if defined(BE_TEST) && !defined(BE_BENCHMARK)
diff --git a/be/test/storage/variant/variant_column_writer_reader_test.cpp 
b/be/test/storage/variant/variant_column_writer_reader_test.cpp
index 4b63f624480..c083243e9a0 100644
--- a/be/test/storage/variant/variant_column_writer_reader_test.cpp
+++ b/be/test/storage/variant/variant_column_writer_reader_test.cpp
@@ -20,6 +20,7 @@
 #include <atomic>
 #include <cstdlib>
 #include <cstring>
+#include <functional>
 #include <mutex>
 #include <optional>
 #include <ranges>
@@ -260,6 +261,36 @@ TEST(VariantPathBuilderTest, 
PromotesValuesAndMaterializesMissingRows) {
     }
 }
 
+TEST(VariantPathBuilderTest, MemoryFailureBypassesJsonbFallback) {
+    VariantBatchBuilder value_builder;
+    auto row = value_builder.begin_row();
+    row.add_int(42);
+    row.finish();
+    auto values = value_builder.finish_batch();
+    const auto value = values.value_at(0);
+
+    for (const auto point :
+         {"VariantPathBuilder.before_append_value", 
"VariantPathBuilder.before_append_null_map"}) {
+        SCOPED_TRACE(point);
+        segment_v2::VariantPathBuilder builder(PathInData("metric"));
+        ASSERT_TRUE(builder.append(value, 0).ok());
+        const bool old_enable_debug_points = config::enable_debug_points;
+        config::enable_debug_points = true;
+        DebugPoints::instance()->add_with_callback(
+                point, std::function<void()>([] {
+                    throw Exception(ErrorCode::MEM_ALLOC_FAILED, "Injected 
allocation failure");
+                }));
+        Defer cleanup {[&] {
+            DebugPoints::instance()->remove(point);
+            config::enable_debug_points = old_enable_debug_points;
+        }};
+        const Status status = builder.append(value, 1);
+        EXPECT_TRUE(status.is<ErrorCode::MEM_ALLOC_FAILED>()) << status;
+        EXPECT_EQ(builder.promotion_count(), 0);
+        EXPECT_EQ(remove_nullable(builder.type())->get_primitive_type(), 
TYPE_BIGINT);
+    }
+}
+
 TEST(VariantPathBuilderTest, PreservesNativeUuidThroughMaterialization) {
     const std::array<uint8_t, 16> bytes {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 
0x66, 0x77,
                                          0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 
0xee, 0xff};
diff --git a/be/test/util/variant/variant_batch_builder_test.cpp 
b/be/test/util/variant/variant_batch_builder_test.cpp
index 257767eef20..415fc8bb875 100644
--- a/be/test/util/variant/variant_batch_builder_test.cpp
+++ b/be/test/util/variant/variant_batch_builder_test.cpp
@@ -21,6 +21,7 @@
 
 #include <array>
 #include <cstdint>
+#include <functional>
 #include <initializer_list>
 #include <limits>
 #include <memory>
@@ -36,6 +37,7 @@
 #include "core/value/variant/variant_parquet_encoding.h"
 #include "runtime/memory/mem_tracker_limiter.h"
 #include "runtime/thread_context.h"
+#include "util/debug_points.h"
 #include "variant_test_utils.h"
 
 namespace doris {
@@ -685,6 +687,147 @@ TEST(VariantMetadataBuilderTest, 
RegisterKeyPreservesValidationOnDictionaryMiss)
     EXPECT_EQ(metadata.metadata_ref().key_at(0), string_ref("valid"));
 }
 
+TEST(VariantMetadataBuilderTest, EmptyKeyRoundTripsThroughMetadataEncoding) {
+    VariantMetadataBuilder metadata;
+    EXPECT_EQ(metadata.register_key(string_ref("")), 0);
+    EXPECT_EQ(metadata.register_key(string_ref("")), 0);
+    EXPECT_EQ(metadata.register_key(string_ref("a")), 1);
+
+    metadata.seal();
+    ASSERT_EQ(metadata.metadata_ref().dict_size(), 2);
+    // The empty key has no bytes to append but still occupies the first 
dictionary slot.
+    EXPECT_EQ(metadata.metadata_ref().key_at(0), string_ref(""));
+    EXPECT_EQ(metadata.metadata_ref().key_at(1), string_ref("a"));
+    EXPECT_EQ(metadata.final_id(0), 0);
+    EXPECT_EQ(metadata.final_id(1), 1);
+}
+
+TEST(VariantMetadataBuilderTest, KeyReferencesStayValidAcrossKeyStorageGrowth) 
{
+    constexpr uint32_t KEY_COUNT = 8'192;
+    VariantMetadataBuilder metadata;
+    std::vector<std::string> keys;
+    keys.reserve(KEY_COUNT);
+    for (uint32_t index = 0; index < KEY_COUNT; ++index) {
+        keys.push_back(numbered_key(index));
+    }
+    for (uint32_t index = 0; index < KEY_COUNT; ++index) {
+        ASSERT_EQ(metadata.register_key(StringRef(keys[index])), index);
+    }
+    // Re-registering after several outer vector growths must still find the 
original entry. A
+    // dangling StringRef in the map would either read freed bytes or return a 
different id.
+    for (uint32_t index = 0; index < KEY_COUNT; index += 97) {
+        EXPECT_EQ(metadata.register_key(StringRef(keys[index])), index);
+    }
+
+    metadata.seal();
+    ASSERT_EQ(metadata.metadata_ref().dict_size(), KEY_COUNT);
+    for (uint32_t index = 0; index < KEY_COUNT; ++index) {
+        EXPECT_EQ(metadata.metadata_ref().key_at(metadata.final_id(index)), 
StringRef(keys[index]));
+    }
+}
+
+// Reject exactly one allocation inside register_key and verify that the 
failed registration
+// publishes nothing and can be retried with the same temporary id. Sweeping 
the allowance covers
+// the byte copy, the outer vector reservation and the first key-map insertion.
+TEST(VariantMetadataBuilderTest, 
RegisterKeyFailureIsAtomicAcrossAllocationSizes) {
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantMetadataBuilderKeyFailure");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    ++enable_thread_catch_bad_alloc;
+    Defer restore_bad_alloc {[] { --enable_thread_catch_bad_alloc; }};
+
+    const std::string key = "a metadata key long enough to own a buffer";
+    for (const int64_t allowance :
+         {int64_t {0}, int64_t {1}, int64_t {4}, int64_t {16}, int64_t {32}, 
int64_t {64},
+          int64_t {128}, int64_t {1'024}, int64_t {8'192}}) {
+        VariantMetadataBuilder metadata;
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const int64_t baseline = tracker->consumption();
+        tracker->set_limit(baseline + allowance);
+        bool failed = false;
+        try {
+            metadata.register_key(StringRef(key));
+        } catch (const Exception& exception) {
+            failed = true;
+            EXPECT_EQ(exception.code(), ErrorCode::MEM_ALLOC_FAILED);
+        }
+        EXPECT_EQ(metadata.num_keys(), failed ? 0 : 1);
+        tracker->set_limit(-1);
+        ASSERT_EQ(metadata.num_keys(), failed ? 0 : 1);
+        // A rejected registration must leave no key and no map entry behind.
+        EXPECT_EQ(metadata.register_key(StringRef(key)), 0);
+        EXPECT_EQ(metadata.num_keys(), 1);
+    }
+}
+
+// Fill to the point where the outer key vector has no spare capacity, then 
reject the growth
+// allocation. The key bytes have already been copied at that point, so this 
also proves the
+// copy is discarded without leaving an unindexed key.
+TEST(VariantMetadataBuilderTest, 
RegisterKeyFailureDuringKeyStorageGrowthIsAtomic) {
+    // The key vector grows geometrically and is full at every power of two.
+    constexpr uint32_t FILLED = 1'024;
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantMetadataBuilderKeyGrowth");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    VariantMetadataBuilder metadata;
+    for (uint32_t index = 0; index < FILLED; ++index) {
+        ASSERT_EQ(metadata.register_key(StringRef(numbered_key(index))), 
index);
+    }
+    ASSERT_EQ(metadata.num_keys(), FILLED);
+
+    const std::string next = numbered_key(FILLED);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const int64_t baseline = tracker->consumption();
+    // The next key copies its bytes first and then grows the outer vector; 
allow only the copy.
+    tracker->set_limit(baseline + static_cast<int64_t>(next.size()));
+    ++enable_thread_catch_bad_alloc;
+    Defer restore_bad_alloc {[] { --enable_thread_catch_bad_alloc; }};
+    expect_builder_exception_code(ErrorCode::MEM_ALLOC_FAILED,
+                                  [&] { 
metadata.register_key(StringRef(next)); });
+    EXPECT_EQ(metadata.num_keys(), FILLED);
+    // Existing entries remain reachable through their map StringRefs.
+    for (uint32_t index = 0; index < FILLED; index += 61) {
+        EXPECT_EQ(metadata.register_key(StringRef(numbered_key(index))), 
index);
+    }
+
+    tracker->set_limit(-1);
+    EXPECT_EQ(metadata.register_key(StringRef(next)), FILLED);
+    EXPECT_EQ(metadata.num_keys(), FILLED + 1);
+}
+
+// Fill just below the flat_hash_map growth threshold with spare capacity in 
the outer key vector,
+// so the rejected allocation is the map table growth rather than a key buffer 
or vector growth.
+TEST(VariantMetadataBuilderTest, RegisterKeyFailureDuringKeyMapGrowthIsAtomic) 
{
+    // phmap keeps a 7/8 load factor: a 2047-slot table grows on its 1793rd 
entry, where the outer
+    // key vector still has room, so only the map table allocation can fail.
+    constexpr uint32_t FILLED = 1'792;
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantMetadataBuilderMapGrowth");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    VariantMetadataBuilder metadata;
+    for (uint32_t index = 0; index < FILLED; ++index) {
+        ASSERT_EQ(metadata.register_key(StringRef(numbered_key(index))), 
index);
+    }
+    ASSERT_EQ(metadata.num_keys(), FILLED);
+
+    const std::string next = numbered_key(FILLED);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const int64_t baseline = tracker->consumption();
+    tracker->set_limit(baseline + static_cast<int64_t>(next.size()));
+    ++enable_thread_catch_bad_alloc;
+    Defer restore_bad_alloc {[] { --enable_thread_catch_bad_alloc; }};
+    expect_builder_exception_code(ErrorCode::MEM_ALLOC_FAILED,
+                                  [&] { 
metadata.register_key(StringRef(next)); });
+    EXPECT_EQ(metadata.num_keys(), FILLED);
+    for (uint32_t index = 0; index < FILLED; index += 61) {
+        EXPECT_EQ(metadata.register_key(StringRef(numbered_key(index))), 
index);
+    }
+
+    tracker->set_limit(-1);
+    EXPECT_EQ(metadata.register_key(StringRef(next)), FILLED);
+    EXPECT_EQ(metadata.num_keys(), FILLED + 1);
+}
+
 std::string build_array_with_nulls(uint32_t count, uint8_t* value_header_out) {
     const OwnedBuilderValue owned = 
build_owned_value([count](VariantBatchBuilder::Row& row) {
         auto array = row.start_array();
@@ -1579,6 +1722,36 @@ TEST(VariantBatchBuilderTest, 
AbortAndRowErrorsRollbackBeforeTheNextRow) {
     EXPECT_EQ(required_field(block.value_at(0), "retained").get_int(), 9);
 }
 
+TEST(VariantBatchBuilderTest, AbortedRowReleasesItsKeysAndTemporaryIds) {
+    VariantBatchBuilder builder;
+    {
+        auto row = builder.begin_row();
+        auto object = row.start_object();
+        object.add_key(string_ref("first"));
+        row.add_int(1);
+        object.add_key(string_ref("second"));
+        row.add_int(2);
+        object.finish();
+        row.abort();
+    }
+    {
+        auto row = builder.begin_row();
+        auto object = row.start_object();
+        // Aborting the first row must remove its keys from both the 
dictionary and the map, so
+        // this key starts a fresh key vector at temporary id 0.
+        object.add_key(string_ref("third"));
+        row.add_int(3);
+        object.finish();
+        row.finish();
+    }
+
+    VariantBatchBuilder block = builder.finish_batch();
+    ASSERT_EQ(block.num_rows(), 1);
+    ASSERT_EQ(block.metadata_ref().dict_size(), 1);
+    EXPECT_EQ(block.metadata_ref().key_at(0), string_ref("third"));
+    EXPECT_EQ(required_field(block.value_at(0), "third").get_int(), 3);
+}
+
 TEST(VariantBatchBuilderTest, 
CopiesBorrowedKeysStringsAndBinaryBeforeRowReturns) {
     VariantBatchBuilder builder;
     std::string key = "borrowed";
@@ -1901,6 +2074,91 @@ TEST(VariantBatchBuilderTest, 
CapacityGrowthIsBatchBoundedRatherThanPerRow) {
     EXPECT_GE(large.pending_object_token_capacity, 
small.pending_object_token_capacity);
 }
 
+TEST(VariantBatchBuilderTest, ReleasesScratchWhenAllocationsAreRejected) {
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantBatchBuilderScratchRelease");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    VariantBatchBuilder builder;
+    auto row = builder.begin_row();
+    auto object = row.start_object();
+    object.add_key(string_ref("key"));
+    row.add_int(42);
+    object.finish();
+    row.finish();
+
+    const bool old_debug_points = config::enable_debug_points;
+    config::enable_debug_points = true;
+    constexpr auto point = "VariantBatchBuilder.before_release_scratch";
+    bool injected = false;
+    DebugPoints::instance()->add_with_callback(point, 
std::function<void()>([&] {
+                                                   tracker->set_limit(1);
+                                                   injected = true;
+                                               }));
+    Defer cleanup {[&] {
+        DebugPoints::instance()->remove(point);
+        config::enable_debug_points = old_debug_points;
+        tracker->set_limit(-1);
+    }};
+    ++enable_thread_catch_bad_alloc;
+    Defer restore_catch {[] { --enable_thread_catch_bad_alloc; }};
+    VariantBatchBuilder block = builder.finish_batch();
+    tracker->set_limit(-1);
+    ASSERT_TRUE(injected);
+    ASSERT_EQ(block.num_rows(), 1);
+    ASSERT_EQ(block.metadata_ref().dict_size(), 1);
+    EXPECT_EQ(required_field(block.value_at(0), "key").get_int(), 42);
+}
+
+TEST(VariantBatchBuilderTest, FailedKeyRegistrationLeavesNoUnindexedKey) {
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantBatchBuilderKeyRegistration");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    // A nonempty key first allocates its bytes; an empty key still needs room 
in the key vector.
+    // Neither may publish a map entry before the key is committed.
+    for (const auto key : {string_ref("new key"), string_ref("")}) {
+        VariantMetadataBuilder metadata;
+        {
+            tracker->set_limit(1);
+            ++enable_thread_catch_bad_alloc;
+            Defer restore {[&] {
+                --enable_thread_catch_bad_alloc;
+                tracker->set_limit(-1);
+            }};
+            expect_builder_exception_code(ErrorCode::MEM_ALLOC_FAILED,
+                                          [&] { metadata.register_key(key); });
+        }
+        EXPECT_EQ(metadata.num_keys(), 0);
+        EXPECT_EQ(metadata.register_key(key), 0);
+        EXPECT_EQ(metadata.num_keys(), 1);
+    }
+}
+
+TEST(VariantBatchBuilderTest, FailedSealLeavesTheBatchTerminal) {
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::QUERY,
+                                                    
"VariantBatchBuilderFailedSeal");
+    auto scoped_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    VariantBatchBuilder builder;
+    {
+        auto row = builder.begin_row();
+        row.add_int(1);
+        row.finish();
+    }
+
+    tracker->set_limit(1);
+    ++enable_thread_catch_bad_alloc;
+    Defer restore {[&] {
+        --enable_thread_catch_bad_alloc;
+        tracker->set_limit(-1);
+    }};
+    expect_builder_exception_code(ErrorCode::MEM_ALLOC_FAILED, [&] { 
builder.finish_batch(); });
+    tracker->set_limit(-1);
+
+    // finish_batch marks the encoding unit terminal before seal(), so a 
failed seal cannot be
+    // retried or extended with another row.
+    EXPECT_THROW(builder.begin_row(), Exception);
+    EXPECT_THROW(builder.finish_batch(), Exception);
+}
+
 TEST(VariantBatchBuilderTest, BatchLifetimeAllocationsAreMemTrackerVisible) {
     constexpr size_t ROWS = 2'048;
     constexpr size_t PAYLOAD_BYTES = 4'096;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to