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

HyukjinKwon pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 1306da8274f0 [SPARK-57656][CONNECT][TEST] Stabilize flaky 
SparkSessionE2ESuite interrupt-all tests
1306da8274f0 is described below

commit 1306da8274f0add5e8b80ffa4517247873b6b52d
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Wed Jun 24 19:44:35 2026 +0900

    [SPARK-57656][CONNECT][TEST] Stabilize flaky SparkSessionE2ESuite 
interrupt-all tests
    
    ### What changes were proposed in this pull request?
    
    Make the two `SparkSessionE2ESuite` "interrupt all" tests robust against 
two flakiness sources:
    
    1. **Class-fetch race.** Run each long-running typed `map` query through a 
single call site and warm
       it up once (`sleep=0`) before any interrupt, so the closure/`TypeTag` 
artifact classes are already
       loaded on the executor. Otherwise an `interruptAll()` landing during the 
executor's first-time
       remote fetch of `$$typecreatorNN` surfaces as `RemoteClassLoaderError` 
instead of
       `OPERATION_CANCELED`.
    2. **Leaked interruptor / cascade.** Wrap the foreground-interrupt test 
body in
       `try/finally { finished = true }`. Previously, if an assertion failed, 
the background `interruptor`
       Future kept calling `interruptAll()` and canceled the operations of 
*subsequent* tests — turning
       one failure into a cascade of `OPERATION_CANCELED` failures across the 
whole suite.
    
    ### Why are the changes needed?
    
    `SparkSessionE2ESuite` intermittently fails in CI. Confirmed flaky (the 
same module group passes on
    other runs of the same commit).
    
    **Before (failing in apache/spark CI):** one `RemoteClassLoaderError` 
cascading into 7 failures —
    https://github.com/apache/spark/actions/runs/28037082466/job/82997066297
    
    **After (this change, validated on a fork):** full connect module `Total 
1614, Failed 0`, and
    `SparkSessionE2ESuite` re-run 8x with 0 failures —
    https://github.com/HyukjinKwon/spark/actions/runs/28066097371 and
    https://github.com/HyukjinKwon/spark/actions/runs/28074772169
    
    ### Does this PR introduce any user-facing change?
    
    No, test-only.
    
    ### How was this patch tested?
    
    Re-ran `SparkSessionE2ESuite` 8x plus the full connect module on CI (links 
above); all green.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes, drafted with Claude Code.
    
    Closes #56728 from HyukjinKwon/SPARK-57656.
    
    Authored-by: Hyukjin Kwon <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
    (cherry picked from commit dae1847a79d1c9929b40ff8945b951c2476d1d85)
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .../spark/sql/connect/SparkSessionE2ESuite.scala   | 46 +++++++++++++++++-----
 1 file changed, 36 insertions(+), 10 deletions(-)

diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/SparkSessionE2ESuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/SparkSessionE2ESuite.scala
index 184868a0df23..9c149a858018 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/SparkSessionE2ESuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/SparkSessionE2ESuite.scala
@@ -41,11 +41,20 @@ class SparkSessionE2ESuite extends ConnectFunSuite with 
RemoteSparkSession {
     val session = spark
     import session.implicits._
     implicit val ec: ExecutionContextExecutor = ExecutionContext.global
+    // Run the long-running query through a single call site, and warm it up 
once before any
+    // interrupt. Otherwise the very first execution has to ship/fetch the map 
closure and its
+    // TypeTag artifact classes to the executor on demand; if an interrupt 
lands during that
+    // first-time remote class fetch, it surfaces as a RemoteClassLoaderError 
instead of
+    // OPERATION_CANCELED, making this test flaky (see 
SparkSessionE2ESuite$$typecreatorNN).
+    def runMapQuery(sleepMs: Long): Unit = {
+      spark.range(10).map(n => { Thread.sleep(sleepMs); n }).collect()
+    }
+    runMapQuery(0)
     val q1 = Future {
-      spark.range(10).map(n => { Thread.sleep(30000); n }).collect()
+      runMapQuery(30000)
     }
     val q2 = Future {
-      spark.range(10).map(n => { Thread.sleep(30000); n }).collect()
+      runMapQuery(30000)
     }
     var q1Interrupted = false
     var q2Interrupted = false
@@ -88,6 +97,16 @@ class SparkSessionE2ESuite extends ConnectFunSuite with 
RemoteSparkSession {
     @volatile var finished = false
     val interrupted = mutable.ListBuffer[String]()
 
+    // Run the long-running query through a single call site, and warm it up 
once before the
+    // background interruptor starts. Otherwise the first execution has to 
ship/fetch the map
+    // closure and its TypeTag artifact classes to the executor on demand; if 
an interrupt lands
+    // during that first-time remote class fetch, it surfaces as a 
RemoteClassLoaderError instead
+    // of OPERATION_CANCELED, making this test flaky (see 
SparkSessionE2ESuite$$typecreatorNN).
+    def runMapQuery(sleepMs: Long): Unit = {
+      spark.range(10).map(n => { Thread.sleep(sleepMs); n }).collect()
+    }
+    runMapQuery(0)
+
     val interruptor = Future {
       eventually(timeout(20.seconds), interval(1.seconds)) {
         val ids = spark.interruptAll()
@@ -96,15 +115,22 @@ class SparkSessionE2ESuite extends ConnectFunSuite with 
RemoteSparkSession {
       }
       finished
     }
-    val e1 = intercept[SparkException] {
-      spark.range(10).map(n => { Thread.sleep(30.seconds.toMillis); n 
}).collect()
-    }
-    assert(e1.getMessage.contains("OPERATION_CANCELED"), s"Unexpected 
exception: $e1")
-    val e2 = intercept[SparkException] {
-      spark.range(10).map(n => { Thread.sleep(30.seconds.toMillis); n 
}).collect()
+    try {
+      val e1 = intercept[SparkException] {
+        runMapQuery(30.seconds.toMillis)
+      }
+      assert(e1.getMessage.contains("OPERATION_CANCELED"), s"Unexpected 
exception: $e1")
+      val e2 = intercept[SparkException] {
+        runMapQuery(30.seconds.toMillis)
+      }
+      assert(e2.getMessage.contains("OPERATION_CANCELED"), s"Unexpected 
exception: $e2")
+    } finally {
+      // Always release the background interruptor. If an assertion above 
fails, this prevents the
+      // interruptor Future from continuing to call interruptAll() and 
canceling operations of the
+      // subsequent tests in this suite (which previously caused cascading 
OPERATION_CANCELED
+      // failures across the whole suite).
+      finished = true
     }
-    assert(e2.getMessage.contains("OPERATION_CANCELED"), s"Unexpected 
exception: $e2")
-    finished = true
     assert(awaitResult(interruptor, 10.seconds))
     assert(interrupted.length == 2, s"Interrupted operations: $interrupted.")
   }


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

Reply via email to