This is an automated email from the ASF dual-hosted git repository.

zhouyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new d1aa092ebf [GLUTEN-13222][VL] Fix race in WithQueryPlanListener 
causing flaky NPE (#13250)
d1aa092ebf is described below

commit d1aa092ebfa4a7115077a58be1cb94e3abed58be
Author: Shivadarshan Devadiga <[email protected]>
AuthorDate: Wed Oct 14 00:12:32 2026 +0530

    [GLUTEN-13222][VL] Fix race in WithQueryPlanListener causing flaky NPE 
(#13250)
---
 .../apache/spark/sql/WithQueryPlanListener.scala   | 34 ++++++++++++++++++----
 1 file changed, 28 insertions(+), 6 deletions(-)

diff --git 
a/gluten-substrait/src/test/scala/org/apache/spark/sql/WithQueryPlanListener.scala
 
b/gluten-substrait/src/test/scala/org/apache/spark/sql/WithQueryPlanListener.scala
index 4fa381b532..412439b9a6 100644
--- 
a/gluten-substrait/src/test/scala/org/apache/spark/sql/WithQueryPlanListener.scala
+++ 
b/gluten-substrait/src/test/scala/org/apache/spark/sql/WithQueryPlanListener.scala
@@ -28,6 +28,8 @@ import org.scalactic.source.Position
 import org.scalatest.Tag
 import org.scalatest.funsuite.AnyFunSuiteLike
 
+import java.util.concurrent.TimeUnit
+
 import scala.collection.mutable
 import scala.reflect.ClassTag
 // format: off
@@ -53,6 +55,8 @@ trait WithQueryPlanListener extends SharedSparkSession with 
AnyFunSuiteLike {
       pos: Position): Unit = {
     super.test(testName, testTags: _*) {
       testFun
+      // Query execution listeners are notified asynchronously; wait for all 
the plans.
+      waitForListenerBus()
       listeners().foreach(_.invokeAll())
     }(pos)
   }
@@ -63,10 +67,19 @@ trait WithQueryPlanListener extends SharedSparkSession with 
AnyFunSuiteLike {
   }
 
   override def afterEach(): Unit = {
-    listeners().foreach(listener => 
spark.sessionState.listenerManager.unregister(listener))
-    super.afterEach()
+    // Let any in-flight notifications finish before unregistering, e.g. when 
the test failed.
+    try {
+      waitForListenerBus()
+    } finally {
+      listeners().foreach(listener => 
spark.sessionState.listenerManager.unregister(listener))
+      super.afterEach()
+    }
   }
 
+  // The no-arg waitUntilEmpty() gives up after 10s, which a slow CI runner 
can exceed.
+  private def waitForListenerBus(): Unit =
+    
spark.sparkContext.listenerBus.waitUntilEmpty(TimeUnit.SECONDS.toMillis(30))
+
   private def listeners() = {
     spark.sessionState.listenerManager
       .listListeners()
@@ -83,20 +96,29 @@ object WithQueryPlanListener {
     with AdaptiveSparkPlanHelper {
     private val plans = mutable.ArrayBuffer[SparkPlan]()
 
+    // Called on the listener bus thread, while invokeAll() runs on the test 
thread.
     override def onSuccess(funcName: String, qe: QueryExecution, durationNs: 
Long): Unit = {
       val plan = qe.executedPlan
-      plans += plan
+      plans.synchronized {
+        plans += plan
+      }
     }
 
     override def onFailure(funcName: String, qe: QueryExecution, exception: 
Exception): Unit = {}
 
     def invokeAll(): Unit = {
-      while (plans.nonEmpty) {
-        val plansCopy = Seq[SparkPlan](plans.toSeq: _*)
-        plans.clear()
+      var plansCopy = takePlans()
+      while (plansCopy.nonEmpty) {
         plansCopy.foreach(plan => listeners.foreach(listener => 
listener.apply(plan)))
+        plansCopy = takePlans()
       }
     }
+
+    private def takePlans(): Seq[SparkPlan] = plans.synchronized {
+      val plansCopy = plans.toList
+      plans.clear()
+      plansCopy
+    }
   }
 
   class QueryPlanListeners private[WithQueryPlanListener] {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to