venkata91 commented on code in PR #3853:
URL: https://github.com/apache/celeborn/pull/3853#discussion_r4202824519


##########
client-spark/spark-3/src/main/scala/org/apache/spark/sql/celeborn/CelebornQueryEndShuffleCleaner.scala:
##########
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.celeborn
+
+import org.apache.spark.SparkEnv
+import org.apache.spark.internal.Logging
+import org.apache.spark.scheduler.{SparkListener, SparkListenerEvent}
+import org.apache.spark.shuffle.celeborn.SparkShuffleManager
+import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanExec, 
ShuffleQueryStageExec}
+import org.apache.spark.sql.execution.exchange.{ReusedExchangeExec, 
ShuffleExchangeExec}
+import org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionEnd
+
+class CelebornQueryEndShuffleCleaner extends SparkListener with Logging {
+
+  override def onOtherEvent(event: SparkListenerEvent): Unit = {
+    event match {
+      case end: SparkListenerSQLExecutionEnd =>
+        try {
+          cleanupShufflesOnQueryEnd(end)
+        } catch {
+          case t: Throwable =>
+            logWarning(
+              s"Failed to cleanup shuffles on completion of SQL execution 
${end.executionId}.",
+              t)
+        }
+      case _ =>
+    }
+  }
+
+  private def extractShuffleIds(plan: SparkPlan): Seq[Int] = {
+    plan.collect {
+      case adaptivePlan: AdaptiveSparkPlanExec => 
extractShuffleIds(adaptivePlan.executedPlan)
+      case stage: ShuffleQueryStageExec => extractShuffleIds(stage.plan)
+      case reused: ReusedExchangeExec => extractShuffleIds(reused.child)
+      case exchange: ShuffleExchangeExec => 
Seq(exchange.shuffleDependency.shuffleId)

Review Comment:
   `ShuffleExchangeExec.shuffleDependency` is a lazy val. For exchanges that 
never materialized (e.g. an AQE query that failed or was cancelled before 
creating that stage), touching it runs `child.execute()`, allocates a new 
shuffle id and calls `shuffleManager.registerShuffle`, all on the listener-bus 
thread. Could we only collect ids from materialized stages (e.g. 
`ShuffleQueryStageExec` with `isMaterialized`)?
   
   Also: `collectWithSubqueries` would cover scalar-subquery shuffles, and 
non-vanilla `ShuffleExchangeLike` implementations (e.g. Gluten's columnar 
exchange) aren't handled.



##########
client-spark/spark-3/src/main/scala/org/apache/spark/sql/celeborn/CelebornQueryEndShuffleCleaner.scala:
##########
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.celeborn
+
+import org.apache.spark.SparkEnv
+import org.apache.spark.internal.Logging
+import org.apache.spark.scheduler.{SparkListener, SparkListenerEvent}
+import org.apache.spark.shuffle.celeborn.SparkShuffleManager
+import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanExec, 
ShuffleQueryStageExec}
+import org.apache.spark.sql.execution.exchange.{ReusedExchangeExec, 
ShuffleExchangeExec}
+import org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionEnd
+
+class CelebornQueryEndShuffleCleaner extends SparkListener with Logging {
+
+  override def onOtherEvent(event: SparkListenerEvent): Unit = {
+    event match {
+      case end: SparkListenerSQLExecutionEnd =>
+        try {
+          cleanupShufflesOnQueryEnd(end)
+        } catch {
+          case t: Throwable =>
+            logWarning(
+              s"Failed to cleanup shuffles on completion of SQL execution 
${end.executionId}.",
+              t)
+        }
+      case _ =>
+    }
+  }
+
+  private def extractShuffleIds(plan: SparkPlan): Seq[Int] = {
+    plan.collect {
+      case adaptivePlan: AdaptiveSparkPlanExec => 
extractShuffleIds(adaptivePlan.executedPlan)
+      case stage: ShuffleQueryStageExec => extractShuffleIds(stage.plan)

Review Comment:
   On Spark 4.x the final AQE plan is wrapped in `ResultQueryStageExec` (also 
`BroadcastQueryStageExec` here), both leaf `QueryStageExec`s, so `collect` 
doesn't descend into them. Matching `case stage: QueryStageExec => 
extractShuffleIds(stage.plan)` fixed all AQE tests for me on `-Pspark-4.0`. 
`TableCacheQueryStageExec.plan` is an `InMemoryTableScanExec`, so cached plans 
still won't be touched.



##########
client-spark/spark-3/src/main/scala/org/apache/spark/sql/celeborn/CelebornQueryEndShuffleCleaner.scala:
##########
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.celeborn
+
+import org.apache.spark.SparkEnv
+import org.apache.spark.internal.Logging
+import org.apache.spark.scheduler.{SparkListener, SparkListenerEvent}
+import org.apache.spark.shuffle.celeborn.SparkShuffleManager
+import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanExec, 
ShuffleQueryStageExec}
+import org.apache.spark.sql.execution.exchange.{ReusedExchangeExec, 
ShuffleExchangeExec}
+import org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionEnd
+
+class CelebornQueryEndShuffleCleaner extends SparkListener with Logging {
+
+  override def onOtherEvent(event: SparkListenerEvent): Unit = {
+    event match {
+      case end: SparkListenerSQLExecutionEnd =>
+        try {
+          cleanupShufflesOnQueryEnd(end)
+        } catch {
+          case t: Throwable =>
+            logWarning(
+              s"Failed to cleanup shuffles on completion of SQL execution 
${end.executionId}.",
+              t)
+        }
+      case _ =>
+    }
+  }
+
+  private def extractShuffleIds(plan: SparkPlan): Seq[Int] = {
+    plan.collect {
+      case adaptivePlan: AdaptiveSparkPlanExec => 
extractShuffleIds(adaptivePlan.executedPlan)
+      case stage: ShuffleQueryStageExec => extractShuffleIds(stage.plan)
+      case reused: ReusedExchangeExec => extractShuffleIds(reused.child)
+      case exchange: ShuffleExchangeExec => 
Seq(exchange.shuffleDependency.shuffleId)
+    }.flatten.distinct
+  }
+
+  private def cleanupShufflesOnQueryEnd(end: SparkListenerSQLExecutionEnd): 
Unit = {
+    val qe = end.qe
+    if (qe == null) {
+      logDebug(
+        s"QueryExecution is null in SparkListenerSQLExecutionEnd 
${end.executionId}, skipping.")
+      return
+    }
+
+    val lifecycleManager =
+      
SparkEnv.get.shuffleManager.asInstanceOf[SparkShuffleManager].getLifecycleManager
+    val shuffleIds = extractShuffleIds(qe.executedPlan)

Review Comment:
   If planning itself failed, `qe.executedPlan` will re-trigger planning here 
on the listener thread and throw again (Spark 4 guards this with 
`isExecutedPlanAvailable`). Nit: could the LifecycleManager be passed into the 
listener's constructor instead of 
`SparkEnv.get.shuffleManager.asInstanceOf[...]` on every event? Also maybe 
`NonFatal` instead of `Throwable` at L37.



##########
tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/CelebornQueryEndShuffleCleanupSuite.scala:
##########
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.celeborn.tests.spark
+
+import org.apache.spark.{SparkConf, SparkEnv, SparkException}
+import org.apache.spark.shuffle.celeborn.SparkShuffleManager
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.internal.SQLConf
+import org.scalatest.BeforeAndAfterEach
+import org.scalatest.funsuite.AnyFunSuite
+
+import org.apache.celeborn.client.{LifecycleManager, ShuffleClient}
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.protocol.ShuffleMode
+
+class CelebornQueryEndShuffleCleanupSuite extends AnyFunSuite
+  with SparkTestBase
+  with BeforeAndAfterEach {
+
+  override def beforeEach(): Unit = {
+    ShuffleClient.reset()
+  }
+
+  override def afterEach(): Unit = {
+    // Always stop the session so that a failed test never leaks its 
SparkSession
+    // (getOrCreate would otherwise reuse it in the next test).
+    stopActiveSparkSessions()
+  }
+
+  private def lifecycleManager(): LifecycleManager = {
+    
SparkEnv.get.shuffleManager.asInstanceOf[SparkShuffleManager].getLifecycleManager
+  }
+
+  private def awaitShufflesUnregistered(lifecycleManager: LifecycleManager): 
Unit = {
+    val deadline = System.currentTimeMillis() + 60000
+    while (!lifecycleManager.registeredShuffle.isEmpty &&
+      System.currentTimeMillis() < deadline) {
+      Thread.sleep(500)
+    }
+    assert(
+      lifecycleManager.registeredShuffle.isEmpty,
+      "Shuffles should be unregistered after the SQL query completes.")
+  }
+
+  private def queryEndShuffleCleanupConf(aqeEnabled: Boolean): SparkConf = {
+    val sparkConf = updateSparkConf(
+      new SparkConf().setAppName("celeborn-test").setMaster("local[2]"),
+      ShuffleMode.HASH)
+    sparkConf.set(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key, aqeEnabled.toString)
+    sparkConf.set(
+      
s"spark.${CelebornConf.CLIENT_SPARK_SQL_QUERY_END_SHUFFLE_CLEANUP_ENABLED.key}",
+      "true")
+    // Speed up the delayed unregister inside LifecycleManager.
+    sparkConf.set(s"spark.${CelebornConf.SHUFFLE_EXPIRED_CHECK_INTERVAL.key}", 
"1s")
+    sparkConf.set("spark.sql.autoBroadcastJoinThreshold", "-1")
+    sparkConf
+  }
+
+  Seq(true, false).foreach { aqeEnabled =>

Review Comment:
   Could we add cases for re-executing the same DataFrame (sequentially and 
concurrently, with stageRerun on/off) and for a failed AQE query? Nit: 
`spark.stop()` at the end of each test is redundant with `afterEach`.



##########
common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala:
##########
@@ -5273,6 +5275,18 @@ object CelebornConf extends Logging {
       .booleanConf
       .createWithDefault(true)
 
+  val CLIENT_SPARK_SQL_QUERY_END_SHUFFLE_CLEANUP_ENABLED: ConfigEntry[Boolean] 
=
+    buildConf("celeborn.client.spark.queryEndShuffleCleanup.enabled")

Review Comment:
   Nit: the PR description says 
`celeborn.client.spark.sql.queryEndShuffleCleanup.enabled`, but the key here is 
`celeborn.client.spark.queryEndShuffleCleanup.enabled`, while the val is 
`CLIENT_SPARK_SQL_...`. Please align them. Also see the top-level comment about 
enforcing `stageRerun.enabled` instead of only recommending 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]

Reply via email to