dongjoon-hyun commented on code in PR #55839:
URL: https://github.com/apache/spark/pull/55839#discussion_r3815937714


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/AdaptiveSparkPlanExec.scala:
##########
@@ -397,6 +401,8 @@ case class AdaptiveSparkPlanExec(
                   currentPhysicalPlan.treeString, 
newPhysicalPlan.treeString).mkString("\n")
                 logOnLevel(log"Plan changed:\n${MDC(QUERY_PLAN, plans)}")
                 cleanUpTempTags(newPhysicalPlan)
+                obsoleteCancelledStageIds ++=
+                  cancelObsoleteStages(newPhysicalPlan, stagesToReplace)

Review Comment:
   `cancelObsoleteStages` only ever sees `stagesToReplace`, and 
`stagesToReplace` is reset three lines below at `:408` every time a plan is 
adopted. So this only covers stages created since the *last* adoption.
   
   `replaceWithQueryStagesInLogicalPlan` wraps a stage into a 
`LogicalQueryStage` regardless of whether it has materialized, so a stage that 
is still materializing can be folded into `currentLogicalPlan` by one adoption 
and then be invisible to the next. Concretely:
   
   - Round 1: stages A (empty, fast) and B (slow, still materializing) are 
created. A completes, a re-plan is adopted for an unrelated reason. B now lives 
in `currentLogicalPlan` as a `LogicalQueryStage`, and `stagesToReplace` is 
cleared.
   - Round 2: stage C materializes, re-planning proves B's branch empty and 
drops it from `newPhysicalPlan`.
   - `cancelObsoleteStages(newPhysicalPlan, stagesToReplace)` only sees `[C]`. 
B is neither cancelled nor added to `obsoleteCancelledStageIds`, so it keeps 
running; and if it later fails (task error, executor loss) `errors.append(ex)` 
at `:361` fires and `cleanUpAndThrowException` aborts the query over a stage 
that is no longer in any plan.
   
   All five new tests exercise a single adoption, so this isn't covered. If the 
intent is "cancel every exchange stage the adopted plan no longer references", 
the candidate set needs to come from `currentPhysicalPlan` before the swap 
rather than from `stagesToReplace` alone.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/AdaptiveSparkPlanExec.scala:
##########
@@ -420,6 +426,45 @@ case class AdaptiveSparkPlanExec(
       .get.asInstanceOf[T]
   }
 
+  private def cancelObsoleteStages(
+      newPhysicalPlan: SparkPlan,
+      stagesToReplace: Seq[QueryStageExec]): Seq[Int] = {
+    val newStages = newPhysicalPlan.collect {
+      case stage: QueryStageExec => stage
+    }
+    val obsoleteStages = stagesToReplace.collect {
+      case stage: ExchangeQueryStageExec
+          if !newStages.exists(newStage =>
+            newStage.id == stage.id || 
newStage.resultOption.eq(stage.resultOption)) => stage
+    }
+    obsoleteStages.flatMap { stage =>
+      context.withStageLifecycleLock {

Review Comment:
   Holding `stageLifecycleLock` across `stage.cancel(...)` keeps a query-global 
lock over a call that can block for a long time.
   
   `ShuffleQueryStageExec.doCancel` -> `cancelShuffleJob` is 
`this.synchronized` on the exchange node (`ShuffleExchangeExec.scala:129`). The 
same monitor is held by `triggerFuture` while it runs 
`mapOutputStatisticsFuture` (`ShuffleExchangeExec.scala:92-105`), which 
evaluates `inputRDD.getNumPartitions` and `shuffleDependency` 
(`ShuffleExchangeExec.scala:227-233`) -- for a file-scan child that is 
partition planning and file listing, which can take seconds.
   
   While the AQE main thread waits on that monitor, every other thread sharing 
this `AdaptiveExecutionContext` -- each subquery's `AdaptiveSparkPlanExec` -- 
is blocked in `createNonResultQueryStages` at `:672` / `:699` waiting for 
`stageLifecycleLock`. Nothing held a shared lock across job teardown before 
this PR.
   
   The race the lock closes is between the `stageCache` lookup / 
`markSharedStageResult` and the cache eviction, so keeping the `isMaterialized` 
/ `isSharedStageResult` checks and `removeStageFromCache` inside the lock while 
moving `stage.cancel(...)` outside it would preserve the invariant without 
serialising stage creation on job cancellation. That would also take the 
quadratic `stageCache` scan @cloud-fan flagged at `:437` out of the critical 
section.



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