Detect and mitigate classic pipeline buffer deadlocks (#7752) - #7854
Open
mattcasters wants to merge 5 commits into
Open
Detect and mitigate classic pipeline buffer deadlocks (#7752)#7854mattcasters wants to merge 5 commits into
mattcasters wants to merge 5 commits into
Conversation
Add static split-rejoin analysis and optional spilling rowsets on the local multi-threaded engine so Stream Lookup / Merge Join-style hangs make progress instead of locking up, without extra consumer threads.
Rowset.clear() already deleted spill segments, but nothing invoked it on normal finish or stop—only single-threaded clearError. Call cleanupRowSets from the execution-finished path, cleanup(), and disposeInitializedTransforms.
Exercise a split-rejoin Stream Lookup with more rows than the local rowset size. Enable detect/mitigate on the transforms local run configuration so the classic engine spills and completes instead of hanging.
core cannot resolve engine classes during javadoc generation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses #7752 for the classic local multi-threaded pipeline engine only (Beam/Spark are out of scope: they materialize between stages).
The issue proposed extra loader threads +
CountDownLatchin Stream Lookup. That is transform-specific, risks loading whole streams into memory, and does not help Merge Join / Append / similar multi-input cases. This PR instead:Goal: prefer correctness and progress over silent hangs for data engineers, with clear logging when risk or spill engages.
Problem (what we are actually fixing)
Pipeline hops are already a DAG (
hasLoop/ GUI loop checks). Stream Lookup / Merge Join hangs are not hop-graph cycles. They are bounded-buffer wait-for cycles:BlockingRowSet) have fixed capacity (rowset size).setDone()→ hang.Raising rowset size, redesigning streams, or inserting Blocking are valid workarounds; they are not a generic engine fix.
Decisions and rationale
Rejected: Stream Lookup-only loader threads
Rejected: spill “info hops only”
Info-only spill does not fix classic Stream Lookup: the main hop is the one left unread while the lookup cache loads, so the shared source still blocks on main. Merge Join happens to list both inputs as INFO, so info-only would accidentally help there — wrong rule for the headline case.
Chosen: detect split–rejoin, then spill recommended inbound hops
Detection (
PipelineBufferDeadlockAnalyzer):T(≥2 predecessors via enabled hops, including info).findPreviousTransforms).Spill hop set (v1, safe minimal): all inbound hops
predecessor → Tinvolved in the risk — typically two hops per site, not the whole pipeline. That includes main+info for Stream Lookup without encoding every transform’s read order.Not in v1: mid-path spill (e.g. before Sort on a long branch); auto-inserting Blocking transforms; unbounded pure-memory queues.
Spilling rowset (
SpillingRowSet)BlockingRowSetwhile under capacity.IRowMeta.writeData(not metadata per row; same pattern as Sort / Blocking / Join Rows).putRowsucceeds without waiting on the consumer — that is what breaks the wait-for cycle.clear()/ done path.Critical performance fix:
size()and BaseTransform sleepsFirst mitigation builds were pathologically slow (~38k rows/s, near-zero CPU/disk). Cause:
BaseTransformdoesThread.sleep(0, 1)whensize() >= 99%of rowset size (and a low-water sleep when nearly empty).Fix:
SpillingRowSet.size()is a flow-control signal: with pending work it reports a mid-level occupancy so producers/consumers are not artificially throttled; idle still reports0. After the fix, ~825k rows/s on a large test pipeline.Defaults (local run configuration GUI)
Existing saved run configs keep their stored flags. Only new local configs get these defaults.
Product rationale for default-on mitigate: detection already logs risks and spill usage; locking up for hours is worse for data engineers than occasional disk I/O on known-bad topologies. Users who want strict bounded memory only can turn mitigation off.
Scope boundaries
LocalPipelineEngine+Pipelinerowset allocation).QueueRowSet.Surfaces
avoiding-deadlocks.adocsection 5 + short design notes.Test plan
SpillingRowSetTest(FIFO past capacity, clear, size() not advertising full while spilling)PipelineBufferDeadlockAnalyzerTest(split–rejoin risk, independent sources no risk, linear chain no risk)Related