Repository navigation
[Bug] BE aborts via std::terminate when a VARIANT read exceeds the query memory limit (allocation in noexcept release path) #68898
Description
Activity
Breakwater-GitHub-Analysis-Slot: slot_6f526aaa4700
Initial assessment: This is a well-supported BE availability defect in the VARIANT V2 batch-finalization path. Query memory-limit rejection should remain a query failure; it must not terminate the BE. The reported simultaneous restarts make this worth prioritizing. The issue currently has no labels; suggested triage areas are BE, VARIANT, and memory management.
Verified source evidence
I inspected the public
4.1.4tag at commitad35a140c7fd0b842f18c23300bac581f7d04326, using the configured local repository without modifying it, and compared the builder with upstream master atdd456ab7a894f54c6553415223c6d5c9c7d4e035.release_batch_container()constructsContainer()insidenoexcept._take_encoded_metadata()is alsonoexceptand calls that helper forKeys, astd::deque<PaddedPODArray<char>, CustomStdAllocator<...>>. Both boundaries therefore prevent an allocation exception from escaping normally. The builder implementation is identical in the inspected release and master snapshots. See the release helper and deque type, metadata release, and master helper.CustomStdAllocator::allocate()callsAllocator::alloc(), which checks the memory tracker before allocating. When the tracker rejects the request andenable_thread_catch_bad_allocis enabled, this version throwsdoris::Exceptionwith codeMEM_ALLOC_FAILED, with the underlying limit error in its message. This matches the reported FE error more precisely than saying the thrown code itself isMEM_LIMIT_EXCEEDED. See the allocator adapter and tracker-check implementation.- The caller is
assemble_hierarchical()→publish_encoded()→finish_batch()→_take_encoded_metadata().VariantAssembler::assemble()already catches Doris exceptions and returns a status, but cannot catch an exception that escapes anoexceptboundary and triggers termination. Its catch returnsexception.to_status()directly, so removing the termination hazard alone does not guarantee normalization toMEM_LIMIT_EXCEEDED. See assembly and its catch.
A standalone allocator-instrumented libstdc++ probe confirmed two allocations when constructing an empty deque, zero allocations for
deque::clear(), and invocation of a custom terminate handler when an allocator rejection escaped the samenoexceptswap-with-empty pattern. This is mechanism validation, not an end-to-end Doris/ARM64 reproduction.Fix direction and validation
Prefer allocation-free cleanup for the deque, such as clearing it and releasing retained map/node storage at ordinary builder destruction. If retaining swap-with-empty with a fallback, catch construction failure inside the
noexcepthelper, before it can escape. A catch outside the helper cannot recover from termination. Alternatively, allow exceptions to propagate by changing every relevantnoexceptboundary, including the header declaration; review cleanup during unwinding and the returned query error code.release_scratch()deserves review, but its current members areDorisVectorinstances: empty vector construction does not have the deque's allocation behavior. The shared helper alone is insufficient evidence of a second defect there. The inspected rollback path shrinks containers or clears them; do not remove destructor/rollbacknoexceptguarantees indiscriminately.For the proposed PR, add deterministic allocation rejection specifically at the deque reset after encoding succeeds, rather than relying on the billion-row workload or random failure earlier in encoding. Verify that the BE survives, the query returns a memory-limit error, cleanup remains safe, and a subsequent query succeeds. Exercise normal finalization and row-abort/unwinding paths; the existing builder tests provide a starting point.
Information still needed to confirm the production incident
- Exact BE build/version output and Git SHA, plus compiler and libstdc++ version. The supplied
finish_batch()line 1728 differs from line 1579 in the public tag inspected here; this needs matching symbols/source, not an assumption about deployment tags. - Full symbolized terminate stack and BE log lines around the allocator rejection, correlated with the query ID; effective query/workload-group memory limits and relevant memory-tracker diagnostics. The reported 13.68 MB allocation is not established as the deque's small map/node allocation, so please include the event immediately preceding termination.
- Sanitized table DDL confirming VARIANT V2, representative nested JSON, exact SQL/session settings, and the smallest reproducible case. An isolated low-memory reproduction is preferable to rerunning it across all BEs.
The scan-projection observation should be tracked separately. The inspected scanner executes projections on each input block, which supports that possibility if CAST appears in the scan projection. Whether this specific plan defers materialization requires
EXPLAIN/EXPLAIN VERBOSEand, if available, a sanitized query profile. It does not explain away the independently verified unsafe release mechanism, and no plan change or memory-limit increase should be treated as a fix for BE termination.
Search before asking
Version
4.1.4 (cloud mode, arm64); master is the same. On master
release_batch_containerisbe/src/core/value/variant/variant_batch_builder.cpp:134and_take_encoded_metadata()is:342.What's Wrong?
A query that reads a VARIANT V2 column and exceeds its memory limit aborts the BE process with
std::terminate, instead of failing the query withMEM_LIMIT_EXCEEDED. When the query runs on every BE, every BE aborts at once. In our case all 4 BEs aborted within about 10 s and restarted.FE error for the query:
[MEM_ALLOC_FAILED]Allocator mem tracker check failed, [MEM_LIMIT_EXCEEDED]failed alloc size 13.68 MB.Symbolized BE stack:
Cause:
A default-constructed libstdc++
std::dequeallocates its map and first node. WithCustomStdAllocator, that allocation goes throughMemoryAllocator::alloc, which throwsdoris::Exception(MEM_ALLOC_FAILED, "Allocator mem tracker check failed") once the thread's memory tracker is over its limit. The throw happens inside anoexceptfunction, so the runtime callsstd::terminate.release_scratch()is alsonoexceptand uses the same helper. Othernoexceptfunctions in this file may allocate as well, and are worth auditing for the same pattern.What You Expected?
The query fails with MEM_LIMIT_EXCEEDED and the BE keeps running.
How to Reproduce?
A table with a VARIANT column (Variant V2) holding about 1B rows of nested JSON. A query that materializes the VARIANT for many rows under the default query memory limit, e.g.:
Any query whose VARIANT read pushes the memory tracker over its limit while
finish_batchruns should reproduce it. (Separately, the CAST is evaluated in the scan projection for every row before the top-N, rather than being deferred by lazy materialization. That makes this query far more memory-hungry than its LIMIT suggests.)Anything Else?
Possible fix: make the release path unable to allocate or throw. For example, fall back to
clear()when constructing an empty container throws, or dropnoexceptwhere no caller relies on it, so the memory-limit exception reaches the query as a normal error.Are you willing to submit PR?
Code of Conduct