Repository navigation
Conversation
|
/gemini review |
There was a problem hiding this comment.
📋 Review Summary
This PR resolves a race condition on the driver in Spark 4 Structured Streaming when multiple sibling leaf StreamingQuery instances share an upstream TransformWithState logical plan node. By deep-cloning the StatefulProcessor instances in TransformWithState nodes for subsequent leaf queries, each query's StreamExecution thread operates on an isolated StatefulProcessor handle during planning. The fix is well-targeted, clean, and accompanied by a multi-leaf streaming unit test.
🔍 General Feedback
- The recursive traversal via
LogicalPlan.mapChildrenandTransformWithState.copywithSerializableUtils.clonecleanly isolates the non-thread-safeStatefulProcessorstate per query without affecting single-leaf pipelines (leafIndex == 0). WindowedGroupByKeyStreamingTest.testGroupByKeySharedAcrossMultipleLeaveseffectively exercises the concurrent multi-leaf query execution path.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #41137 +/- ##
=========================================
Coverage 57.53% 57.53%
Complexity 2502 2502
=========================================
Files 1207 1207
Lines 171557 171557
Branches 2121 2121
=========================================
Hits 98710 98710
Misses 70187 70187
Partials 2660 2660 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
R: @tkaymak |
|
Assigning reviewers: R: @kennknowles added as fallback since no labels match configuration Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
|
Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment |
There was a problem hiding this comment.
Thanks for chasing this down, the diagnosis is right and one StatefulProcessor per query thread is the invariant we need. Two things before I can approve:
SerializableUtils.clonealso copies theMetricsAccumulatorthatParDoStatefulProcessorholds. Spark only merges the accumulator the driver registered (see the note inMetricsAccumulator.getInstance), so a stateful ParDo in every leaf after the first would stop reporting metrics.- The copies for leaves 1..n are taken after query 0 is started on the original instance, and
StatefulProcessorkeeps its handle in a non transient field that query 0 sets and clears while planning its first batch. That narrows the race, it doesn't remove it.
Both go away if BeamStatefulProcessor gets a copy method that the two subclasses implement with their constructor and the same final fields, instead of a Java serialization round trip. It keeps the registered accumulator and reads no Spark handle, so the order against query 0 no longer matters.
Smaller points: could you align ofRows with the existing Dataset$.MODULE$ call sites (BoundedDatasetFactory, UnboundedSourceDataset), add a short comment on why leaf 0 keeps the original, and mention #41198 as the long term fix (one query per pipeline would remove this code). Does the new test fail without the change? A stateful ParDo with a counter and two leaves would also cover point 1.
And could you please bump the Spark 4 ValidatesRunner trigger file so the PostCommit runs here?
Thanks - I think I addressed the feedback. I am a little out of my wheelhouse here (this is part of a broader effort I'm working on to automate away some of our test flakes/toil around that), so there may be pieces I'm missing. If so, feel free to either help me through it or try to fork/fix yourself as you have time available - whatever is easiest for you depending on your availability. Regardless, it would be good to clean up the failures introduced here. |
Yes, both do |
There was a problem hiding this comment.
Thanks, this addresses everything. The copies keep the registered accumulator and the new stateful test proves both leaves report metrics. LGTM.
One small thing for later: the expected 10 in testStatefulParDoSharedAcrossMultipleLeaves is 5 per leaf because each leaf query runs the stateful ParDo, a short comment pointing at #41198 would help whoever fixes that (I am happy to do that too), since it then drops to 5.
…DoSharedAcrossMultipleLeaves
Thanks, added |
Note: this PR was auto-generated by tooling. Before moving it out of draft, the author has done a full review of the PR, specifically evaluating it on the following criteria:
Full details of the methodology of generation and review can be found here - https://docs.google.com/document/d/14lj7ECVov4T4EdwmIzNlWrRGE3lLDMOwX5xUMGzaLoc/edit?usp=sharing
If you have concerns with the PR or with the underlying methodology used to generate this, please comment below - they will be addressed by the author not the tooling :)
Summary
Fixes a driver concurrency race in Spark 4 Structured Streaming when multiple leaf
StreamingQueryinstances share an upstreamTransformWithStatelogical plan node (such asGroupByKeyor statefulParDoconsumed by multiplePAssertqueries inGroupByKeyTest$WindowTests.testGroupByKeyMultipleWindows).Production Impact
Affects production Spark 4 Structured Streaming pipelines where a stateful transform (
GroupByKeyor statefulParDo) is consumed by multiple downstream outputs/sinks. Because Spark starts a separateStreamingQueryper leaf output that shares the upstream logical plan, sharing a single mutableBeamStatefulProcessorinstance across sibling queries causes concurrent driver micro-batch planning threads to overwrite and clearstatefulProcessorHandle = nullmid-initialization, crashing the pipeline withSparkRuntimeException: [STATE_STORE_HANDLE_NOT_INITIALIZED].Root Cause
Failed master run: https://github.com/apache/beam/actions/runs/42658100679/job/129301949163 (
PostCommit Java ValidatesRunner Spark4).In Spark 4 (
org.apache.spark.sql.execution.streaming.TransformWithStateExec),getDriverProcessorHandle()andcloseProcessorHandle()are invoked on the driver by eachStreamingQuery'sStreamExecutionthread during micro-batch planning (IncrementalExecution$StateSchemaAndOperatorMetadataRule->getColFamilySchemas/getStateVariableInfos):getDriverProcessorHandle()callsstatefulProcessor.setHandle(driverProcessorHandle)followed bystatefulProcessor.init(outputMode, timeMode).closeProcessorHandle()callsstatefulProcessor.close()followed bystatefulProcessor.setHandle(null).Because
StatefulProcessor.setHandleandStatefulProcessor.getHandlearepublic finalmethods that mutate a single non-thread-safeprivate StatefulProcessorHandle statefulProcessorHandlefield on theStatefulProcessorinstance, sharing the sameTransformWithStatelogical plan node across sibling leafStreamingQuerys causes theirStreamExecutionthreads to concurrently set and clearstatefulProcessorHandle = nullmid-init(), throwing:Fix
StreamingEvaluationContext.startQuery, cloneStatefulProcessorinstances insideTransformWithStatelogical plan nodes for additional leaf queries (leafIndex > 0) viaSerializableUtils.clone(tws.statefulProcessor())so each leafStreamingQueryhas its own isolatedStatefulProcessorinstance on the driver.WindowedGroupByKeyStreamingTest.testGroupByKeySharedAcrossMultipleLeavesto verify multiplePAssertleaf queries consuming a sharedGroupByKey.Verification
beam_PostCommit_Java_ValidatesRunner_Spark4.ymlworkflow run on branch:42696643750