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


##########
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:
   Fixed in `ef44aa21596`, though not by the design you prescribed. The hole is 
real, and I turned it into a test before changing anything: with the reason on 
a tag, `rebuilt.supportCodegen` came back true after `withNewChildren` 
installed a child the gate rejects.
   
   Instead of moving both decisions into per-instance state with a handoff from 
`CollapseCodegenStages`, I split the reason by what makes each term move. The 
two confs stay latched in a tag, since they move only because the read happens 
at a different time. Everything that reads the children (nested union, 
multi-RDD child, partition-index dependence, child count, `supportsColumnar`, 
type mismatch) is memoized per instance, so a copy starts cold and answers 
against its own children.
   
   The copy in the codegen shell still agrees with the gate, and by 
construction rather than by routing: `InputAdapter` delegates `output` and 
`supportsColumnar` to its child, the other terms walk the subtree through it, 
and each of them is fixed for a given set of children 
(`InMemoryTableScanExec.supportsColumnar` is an `override val`, 
`AdaptiveSparkPlanExec`'s is a constructor val). So this needs no 
`UnionExec`-shaped hook inside a generic rule, and the guarantee does not rest 
on `insertInputAdapter` remaining the only rebuild path between the gate and 
execution. The new case is a plain unit test with no injected extension: force 
`metrics`, hand the node a nested union through `withNewChildren`, assert the 
gate flips.
   
   `isPlainUnion` stays latched on purpose. Stale-plain is the conservative 
direction (the node reports `UnknownPartitioning`), per-plan stability is what 
fixes the wrong answer this PR is about, and making it per-instance would let 
`CoalesceShufflePartitions`' grouping decision and a later rebuilt node 
disagree again. Glad to reconsider if you read that one differently.
   



-- 
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