LuciferYang commented on code in PR #58419:
URL: https://github.com/apache/spark/pull/58419#discussion_r3995829265


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala:
##########
@@ -1044,11 +1091,21 @@ case class UnionExec(children: Seq[SparkPlan]) extends 
SparkPlan with CodegenSup
       }
     }
 
-  // Memoized: consulted by `supportCodegen` (called multiple times by
-  // `CollapseCodegenStages`) and by `metrics`. Conf and children are stable
-  // for a given UnionExec instance; cross-plan staleness is impossible since
-  // UnionExec is a case class and `withNewChildren` produces a fresh instance.
-  @transient private lazy val supportCodegenFailureReason: Option[String] = {
+  // Latched for the same reason `isPlainUnion` is: `supportCodegen` and 
`metrics` must see one
+  // answer, and `conf` is live. When a child is not `CodegenSupport`, 
`insertInputAdapter` wraps
+  // it, so `withNewChildren` returns a real copy whose first evaluation of 
this would land at
+  // execution; re-deriving there left `metrics` empty while `doProduce` asked 
`metricTerm` for
+  // `numOutputRows`. The first force is not always the gate: under AQE it is 
a plan-update event
+  // on the pre-stage-creation tree, so a term added here sees more of the 
plan than the gate does.
+  private def supportCodegenFailureReason: Option[String] = 
decisionLock.synchronized {
+    getTagValue(UnionExec.CODEGEN_FAILURE_REASON).getOrElse {

Review Comment:
   Done in `3ed87a98166`, with a trim in `773c28999eb`. `StampUnionDecisions` 
runs right after `EnsureRequirements` in both `QueryExecution.preparations` and 
AQE's `queryStagePreparationRules`, and nothing else writes the decision, so a 
read before that point answers from the state it sees and decides nothing.
   
   Both halves reproduce as test failures before the change. Reading 
`queryExecution.sparkPlan`'s union partitioning left the prepared plan 
reporting `UnknownPartitioning(0)`, the pre-`EnsureRequirements` answer riding 
in through `clone` and `copyTagsFrom`. And with whole-stage codegen off over a 
root union, where nothing consults the node during preparation, a conf flip 
afterwards moved the executed layout from four partitions to eight.
   
   I put the stamp immediately after `EnsureRequirements` rather than at the 
end of preparation. Late stamping has a worse failure: `EnsureRequirements` can 
elide a parent's exchange on the strength of a concrete pass-through, and a 
stamp taken afterwards could freeze plain, leaving the union to concatenate 
under a parent that no longer shuffles. Stamping early can only freeze an 
answer a later rule would have sharpened, which costs an exchange elision 
rather than a result, and every rule below the stamp then reads the frozen 
value, including the `EnsureRequirements` re-runs inside `OptimizeSkewedJoin`.
   
   Your constraint about pre-preparation reads sent me back to the codegen 
confs, which were still pinned on first read. They are stamp-only now too, so 
the whole decision is one immutable value written in one place. That also left 
`decisionLock` with nothing to serialize, so it and its lock-order comment are 
gone, and the two tags became one `Decisions(plainUnion, unionCodegenEnabled, 
maxChildren)`, written by a single `setTagValue`, so a concurrent reader can no 
longer see half a decision. The cost of the read-no-write change is that a 
`UnionExec` built after the stamp point by an injected rule no longer gets the 
conf pinned by the gate's own read, so for that node a conf flip can still 
reach the copy in the codegen shell. It seemed the right way round, since such 
a node has no protection on the partitioning half either.
   
   On the ordering question: reordering the gate does not buy anything once the 
decision is stamped, because every `UnionExec` is asked during preparation 
whether or not the gate would reject it, and `isPlainUnion` no longer writes. I 
tried it and reverted it in `dc85f998986`; all it changed was the reason 
reported for a union failing several gates.
   
   I cannot confirm the other half of your question, and I think the answer is 
the opposite of hopeful. The stamp lands after `EnsureRequirements`, which is 
still before a cached child's inner AQE plan finalises, so such a union is 
stamped plain and keeps reporting `UnknownPartitioning` for the rest of the 
query. `UnionCodegenSuite`'s first `SPARK-59122` case shows it: after 
`collect()` the fused union reports `UnknownPartitioning` while its children 
have both reached the same concrete layout, which the test now asserts as the 
premise of that check. So the elimination is not merely unavailable already, it 
is given up. Deferring past that point means deciding at execution, which is 
the crash this PR fixes; `outputPartitioning`'s scaladoc records the trade 
rather than hiding it.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to