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


##########
tests/spark-it/src/test/scala/org/apache/celeborn/tests/spark/CelebornShuffleCleanupSuite.scala:
##########
@@ -0,0 +1,430 @@
+/*
+ * 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 java.util.concurrent.{CountDownLatch, Executors, TimeUnit}
+import java.util.concurrent.atomic.{AtomicInteger, AtomicReference}
+
+import scala.concurrent.{Await, ExecutionContext, Future}
+import scala.concurrent.duration.Duration
+
+import org.apache.spark.{SparkConf, SparkEnv, SparkException}
+import org.apache.spark.scheduler.{SparkListener, SparkListenerEvent, 
SparkListenerJobStart}
+import org.apache.spark.shuffle.celeborn.SparkShuffleManager
+import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionEnd
+import org.apache.spark.sql.functions.{col, expr}
+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 CelebornShuffleCleanupSuite 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,
+      remaining: Int = 0): Unit = {
+    val deadline = System.currentTimeMillis() + 60000
+    while (lifecycleManager.registeredShuffle.size() > remaining &&
+      System.currentTimeMillis() < deadline) {
+      Thread.sleep(500)
+    }
+    assert(
+      lifecycleManager.registeredShuffle.size() <= remaining,
+      s"Shuffles should be unregistered after the SQL query completes, " +
+        s"remaining: ${lifecycleManager.registeredShuffle.size()}, expected: 
$remaining.")
+  }
+
+  // Runs a warmup query so that the cleanup listener is registered before the 
returned
+  // listener; the listener bus delivers events in registration order, so once 
the
+  // returned latch fires the cleanup listener has already processed the 
SQLExecutionEnd
+  // event of the query under test.
+  private def registerQueryEndLatch(spark: SparkSession): CountDownLatch = {
+    spark.range(0, 100, 1, 4).repartition(4).count()
+    awaitShufflesUnregistered(lifecycleManager())
+
+    val queryEndLatch = new CountDownLatch(1)
+    spark.sparkContext.addSparkListener(new SparkListener {
+      override def onOtherEvent(event: SparkListenerEvent): Unit = event match 
{
+        case _: SparkListenerSQLExecutionEnd => queryEndLatch.countDown()
+        case _ =>
+      }
+    })
+    queryEndLatch
+  }
+
+  private def registerFailUdf(spark: SparkSession): Unit = {
+    spark.udf.register(
+      "fail_udf",
+      (v: Long) => {
+        throw new RuntimeException("intentional failure for test")
+        v
+      })
+  }
+
+  private def shuffleCleanupConf(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
+  }
+
+  private val groupByQuery = "SELECT id % 10 AS k, COUNT(1) AS cnt FROM ta 
GROUP BY id % 10"
+
+  private def expectedGroupByResult: Map[Long, Long] = (0L until 10L).map(_ -> 
100L).toMap
+
+  Seq(true, false).foreach { aqeEnabled =>
+    test(s"CELEBORN-2465: shuffles are unregistered on SQL query completion, " 
+
+      s"aqeEnabled: $aqeEnabled") {
+      val spark = SparkSession.builder().config(shuffleCleanupConf(aqeEnabled))
+        .getOrCreate()
+
+      spark.range(0, 1000, 1, 4).createOrReplaceTempView("ta")
+      spark.sql("SELECT id % 10 AS k, COUNT(1) AS cnt FROM ta GROUP BY id % 
10").collect()
+      awaitShufflesUnregistered(lifecycleManager())
+
+      // A subsequent query must not be affected by the cleanup of the 
previous query.
+      val result = spark.sql(
+        """
+          |SELECT k, SUM(cnt) FROM (
+          |  SELECT id % 10 AS k, COUNT(1) AS cnt FROM ta GROUP BY id % 10
+          |) t GROUP BY k
+          |""".stripMargin).collect()
+      assert(result.length == 10)
+      awaitShufflesUnregistered(lifecycleManager())
+    }
+  }
+
+  test("CELEBORN-2465: shuffles are unregistered on SQL query completion " +
+    "with exchange reuse disabled, aqeEnabled: true") {
+    val sparkConf = shuffleCleanupConf(true)
+    sparkConf.set(SQLConf.EXCHANGE_REUSE_ENABLED.key, "false")
+    val spark = SparkSession.builder().config(sparkConf).getOrCreate()
+
+    // With exchange reuse off the AQE stage cache stays empty, so shuffles 
must be
+    // collected from materialized stages in the plan tree.
+    spark.range(0, 1000, 1, 4).createOrReplaceTempView("ta")
+    spark.sql("SELECT id % 10 AS k, COUNT(1) AS cnt FROM ta GROUP BY id % 
10").collect()
+    awaitShufflesUnregistered(lifecycleManager())

Review Comment:
   could we disable `spark.cleaner.referenceTracking` in this test so it proves 
query-end cleanup? `awaitShufflesUnregistered` waits up to 60s, so 
GC/ContextCleaner can make it pass even with the tree-walk fix removed. The 
earlier reuse-off repro already hit that fallback after about 17s.



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