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

airborne12 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 655b46eb3eb [fix](be) Preserve SNII NULL-docid memory accounting 
(#66880)
655b46eb3eb is described below

commit 655b46eb3eb3675c4481d5505998505ec836bcdb
Author: Jack <[email protected]>
AuthorDate: Tue Oct 27 18:30:07 2026 +0800

    [fix](be) Preserve SNII NULL-docid memory accounting (#66880)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66052
    
    Problem Summary: SNII released the ingestion writer NULL-docid memory
    charge before flush, then copied the still-live vector into
    `LogicalIndexWriter` without a `Reservation`. A one-million-NULL segment
    retained a 4 MiB docid vector while the build reporter observed only
    about 674 KiB of bitmap scratch, and the handoff briefly held two
    complete vectors. This PR carries the existing move-only vector and
    `Reservation` through `IndexFileWriter` and `SniiCompoundWriter`,
    precharges geometric vector replacements during growth, and releases the
    charge only after the NULL bitmap consumes the docids. A real
    one-million-NULL compound build verifies reporter current/peak bytes and
    terminal zero balance.
---
 be/src/storage/index/index_file_writer.cpp         |   5 +-
 be/src/storage/index/index_file_writer.h           |   2 +-
 .../index/inverted/snii/snii_index_writer.cpp      |  49 +++++----
 .../index/inverted/snii/snii_index_writer.h        |  11 +-
 .../inverted/snii/writer/logical_index_writer.cpp  |   3 -
 .../inverted/snii/writer/logical_index_writer.h    |   3 +-
 .../inverted/snii/writer/snii_compound_writer.cpp  |  27 ++++-
 .../inverted/snii/writer/snii_compound_writer.h    |   8 +-
 .../snii/compaction/snii_streamed_session_test.cpp |  52 +++++++++
 .../index/inverted/snii/snii_writer_test.cpp       | 118 +++++++++++++++++++++
 .../snii/writer/spimi_streaming_writer_test.cpp    |   2 +-
 11 files changed, 234 insertions(+), 46 deletions(-)

diff --git a/be/src/storage/index/index_file_writer.cpp 
b/be/src/storage/index/index_file_writer.cpp
index 24195f6250f..30b81302f62 100644
--- a/be/src/storage/index/index_file_writer.cpp
+++ b/be/src/storage/index/index_file_writer.cpp
@@ -325,7 +325,7 @@ void IndexFileWriter::_release_snii_blob_directories() {
 }
 
 Status IndexFileWriter::add_snii_index(const TabletIndex* index_meta, uint32_t 
doc_count,
-                                       std::vector<uint32_t> null_docids,
+                                       doris::snii::writer::TrackedNullDocids 
null_docids,
                                        doris::snii::writer::SpimiTermBuffer* 
const term_buffer,
                                        doris::snii::format::IndexConfig 
index_config,
                                        SniiAddIndexOptions options,
@@ -348,13 +348,12 @@ Status IndexFileWriter::add_snii_index(const TabletIndex* 
index_meta, uint32_t d
     input.index_suffix = index_meta->get_index_suffix();
     input.config = index_config;
     input.doc_count = doc_count;
-    input.null_docids = std::move(null_docids);
     input.encoded_norms = std::move(options.encoded_norms);
     input.gram_scheme = options.gram_scheme;
     input.term_source = term_buffer;
     input.mem_reporter = mem_reporter;
     snii_resolve_index_write_params(options.is_direct_load, 
!input.encoded_norms.empty(), &input);
-    RETURN_IF_ERROR(_snii_compound_writer->add_logical_index(input));
+    RETURN_IF_ERROR(_snii_compound_writer->add_logical_index(input, 
std::move(null_docids)));
     ++_snii_index_count;
     return Status::OK();
 }
diff --git a/be/src/storage/index/index_file_writer.h 
b/be/src/storage/index/index_file_writer.h
index 51c254f68e9..ef58cd33e7b 100644
--- a/be/src/storage/index/index_file_writer.h
+++ b/be/src/storage/index/index_file_writer.h
@@ -118,7 +118,7 @@ public:
         std::vector<uint8_t> encoded_norms;
     };
     Status add_snii_index(const TabletIndex* index_meta, uint32_t doc_count,
-                          std::vector<uint32_t> null_docids,
+                          doris::snii::writer::TrackedNullDocids null_docids,
                           doris::snii::writer::SpimiTermBuffer* const 
term_buffer,
                           doris::snii::format::IndexConfig index_config,
                           SniiAddIndexOptions options,
diff --git a/be/src/storage/index/inverted/snii/snii_index_writer.cpp 
b/be/src/storage/index/inverted/snii/snii_index_writer.cpp
index 1e34461aa55..94766c5ade6 100644
--- a/be/src/storage/index/inverted/snii/snii_index_writer.cpp
+++ b/be/src/storage/index/inverted/snii/snii_index_writer.cpp
@@ -161,6 +161,7 @@ Status SniiIndexColumnWriter::init() {
                     ::doris::snii::writer::BuildMemoryPopulation::kRegistered),
             spill_threshold, 
::doris::snii::writer::MemoryReporter::CapPolicy::kSpillThreshold,
             static_cast<uint64_t>(config::snii_postings_workspace_bytes));
+    _null_docids_reservation = _memory_reporter->make_reservation();
     _term_buffer = std::make_unique<::doris::snii::writer::SpimiTermBuffer>(
             _has_positions, spill_threshold, _memory_reporter.get());
     // Register with the process-wide memory limiter so writers can honor 
advisory spill requests.
@@ -352,16 +353,24 @@ Status SniiIndexColumnWriter::add_array_values(size_t 
field_size, const uint8_t*
     return Status::OK();
 }
 
-void SniiIndexColumnWriter::_report_null_docids_capacity(bool release_all) {
-    if (_memory_reporter == nullptr) {
-        return;
+Status SniiIndexColumnWriter::_reserve_null_docids_for_append(size_t count) {
+    const size_t need = _null_docids.size() + count;
+    // Repeated NULL runs reuse capacity until geometric growth is needed.
+    if (need <= _null_docids.capacity()) {
+        return Status::OK();
     }
-    const int64_t now =
-            release_all ? 0 : static_cast<int64_t>(_null_docids.capacity() * 
sizeof(uint32_t));
-    if (now != _null_docids_charged_bytes) {
-        _memory_reporter->report(now - _null_docids_charged_bytes);
-        _null_docids_charged_bytes = now;
+    const size_t target_capacity = std::max(need, _null_docids.capacity() * 2);
+    // Uninitialized writers in unit tests have no reporter.
+    if (_memory_reporter == nullptr) {
+        _null_docids.reserve(target_capacity);
+        return Status::OK();
     }
+    ::doris::snii::writer::MemoryReporter::Reservation replacement;
+    RETURN_IF_ERROR(_null_docids_reservation.prepare_replacement(
+            static_cast<uint64_t>(target_capacity) * sizeof(uint32_t), 
&replacement));
+    _null_docids.reserve(target_capacity);
+    _null_docids_reservation = std::move(replacement);
+    return Status::OK();
 }
 
 // Hand back the token stream for one row. The two ways to get one differ in 
ownership AND in
@@ -540,10 +549,7 @@ Status SniiIndexColumnWriter::add_nulls(uint32_t count) {
     // the compaction ran 8+x slower than V3 (whose add_nulls is a roaring
     // addRange). Doubling on overflow keeps the O(count) amortization AND 
makes
     // one large run pay at most one reallocation.
-    const size_t need = _null_docids.size() + count;
-    if (need > _null_docids.capacity()) {
-        _null_docids.reserve(std::max(need, _null_docids.capacity() * 2));
-    }
+    RETURN_IF_ERROR(_reserve_null_docids_for_append(count));
     for (uint32_t i = 0; i < count; ++i) {
         _null_docids.push_back(_rid + i);
     }
@@ -552,7 +558,6 @@ Status SniiIndexColumnWriter::add_nulls(uint32_t count) {
         _encoded_norms.insert(_encoded_norms.end(), count, 
::doris::snii::query::encode_norm(0));
         _report_encoded_norms_capacity();
     }
-    _report_null_docids_capacity();
     return Status::OK();
 }
 
@@ -564,13 +569,14 @@ Status SniiIndexColumnWriter::add_array_nulls(const 
uint8_t* null_map, size_t nu
     if (num_rows == 0 || null_map == nullptr) {
         return Status::OK();
     }
+    const size_t null_count = std::count(null_map, null_map + num_rows, 
static_cast<uint8_t>(1));
+    RETURN_IF_ERROR(_reserve_null_docids_for_append(null_count));
     const auto first_row = _rid - num_rows;
     for (size_t i = 0; i < num_rows; ++i) {
         if (null_map[i] == 1) {
             _null_docids.push_back(cast_set<uint32_t>(first_row + i));
         }
     }
-    _report_null_docids_capacity();
     return Status::OK();
 }
 
@@ -583,10 +589,6 @@ Status SniiIndexColumnWriter::finish() {
     if (!status.ok()) {
         return Status::InternalError("SNII term buffer error: {}", 
status.to_string());
     }
-    // Ownership of _null_docids hands off to the flush below (transient,
-    // flush-scoped); release the accumulation-phase charge so the retained
-    // reporter (and the observation tracker behind it) balances to zero.
-    _report_null_docids_capacity(/*release_all=*/true);
     // A segment smaller than the sample budget reaches here still holding its 
rows back. Solve
     // on what there is and tokenize them, so a short segment is indexed like 
any other -- and
     // so `options.gram_scheme` below carries the rate its rows were actually 
cut at.
@@ -599,8 +601,10 @@ Status SniiIndexColumnWriter::finish() {
         options.encoded_norms = std::move(_encoded_norms);
     }
     status = _index_file_writer->add_snii_index(
-            _index_meta, cast_set<uint32_t>(_rid), std::move(_null_docids), 
_term_buffer.get(),
-            _config, std::move(options), _memory_reporter.get());
+            _index_meta, cast_set<uint32_t>(_rid),
+            
::doris::snii::writer::TrackedNullDocids(std::move(_null_docids_reservation),
+                                                     std::move(_null_docids)),
+            _term_buffer.get(), _config, std::move(options), 
_memory_reporter.get());
     _report_encoded_norms_capacity(/*release_all=*/true);
     // The tokenizer's buffers outlive this call (they die with _analyzer), 
but the reporter is
     // about to be handed to the index file writer and become a long-lived 
process-wide counter,
@@ -622,12 +626,11 @@ Status 
SniiIndexColumnWriter::_latch_analysis_failure(Status status) {
 
 void SniiIndexColumnWriter::close_on_error() {
     _term_buffer.reset();
-    // Balance the observation-tracker mirror before dropping the reporter.
-    _report_null_docids_capacity(/*release_all=*/true);
+    std::vector<uint32_t>().swap(_null_docids);
+    _null_docids_reservation.reset();
     _report_encoded_norms_capacity(/*release_all=*/true);
     _report_gram_buffers_capacity(/*release_all=*/true);
     _memory_reporter.reset();
-    _null_docids.clear();
     std::vector<uint8_t>().swap(_encoded_norms);
 }
 
diff --git a/be/src/storage/index/inverted/snii/snii_index_writer.h 
b/be/src/storage/index/inverted/snii/snii_index_writer.h
index bc7b6ee176a..f522d984bee 100644
--- a/be/src/storage/index/inverted/snii/snii_index_writer.h
+++ b/be/src/storage/index/inverted/snii/snii_index_writer.h
@@ -113,9 +113,7 @@ private:
     // Mirror of the above, run once the row's tokens are consumed: closes an 
owned stream, or
     // charges the reused gram tokenizer's settled buffer capacity to the 
memory reporter.
     void _finish_plain_lane_row(lucene::analysis::TokenStream* token_stream, 
bool owned);
-    // Mirrors _null_docids' capacity into _memory_reporter (delta-charged);
-    // release_all zeroes the charge (finish() handoff / close_on_error()).
-    void _report_null_docids_capacity(bool release_all = false);
+    Status _reserve_null_docids_for_append(size_t count);
     void _report_encoded_norms_capacity(bool release_all = false);
     // Mirrors the reused gram tokenizer's buffer capacity into 
_memory_reporter
     // (delta-charged), same shape as the two above. A no-op until the reusable
@@ -187,15 +185,10 @@ private:
     // analyzed row, and always null off the gram lane.
     inverted_index::GramTokenizer* _gram_tokenizer = nullptr;
     std::unique_ptr<::doris::snii::writer::MemoryReporter> _memory_reporter;
+    ::doris::snii::writer::MemoryReporter::Reservation 
_null_docids_reservation;
     std::unique_ptr<::doris::snii::writer::SpimiTermBuffer> _term_buffer;
     std::vector<uint32_t> _null_docids;
     std::vector<uint8_t> _encoded_norms;
-    // Bytes of _null_docids capacity currently mirrored into _memory_reporter
-    // (and through it the SNII index-build observation tracker). Re-charged on
-    // growth in add_nulls / add_array_nulls, released in finish() / 
close_on_error() --
-    // without it a large interleaved-null segment accumulates untracked RSS 
the
-    // Limiter cannot see.
-    int64_t _null_docids_charged_bytes = 0;
     int64_t _encoded_norms_charged_bytes = 0;
     int64_t _gram_buffers_charged_bytes = 0;
     Status _failure_status = Status::OK();
diff --git a/be/src/storage/index/inverted/snii/writer/logical_index_writer.cpp 
b/be/src/storage/index/inverted/snii/writer/logical_index_writer.cpp
index e9f93783f34..2a540a37fcd 100644
--- a/be/src/storage/index/inverted/snii/writer/logical_index_writer.cpp
+++ b/be/src/storage/index/inverted/snii/writer/logical_index_writer.cpp
@@ -522,9 +522,6 @@ uint32_t high_df_floor_for(const SniiIndexInput& in) {
                                  format::kHighDfDigestDivisor);
 }
 
-LogicalIndexWriter::LogicalIndexWriter(const SniiIndexInput& in)
-        : LogicalIndexWriter(in, 
TrackedNullDocids(std::vector<uint32_t>(in.null_docids))) {}
-
 LogicalIndexWriter::LogicalIndexWriter(const SniiIndexInput& in, 
TrackedNullDocids null_docids)
         : index_id_(in.index_id),
           index_suffix_(in.index_suffix),
diff --git a/be/src/storage/index/inverted/snii/writer/logical_index_writer.h 
b/be/src/storage/index/inverted/snii/writer/logical_index_writer.h
index fb13a27b0c7..7a3375f74f1 100644
--- a/be/src/storage/index/inverted/snii/writer/logical_index_writer.h
+++ b/be/src/storage/index/inverted/snii/writer/logical_index_writer.h
@@ -194,7 +194,7 @@ struct FreqStats {
 // Builds and holds the section bytes + meta sub-sections for one logical 
index.
 class LogicalIndexWriter {
 public:
-    explicit LogicalIndexWriter(const SniiIndexInput& in);
+    LogicalIndexWriter(const SniiIndexInput& in, TrackedNullDocids 
null_docids);
     // Out-of-line: stream_state_ points at the private nested BlockState, 
which
     // is incomplete here (unique_ptr needs the complete type at destruction).
     ~LogicalIndexWriter();
@@ -281,7 +281,6 @@ public:
 
 private:
     friend class SniiStreamedIndexSession;
-    LogicalIndexWriter(const SniiIndexInput& in, TrackedNullDocids 
null_docids);
 
     // One DICT block's directory record. The block's serialized bytes are 
appended to
     // the in-RAM dict buffer as soon as the block is cut; only this compact 
summary
diff --git a/be/src/storage/index/inverted/snii/writer/snii_compound_writer.cpp 
b/be/src/storage/index/inverted/snii/writer/snii_compound_writer.cpp
index dca6daa2ad6..11868e3c1ae 100644
--- a/be/src/storage/index/inverted/snii/writer/snii_compound_writer.cpp
+++ b/be/src/storage/index/inverted/snii/writer/snii_compound_writer.cpp
@@ -150,6 +150,31 @@ Status SniiCompoundWriter::inherit(const 
reader::SniiRewriteSnapshot& snapshot,
 }
 
 Status SniiCompoundWriter::add_logical_index(const SniiIndexInput& in) {
+    MemoryReporter::Reservation null_docids_reservation =
+            in.mem_reporter == nullptr ? MemoryReporter::Reservation()
+                                       : in.mem_reporter->make_reservation();
+    if (in.mem_reporter != nullptr) {
+        // A hard cap must reject the copy before its allocation becomes 
resident.
+        RETURN_IF_ERROR(null_docids_reservation.set_bytes(
+                static_cast<uint64_t>(in.null_docids.size()) * 
sizeof(uint32_t)));
+    }
+    std::vector<uint32_t> null_docids(in.null_docids);
+    return _add_logical_index(
+            in, TrackedNullDocids(std::move(null_docids_reservation), 
std::move(null_docids)));
+}
+
+Status SniiCompoundWriter::add_logical_index(const SniiIndexInput& in,
+                                             TrackedNullDocids null_docids) {
+    // Supplying both forms would make the caller's NULL-docid ownership 
ambiguous.
+    if (!in.null_docids.empty()) {
+        return Status::Error<ErrorCode::INVALID_ARGUMENT, false>(
+                "compound: tracked NULL docids must not also be present in 
input");
+    }
+    return _add_logical_index(in, std::move(null_docids));
+}
+
+Status SniiCompoundWriter::_add_logical_index(const SniiIndexInput& in,
+                                              TrackedNullDocids null_docids) {
     if (out_ == nullptr)
         return Status::Error<ErrorCode::INVALID_ARGUMENT, false>("compound: 
null file writer");
     if (finished_)
@@ -174,7 +199,7 @@ Status SniiCompoundWriter::add_logical_index(const 
SniiIndexInput& in) {
         }
     }
     RETURN_IF_ERROR(ensure_bootstrap());
-    auto liw = std::make_unique<LogicalIndexWriter>(in);
+    auto liw = std::make_unique<LogicalIndexWriter>(in, 
std::move(null_docids));
     Placement p;
     // The posting region streams DIRECTLY into the container during build() 
-- no temp
     // round-trip for the bulk -- followed immediately by this index's compact 
DICT
diff --git a/be/src/storage/index/inverted/snii/writer/snii_compound_writer.h 
b/be/src/storage/index/inverted/snii/writer/snii_compound_writer.h
index 49346ee2b46..fd05f057132 100644
--- a/be/src/storage/index/inverted/snii/writer/snii_compound_writer.h
+++ b/be/src/storage/index/inverted/snii/writer/snii_compound_writer.h
@@ -160,10 +160,11 @@ public:
     // holding a partial prefix.
     Status inherit(const reader::SniiRewriteSnapshot& snapshot, 
io::FileReader* source);
 
-    // Buffers one logical index: builds its section bytes and meta 
sub-sections.
-    // The actual file writing happens in finish() (single front-to-back pass).
-    // The key (index_id, suffix) must not collide with an inherited one.
+    // Builds one logical index synchronously. The convenience overload copies
+    // and charges in.null_docids; ingestion uses the tracked overload to 
transfer
+    // the already-charged allocation without a second resident vector.
     Status add_logical_index(const SniiIndexInput& in);
+    Status add_logical_index(const SniiIndexInput& in, TrackedNullDocids 
null_docids);
 
     // Registers one opaque BLOB logical index (kind must not be kInverted).
     // Registration is pure bookkeeping -- NOT A BYTE is written here, so it is
@@ -245,6 +246,7 @@ private:
     Status write_index_aux_sections(LogicalIndexWriter& writer, Placement& 
placement);
     Status write_tail();
     Status append(const std::vector<uint8_t>& bytes);
+    Status _add_logical_index(const SniiIndexInput& in, TrackedNullDocids 
null_docids);
     Status poison(Status status);
     // Argument validation for one add_blob_index call; see the .cpp.
     static Status validate_blob_registration(format::LogicalIndexKind kind,
diff --git 
a/be/test/storage/index/inverted/snii/compaction/snii_streamed_session_test.cpp 
b/be/test/storage/index/inverted/snii/compaction/snii_streamed_session_test.cpp
index b332a16e723..3eabe97a226 100644
--- 
a/be/test/storage/index/inverted/snii/compaction/snii_streamed_session_test.cpp
+++ 
b/be/test/storage/index/inverted/snii/compaction/snii_streamed_session_test.cpp
@@ -500,6 +500,58 @@ TEST(SniiStreamedWriterSessionTest, 
NullDocidsStorageIsTransferredIntoStreamedWr
     assert_ok(compound.finish());
 }
 
+TEST(SniiStreamedWriterSessionTest, 
OrdinaryLargeNullHandoffTracksRetainedCapacity) {
+    constexpr uint32_t kNullCount = 1U << 20;
+    SniiIndexInput input = empty_input(119, "ordinary_null_handoff", 
kNullCount);
+    input.null_docids.resize(kNullCount);
+    std::iota(input.null_docids.begin(), input.null_docids.end(), 0U);
+    const uint64_t retained_null_bytes = input.null_docids.capacity() * 
sizeof(uint32_t);
+    const uint64_t bitmap_build_bytes = 
format::NullBitmapWriter::build_memory_upper_bound(
+            std::span<const uint32_t>(input.null_docids));
+
+    int64_t mirrored_bytes = 0;
+    int64_t peak_bytes = 0;
+    writer::MemoryReporter reporter([&](int64_t delta) {
+        mirrored_bytes += delta;
+        peak_bytes = std::max(peak_bytes, mirrored_bytes);
+    });
+    input.mem_reporter = &reporter;
+    std::vector<uint32_t> null_docids;
+    null_docids.swap(input.null_docids);
+    auto null_docids_reservation = reporter.make_reservation();
+    assert_ok(null_docids_reservation.set_bytes(retained_null_bytes));
+    writer::TrackedNullDocids 
tracked_null_docids(std::move(null_docids_reservation),
+                                                  std::move(null_docids));
+    EXPECT_EQ(reporter.current_bytes(), retained_null_bytes);
+    EXPECT_EQ(mirrored_bytes, retained_null_bytes);
+
+    MemoryFile file;
+    SniiCompoundWriter compound(&file);
+    assert_ok(compound.add_logical_index(input, 
std::move(tracked_null_docids)));
+
+    EXPECT_GE(peak_bytes, static_cast<int64_t>(retained_null_bytes + 
bitmap_build_bytes));
+    EXPECT_EQ(reporter.current_bytes(), 0);
+    EXPECT_EQ(mirrored_bytes, 0);
+    assert_ok(compound.finish());
+    EXPECT_EQ(reporter.current_bytes(), 0);
+    EXPECT_EQ(mirrored_bytes, 0);
+}
+
+TEST(SniiStreamedWriterSessionTest, OrdinaryNullCopyHonorsReporterCap) {
+    SniiIndexInput input = empty_input(120, "ordinary_null_copy_cap", 1024);
+    input.null_docids.resize(input.doc_count);
+    std::iota(input.null_docids.begin(), input.null_docids.end(), 0U);
+    writer::MemoryReporter reporter(nullptr, input.null_docids.size() * 
sizeof(uint32_t) - 1);
+    input.mem_reporter = &reporter;
+    MemoryFile file;
+    SniiCompoundWriter compound(&file);
+    const Status status = compound.add_logical_index(input);
+    EXPECT_TRUE(status.is<ErrorCode::MEM_LIMIT_EXCEEDED>()) << status;
+    EXPECT_EQ(reporter.current_bytes(), 0);
+    EXPECT_EQ(file.bytes_written(), 0);
+    EXPECT_EQ(input.null_docids.size(), input.doc_count);
+}
+
 TEST(SniiStreamedWriterSessionTest, NullBitmapFinalizationHonorsReporterCap) {
     SniiIndexInput input = empty_input(95, "null_memory_cap", 2);
     input.null_docids = {1};
diff --git a/be/test/storage/index/inverted/snii/snii_writer_test.cpp 
b/be/test/storage/index/inverted/snii/snii_writer_test.cpp
index 3ec76ae8f8e..2f6208b5b52 100644
--- a/be/test/storage/index/inverted/snii/snii_writer_test.cpp
+++ b/be/test/storage/index/inverted/snii/snii_writer_test.cpp
@@ -31,6 +31,7 @@
 #include <cstdint>
 #include <map>
 #include <optional>
+#include <span>
 #include <string>
 #include <vector>
 
@@ -38,8 +39,10 @@
 #include "common/exception.h"
 #include "common/status.h"
 #include "gen_cpp/AgentService_types.h"
+#include "io/fs/local_file_system.h"
 #include "runtime/exec_env.h"
 #include "runtime/index_policy/index_policy_mgr.h"
+#include "storage/index/index_file_writer.h"
 #include "storage/index/inverted/analysis/analyzer/analyzer.h"
 #include "storage/index/inverted/analysis/analyzer/custom_analyzer.h"
 #include "storage/index/inverted/analysis/gram/gram_extractor.h"
@@ -52,6 +55,7 @@
 #include "storage/index/inverted/snii/format/dict_block_directory.h"
 #include "storage/index/inverted/snii/format/dict_entry.h"
 #include "storage/index/inverted/snii/format/format_constants.h"
+#include "storage/index/inverted/snii/format/null_bitmap.h"
 #include "storage/index/inverted/snii/format/phrase_bigram.h"
 #include "storage/index/inverted/snii/format/sampled_term_index.h"
 #include "storage/index/inverted/snii/query/bm25_scorer.h"
@@ -505,6 +509,120 @@ TEST(SniiWriterNullDocids, 
AddNullsGrowsGeometricallyNotQuadratically) {
     EXPECT_TRUE(std::ranges::is_sorted(nulls));
 }
 
+namespace {
+constexpr uint32_t kNullHandoffRows = 1U << 20;
+
+void add_null_handoff_rows(doris::segment_v2::SniiIndexColumnWriter* writer, 
bool arrays) {
+    ASSERT_OK(writer->add_nulls(1));
+    if (arrays) {
+        std::vector<uint64_t> offsets(kNullHandoffRows + 1, 0);
+        std::vector<uint8_t> null_map(kNullHandoffRows, 1);
+        ASSERT_OK(writer->add_array_values(sizeof(doris::Slice), nullptr, 
nullptr, offsets.data(),
+                                           kNullHandoffRows));
+        ASSERT_OK(writer->add_array_nulls(null_map.data(), kNullHandoffRows));
+    } else {
+        ASSERT_OK(writer->add_nulls(kNullHandoffRows));
+    }
+}
+
+void check_null_handoff_growth(const doris::segment_v2::SniiIndexColumnWriter& 
writer,
+                               int64_t mirrored_bytes, int64_t peak_bytes) {
+    const auto& nulls = writer.null_docids_for_test();
+    ASSERT_EQ(nulls.size(), kNullHandoffRows + 1);
+    EXPECT_EQ(nulls.front(), 0);
+    EXPECT_EQ(nulls.back(), kNullHandoffRows);
+    const int64_t retained_bytes = nulls.capacity() * sizeof(uint32_t);
+    EXPECT_EQ(writer.memory_reporter_for_test()->current_bytes(), 
retained_bytes);
+    EXPECT_EQ(mirrored_bytes, retained_bytes);
+    EXPECT_GE(peak_bytes, retained_bytes + 
static_cast<int64_t>(sizeof(uint32_t)));
+}
+
+void check_null_handoff_finish(doris::segment_v2::SniiIndexColumnWriter* 
writer,
+                               doris::segment_v2::IndexFileWriter* 
index_writer,
+                               const int64_t* mirrored_bytes, int64_t* 
peak_bytes) {
+    auto* reporter = writer->memory_reporter_for_test();
+    const auto& nulls = writer->null_docids_for_test();
+    const int64_t retained_bytes = nulls.capacity() * sizeof(uint32_t);
+    const auto bitmap_bytes = 
doris::snii::format::NullBitmapWriter::build_memory_upper_bound(
+            std::span<const uint32_t>(nulls));
+    *peak_bytes = *mirrored_bytes;
+    ASSERT_OK(writer->finish());
+    EXPECT_GE(*peak_bytes, retained_bytes + bitmap_bytes);
+    EXPECT_EQ(reporter->current_bytes(), 0);
+    EXPECT_EQ(*mirrored_bytes, 0);
+    EXPECT_EQ(writer->memory_reporter_for_test(), nullptr);
+    ASSERT_EQ(index_writer->_snii_memory_reporters.size(), 1);
+    EXPECT_EQ(index_writer->_snii_memory_reporters.front().get(), reporter);
+    ASSERT_OK(index_writer->begin_close());
+    ASSERT_OK(index_writer->finish_close());
+}
+} // namespace
+
+TEST(SniiWriterNullDocids, 
InitializedScalarAndArrayHandoffPreservesAccounting) {
+    const std::string directory = "./ut_dir/snii_null_docids_handoff";
+    auto fs = doris::io::global_local_filesystem();
+    ASSERT_OK(fs->delete_directory(directory));
+    ASSERT_OK(fs->create_directory(directory));
+    for (const bool arrays : {false, true}) {
+        SCOPED_TRACE(arrays ? "array" : "scalar");
+        int64_t mirrored_bytes = 0;
+        int64_t peak_bytes = 0;
+        {
+            const std::string path = directory + (arrays ? "/array.idx" : 
"/scalar.idx");
+            doris::io::FileWriterPtr file;
+            ASSERT_OK(fs->create_file(path, &file));
+            doris::segment_v2::IndexFileWriter index_writer(
+                    fs, path, "null_handoff", 0, 
doris::InvertedIndexStorageFormatPB::SNII,
+                    std::move(file));
+            doris::TabletIndex meta;
+            doris::TabletIndexPB pb;
+            pb.set_index_type(doris::IndexType::INVERTED);
+            pb.set_index_id(119);
+            pb.add_col_unique_id(0);
+            meta.init_from_pb(pb);
+            doris::segment_v2::SniiIndexColumnWriter writer(
+                    &index_writer, &meta, 
doris::FieldType::OLAP_FIELD_TYPE_VARCHAR);
+            ASSERT_OK(writer.init());
+            auto* reporter = writer.memory_reporter_for_test();
+            mirrored_bytes = reporter->current_bytes();
+            reporter->consume_release_ = [&, original = 
reporter->consume_release_](int64_t delta) {
+                original(delta);
+                mirrored_bytes += delta;
+                peak_bytes = std::max(peak_bytes, mirrored_bytes);
+            };
+            add_null_handoff_rows(&writer, arrays);
+            check_null_handoff_growth(writer, mirrored_bytes, peak_bytes);
+            check_null_handoff_finish(&writer, &index_writer, &mirrored_bytes, 
&peak_bytes);
+        }
+        EXPECT_EQ(mirrored_bytes, 0);
+    }
+}
+
+TEST(SniiWriterNullDocids, FailedHandoffReleasesNullAllocationBeforeReporter) {
+    doris::TabletIndex meta;
+    doris::TabletIndexPB pb;
+    pb.set_index_type(doris::IndexType::INVERTED);
+    pb.set_index_id(120);
+    pb.add_col_unique_id(0);
+    meta.init_from_pb(pb);
+    doris::segment_v2::IndexFileWriter 
index_writer(doris::io::global_local_filesystem(),
+                                                    "missing_null_output", 
"null_handoff", 0,
+                                                    
doris::InvertedIndexStorageFormatPB::SNII);
+    doris::segment_v2::SniiIndexColumnWriter writer(&index_writer, &meta,
+                                                    
doris::FieldType::OLAP_FIELD_TYPE_VARCHAR);
+    ASSERT_OK(writer.init());
+    auto* reporter = writer.memory_reporter_for_test();
+    ASSERT_OK(writer.add_nulls(1U << 20));
+    ASSERT_GT(reporter->current_bytes(), 0);
+    EXPECT_EQ(writer.finish().code(), 
doris::ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND);
+    EXPECT_EQ(reporter->current_bytes(), 0);
+    EXPECT_TRUE(index_writer._snii_memory_reporters.empty());
+    EXPECT_TRUE(writer.null_docids_for_test().empty());
+    writer.close_on_error();
+    EXPECT_EQ(writer.memory_reporter_for_test(), nullptr);
+    EXPECT_EQ(writer.null_docids_for_test().capacity(), 0);
+}
+
 TEST(SniiWriterFailureLatch, AnalyzerFailureDiscardsStateAndBlocksFinish) {
     doris::TabletIndex index_meta;
     init_failure_index_meta(&index_meta, 91);
diff --git 
a/be/test/storage/index/inverted/snii/writer/spimi_streaming_writer_test.cpp 
b/be/test/storage/index/inverted/snii/writer/spimi_streaming_writer_test.cpp
index 15ebca239bd..b588f6877c9 100644
--- a/be/test/storage/index/inverted/snii/writer/spimi_streaming_writer_test.cpp
+++ b/be/test/storage/index/inverted/snii/writer/spimi_streaming_writer_test.cpp
@@ -209,7 +209,7 @@ TEST(SniiSpimiStreamingWriter, StreamingConsumesSource) {
 
     SniiIndexInput in = BaseInput(50);
     in.term_source = &buf;
-    LogicalIndexWriter writer(in);
+    LogicalIndexWriter writer(in, 
TrackedNullDocids(std::move(in.null_docids)));
     // build() streams the posting region straight into a FileWriter sink; 
this test
     // only asserts the source is drained, so a throwaway temp sink suffices.
     const std::string post_path = TempPath();


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

Reply via email to