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


##########
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:
   Three cases added in `SparkSessionExtensionSuite`, each building a 
`UnionExec` through a real hook: an injected columnar rule with AQE off, an 
injected query stage prep rule, and the columnar hook inside AQE post stage 
creation. Each one prepares a union over a repartition, then turns 
`spark.sql.unionOutputPartitioning` off and reads the node again. A stamped 
node keeps answering from the decision it was prepared with; one no barrier 
reached derives a decision at that read and comes back `UnknownPartitioning`.
   
   I then removed the barriers one at a time to see what each case actually 
pins:
   
   | removed | columnar, AQE off | prep rule | post stage creation |
   | --- | --- | --- | --- |
   | `preparations`, after the columnar rules | **fails** | passes | passes |
   | AQE, after the injected prep rules | passes | passes | passes |
   | AQE `postStageCreationRules` | passes | passes | **fails** |
   | both AQE listings | passes | **fails** | **fails** |
   
   Two of the listings are pinned one for one. The AQE pair is pinned jointly, 
because the barrier in `postStageCreationRules` stands behind the one after the 
prep rules: nothing an injected prep rule creates can reach execution undecided 
even with that listing gone, so a case that fails on it alone does not exist. I 
would rather tell you that than write one that looks like it does. What the 
earlier listing buys is upstream of execution: 
`CoalesceShufflePartitions.childrenNeedCompatiblePartitioning` reads 
`isPlainUnion` to decide whether a union's children coalesce as one group, and 
it runs before the post-stage barrier, so stamping earlier keeps that reader 
and the final decision in step. The test name and comment say exactly that much 
and no more.
   
   An injected stage-optimizer rule needs no case of its own: it runs ahead of 
the same `postStageCreationRules` barrier, which the third case pins.
   
   Your two nits are in as well. `rawPartitioning`'s scaladoc now describes it 
as the child-derived candidate and gives the plain answer's two grounds 
separately, and the `whereas`.
   



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