sunchao commented on code in PR #55839:
URL: https://github.com/apache/spark/pull/55839#discussion_r3816463036
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/AQEPropagateEmptyRelation.scala:
##########
@@ -52,19 +53,46 @@ object AQEPropagateEmptyRelation extends
PropagateEmptyRelationBase {
// - positive value means an estimated row count which can be
over-estimated
// - none means the plan has not materialized or the plan can not be
estimated
private def getEstimatedRowCount(plan: LogicalPlan): Option[BigInt] = plan
match {
- case LogicalQueryStage(_, stage: QueryStageExec) if stage.isMaterialized =>
+ case LogicalQueryStage(_, physicalPlan) =>
+ getEstimatedRowCount(physicalPlan)
+
+ case _: EmptyRelation => Some(0)
+
+ case _ => None
+ }
+
+ private def getEstimatedRowCount(plan: SparkPlan): Option[BigInt] = plan
match {
Review Comment:
I kept the correctness-sensitive helper separate and documented why
`LogicalQueryStage.computeStats()` cannot be reused directly here.
`computeStats()` walks through arbitrary physical operators and may fall back
to logical estimates; that is acceptable for planning statistics, but using it
to prove emptiness/nonemptiness could incorrectly cross a filter or join and
change query results. This rule instead accepts runtime statistics only from a
materialized stage through an explicit allowlist and applies the
global-aggregate one-row correction locally. Consolidating the helpers would
first require changing the broader statistics contract, which is outside this
patch.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/adaptive/AdaptiveSparkPlanExec.scala:
##########
@@ -986,6 +1042,23 @@ case class AdaptiveExecutionContext(session:
SparkSession, qe: QueryExecution) {
val stageCache: TrieMap[SparkPlan, ExchangeQueryStageExec] =
new TrieMap[SparkPlan, ExchangeQueryStageExec]()
+ private val stageLifecycleLock = new Object
+
+ private[adaptive] def withStageLifecycleLock[T](body: => T): T = {
+ stageLifecycleLock.synchronized(body)
+ }
+
+ private val sharedStageResults =
+ new ConcurrentHashMap[AtomicReference[Option[Any]], Boolean]()
+
+ def markSharedStageResult(resultOption: AtomicReference[Option[Any]]): Unit
= {
+ sharedStageResults.put(resultOption, true)
+ }
+
+ def isSharedStageResult(resultOption: AtomicReference[Option[Any]]): Boolean
= {
Review Comment:
Fixed. Both `markSharedStageResult` and `isSharedStageResult` are now
`private[adaptive]`, matching the lifecycle-lock helper and keeping this
bookkeeping off the public `AdaptiveExecutionContext` surface.
##########
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 =>
Review Comment:
Fixed. The adopted physical plan is scanned once to build the retained
stage-ID and result-identity sets, and obsolete candidates are checked against
those sets without a nested `exists`. Cache removal uses the exchange's
canonicalized key rather than scanning the entire `stageCache` for every
obsolete stage. The stage-lifecycle critical section is also kept clear of a
blocking shuffle-submission/cancellation wait.
##########
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:
Agreed. `stagesToReplace` only describes the current adoption window, so it
cannot be the sole source of obsolete-stage candidates. Candidate discovery now
unions and deduplicates stages from the previous physical plan and the latest
replacement batch before comparing them with the newly adopted plan. The
existing materialized/shared-stage guards remain in place; an obsolete
broadcast or identifiable cleanup-protected shuffle that cannot safely be
cancelled is still tracked so a later ordinary failure cannot abort the query
once the stage is confirmed private and unreferenced. Opaque delegated shuffles
remain excluded because their hidden delegate shuffle IDs cannot safely be
registered. The deterministic regression constructs an old physical plan
containing an unfinished stage absent from the latest replacement batch, checks
candidate deduplication, verifies that the old stage is cancelled and its later
ordinary failure is ignored, and confirms that the newly retained stage stays
cached.
##########
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:
Agreed that a slow shuffle monitor must not stall every adaptive subquery
sharing the context. Cancellation now claims a stage-result-scoped
`CompletableFuture` reservation; exchange-cache lookup, reuse, and
shared-result marking stay under short global-lock sections, while
shuffle-monitor acquisition and `stage.cancel()` occur outside that lock. Both
cache lookup paths wait for same-result completion outside the global lock and
retry, while unrelated exchanges continue. Deterministic regressions cover a
blocked generic cancellation, a genuinely held shuffle monitor, same-result
reuse, unrelated exchange progress, and a failed cancellation whose waiting
consumer reuses the original cached stage. Atomic shuffle-submission
eligibility checks, `RemoveShuffleFiles` protections, fatal-error propagation,
guarded cleanup of identifiable failed obsolete stages, and conservative
propagation of opaque delegated-shuffle failures remain intact.
--
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]