[BugFix] Fix LoadChunkSpiller init race that crashes BE during load spill#76098
[BugFix] Fix LoadChunkSpiller init race that crashes BE during load spill#76098sevev wants to merge 2 commits into
Conversation
…pill LoadChunkSpiller::_prepare() lazily creates the spiller and its serde but used `_spiller == nullptr` as the readiness flag with no lock. Multiple memtable-flush threads share one LoadChunkSpiller and call spill() concurrently on the first spill (see the parallel-flush comment on spill()). One thread could publish `_spiller` (making it non-null) before creating the serde's encode context, while a racing thread observed the non-null spiller, skipped initialization, and called ColumnarSerde::serialize() with a null encode context. serialize() then dereferenced it in _get_encode_levels() and crashed the BE/CN with SIGSEGV. Serialize the one-time initialization with a mutex and gate readiness on a separate `_prepared` atomic that is stored with release ordering only after the spiller and its serde are fully constructed; readers check it with acquire ordering. The spiller is built on a local and published via std::move, so a half-initialized spiller is never observable through `_spiller`. As defense in depth, ColumnarSerde::serialize() now returns an error instead of dereferencing a null encode context, mirroring the existing null guard in _max_serialized_size(). Signed-off-by: sevev <qiangzh95@gmail.com>
bc43745 to
0404bac
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
[BE Incremental Coverage Report]✅ pass : 18 / 19 (94.74%) file detail
|
Address review feedback: replace the hand-rolled double-checked locking (std::mutex + std::atomic<bool>) in _prepare() with std::call_once, the idiomatic one-time-init primitive used elsewhere in the codebase. It establishes the happens-before edge for the fully-constructed spiller without any manual acquire/release reasoning. The init body still builds the spiller on a local and publishes it via std::move only after its serde's encode context is prepared, so a half-initialized spiller is never observable. The outcome is cached in _prepare_status and returned to every caller, so a failed init is reported to all concurrent flush threads instead of leaving a null/half-built spiller reachable (the crash this fix addresses). LoadChunkSpiller is per-load (a fresh unique_ptr in each SpillMemTableSink), so a cached failure never leaks into a later load. Signed-off-by: sevev <qiangzh95@gmail.com>
|
@codex review |
Module-risk briefing for Codex reviewThis is review focus, not a finding list. Validate against the diff. High-confidence risks
|
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
[Java-Extensions Incremental Coverage Report]✅ pass : 0 / 0 (0%) |
[FE Incremental Coverage Report]✅ pass : 0 / 0 (0%) |
Why I'm doing:
LoadChunkSpiller::_prepare()lazily creates the spiller and its serde, but used_spiller == nullptras its "already initialized" flag without any lock. Multiplememtable-flush threads share a single
LoadChunkSpillerand callspill()concurrentlyon the first spill (see the parallel-flush comment on
LoadChunkSpiller::spill).Because
_spilleris assigned before the serde's encode context is created, a racingthread could observe a non-null
_spiller, skip initialization, and callColumnarSerde::serialize()while the encode context was still null.serialize()thendereferenced the null context in
_get_encode_levels()and crashed the BE/CN withSIGSEGV. Observed on a shared-data (CN) production cluster during load spill:What I'm doing:
_prepare()with astd::mutex, and gatereadiness on a separate
std::atomic<bool> _preparedthat is stored with releaseordering only after the spiller and its serde are fully constructed; readers load
it with acquire ordering. Readiness is no longer inferred from
_spiller != nullptr.std::move, so a half-initializedspiller is never observable through
_spiller.ColumnarSerde::serialize()now returns an error instead ofdereferencing a null encode context, mirroring the existing null guard in
_max_serialized_size().LoadChunkSpillerand asserts all concurrent spills succeed.What type of PR is this:
Does this PR entail a change in behavior?
If yes, please specify the type of change:
Checklist:
Bugfix cherry-pick branch check: