[AURON #2491] Carry Spark's logical link onto converted native plans - #2492
Open
weiqingy wants to merge 2 commits into
Open
[AURON #2491] Carry Spark's logical link onto converted native plans#2492weiqingy wants to merge 2 commits into
weiqingy wants to merge 2 commits into
Conversation
…plans Converting a plan node builds a new node, and Spark's logical link does not travel with it. AQE asserts that a new query stage's subtree carries one. Spark 3 never reaches that assertion on a repeated collect, because the adaptive plan returns its finalized form unchanged; Spark 4 rebuilds query stages on every collect and sees the plan after the columnar rules have run, so a fully native plan with no exchange in it carries no link anywhere and the assertion fires. Set the link on the node that replaces the original, alongside the tags the recursive walk already carries across. Only a replacement needs it: a node conversion returned unchanged already holds its link, and setting it again would promote an inherited tag to a primary one. The walk is the right place rather than tryConvert, which is not the only path that builds a replacement and which also runs during the throwaway conversion that computes tags.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a Spark 4 regression where native plan conversion drops Spark’s logicalLink, causing repeated execution of the same DataFrame (e.g., multiple collect() calls) to fail an AQE assertion when the final plan is fully native and exchange-free. It does this by explicitly propagating the source node’s logical link onto any newly constructed replacement node during recursive conversion, and adds targeted regression tests across Spark versions.
Changes:
- Propagate Spark
logicalLinkwhenconvertSparkPlanreturns a new (non-eq) replacement node duringconvertSparkPlanRecursively. - Add regression coverage ensuring repeated execution succeeds and that native operators retain per-operator logical links (including join linkage).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| spark-extension/src/main/scala/org/apache/spark/sql/auron/AuronConverters.scala | When conversion creates a new node instance, stamps the original node’s logicalLink onto the replacement via Shims.get.setLogicalLink. |
| spark-extension-shims-spark/src/test/scala/org/apache/auron/AuronQuerySuite.scala | Adds three tests covering repeated execution and validating logical link presence and per-operator distinctness (including join linkage). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… and above Before Spark 3.2, TreeNode.withNewChildren is overridable and reaches copyTagsFrom only by way of makeCopy. The native plan nodes override it with a plain copy of the case class, so no node tag survives a child rebuild on those versions. The converter sets the logical link on every version, but on 3.0 and 3.1 adaptive execution drops it again as soon as it substitutes a query stage into the tree, so the two assertions on the executed plan cannot hold there. They are now assumed away on those versions rather than asserted. The repeated execution test is unchanged and still runs everywhere.
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.
Which issue does this PR close?
Closes #2491
Rationale for this change
Converting a Spark plan node to a native one builds a new node, and Spark's
logicalLinkdoes not travel with it. On Spark 4 that is enough to fail a query.AdaptiveSparkPlanExec.setLogicalLinkForNewQueryStageasserts that a new query stage's subtree carries a logical link. The assertion is the same in 3.5 and 4.0; what differs is when it sees Auron's output. Spark 3.5 short-circuits a repeated collect, sincegetFinalPhysicalPlan()returns early onceisFinalPlanis set, so no new stage is created. Spark 4 re-enterscreateQueryStages(..., firstRun = true)on every collect, and the comment on that branch names the case directly ("e.g, when we dodf.collectmultiple times"). On that pass the plan handed to the assertion is the post-preColumnarTransitionsone, and the link is gone.So executing the same
DataFrametwice fails on Spark 4 whenever the final plan is fully native and has no exchange in it:Any shuffle in the tree masks it, because a
ShuffleQueryStageExecsatisfies the assertion on its own, and so does any partial fallback that leaves a Spark node carrying a link. Auron setsADAPTIVE_EXECUTION_FORCE_APPLY, so AQE is always in the path.The first collect is unaffected: it runs against the pre-columnar plan.
QueryStageExecis aLeafExecNode, so link-setting does not descend into a stage's inner plan either.What changes are included in this PR?
AuronConverters.convertSparkPlanRecursivelynow carries the original node's logical link onto the node that replaces it, alongside the four tags it already copies there.Shims.setLogicalLinkalready existed and is unchanged; this adds the call site.The link is set only when conversion actually returned a different instance. When it returns the same node, that node already carries its link through
copyTagsFrom, and setting it again would promote its inherited tag to a primary one and recurse into the original children.The call sits in the recursive walk rather than in
tryConvertfor two reasons.tryConvertis not the only path that builds a replacement —Shims.get.convertMoreSparkPlandoes so outside it. AndconvertSparkPlanruns twice per plan:AuronConvertStrategyconverts the whole tree once purely to compute tags and discards the result, so a call intryConvertwould also fire on that discarded pass.Some history, since the call is not new. It existed until
441a1a24(2022-12-12), which movedsetTagValue(convertibleTag, true)from the shim intotryConvertand dropped thesetLogicalLinkeffect along with it. The shim body of that era also recursed over children, though under a guard that made the recursion inert — Spark's ownsetLogicalLinkfans inherited tags downward first, so the guard never passed below the top node. The current shim has no such recursion.Are there any user-facing changes?
Executing the same
DataFramemore than once now succeeds on Spark 4, as it does on Spark 3 and as it does without Auron.Converted nodes now carry a logical link where they previously carried none. Spark's
setLogicalLinkwrites a primary tag on the node it is called on and fans inherited tags downward, halting at any descendant that already owns a primary tag, so a link is only ever added below a node that already had it — never moved upward. Both consumers that could be affected take the topmost match, so the answers they compute are unchanged. Every other reader oflogicalLinkinsql/coreruns before the columnar rules, and the only rule running after Auron isCollapseCodegenStages, which does not read it.One consumer was not proven unaffected and is worth watching rather than claiming:
RemoveRedundantProjects.canRemovekeys offlogicalLink.isEmpty, and a pre-columnarProjectExecthat gains an inherited tag stops qualifying. The rule lists were traced and no path was found where it re-runs on nodes Auron has touched, but that is an absence of evidence rather than a proof. TPC-DS plan stability is the check that would catch it.How was this patch tested?
Three cases in
AuronQuerySuite, on the same fully native, exchange-free fixture.The repeated execution case runs on every profile from spark-3.0 to spark-4.2. The two assertions on the executed plan run on spark-3.2 and above and are assumed away below it. Before Spark 3.2,
TreeNode.withNewChildrenis overridable and reachescopyTagsFromonly by way ofmakeCopy, and the native plan nodes override it with a plain copy of the case class, so no node tag survives a child rebuild there. The converter sets the link on every version, but on 3.0 and 3.1 adaptive execution drops it again as soon as it substitutes a query stage into the tree. That gap sits in the node overrides rather than in the conversion path, it predates this change, and it does not affect the bug being fixed, which is specific to Spark 4.DataFrameexecuted twice returns the right rows. This is the regression itself, and it is inert on Spark 3, where the second collect short-circuitsJoinEach was proved to pin what it claims by mutation, run module-wide against the whole suite rather than a single class:
The third mutation is why both assertions are kept: a shift mislabels every operator while leaving the distinct count intact, so a cardinality check alone would pass it.
Measured on spark-3.5/scala-2.12 and spark-4.0.2/scala-2.13: 150 → 152 passing on 3.5, 149 → 152 on 4.0, with exactly the three new tests changing state and no other test affected in either direction, confirmed by set difference rather than by counting.
The identity assertion matches the three native join base types rather than a concrete class, and was re-run with broadcast joins disabled and sort-merge preferred to confirm it does not depend on a join strategy.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)