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

sarutak pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 9f7148b08412 [SPARK-57963][SS] Restore interrupt status unexpectedly 
cleared so that `StreamExecution.stop` can stop micro-batch streaming query
9f7148b08412 is described below

commit 9f7148b084126360b1f4d9a53f9c558bedc14078
Author: Kousuke Saruta <[email protected]>
AuthorDate: Tue Jul 7 18:25:11 2026 +0900

    [SPARK-57963][SS] Restore interrupt status unexpectedly cleared so that 
`StreamExecution.stop` can stop micro-batch streaming query
    
    ### What changes were proposed in this pull request?
    This PR fixes an issue that interrupt statuses set by 
`queryExecutionThread.interrupt()` in 
[QueryExecution.interruptAndAwaitExecutionThreadTermination()](https://github.com/apache/spark/blob/4995e6f68c424de361371ecfa6fa0cd87c83617f/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/StreamExecution.scala#L503)
  are unexpectedly cleared.
    This can be the root cause of the flakiness of the test `python 
foreachBatch process: process terminates after query is stopped` in 
`SparkConnectSessionHolderSuite` which sometimes fails recently.
    https://github.com/apache/spark/actions/runs/28604689132/job/84825842409
    ```
    [info] - python foreachBatch process: process terminates after query is 
stopped *** FAILED *** (1 minute, 46 seconds)
    [info]   java.util.concurrent.TimeoutException: Stream Execution thread for 
stream foreachBatch_termination_test_q1_3866433486358 [id = 
e5635fe9-1633-4de9-a0d7-1f47a267e44a, runId = 
5c01eacc-9822-4b9a-8390-e47267a2a6d0] failed to stop within 30000 milliseconds 
(specified by spark.sql.streaming.stopTimeout). See the cause on what was being 
executed in the streaming query thread.
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.StreamExecution.interruptAndAwaitExecutionThreadTermination(StreamExecution.scala:510)
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.MicroBatchExecution.stop(MicroBatchExecution.scala:475)
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.StreamingQueryWrapper.stop(StreamingQueryWrapper.scala:61)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.runPythonForeachBatchTerminationTestBody(SparkConnectSessionHolderSuite.scala:387)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.$anonfun$new$16(SparkConnectSessionHolderSuite.scala:450)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.$anonfun$awaitTestBodyInNewThread$1(SparkConnectSessionHolderSuite.scala:281)
    [info]   at java.base/java.lang.Thread.run(Thread.java:840)
    [info]   Cause: org.apache.spark.SparkException: The stream thread was last 
executing:
    ```
    
    When a streaming query runs with micro-batch mode, 
`markMicroBatchExecutionStart()` is invoked 
[here](https://github.com/apache/spark/blob/4995e6f68c424de361371ecfa6fa0cd87c83617f/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala#L1236)
 every batch, and it eventually leads to 
`FileContextBasedCheckpointFileManager.renameTempFile()`. In this method, 
[fc.rename()](https://github.com/apache/spark/blob/4995e6f68c424de361371ecfa6fa0cd87c8361
 [...]
    Surprisingly, `fc.rename()` seems to clear interrupt statuses.
    You can confirm that by modifying `CheckpointFileManager.scala` like as 
follows:
    
    ```
     -388,7 +388,18  class FileContextBasedCheckpointFileManager(path: Path, 
hadoopConf: Configuratio
    
       override def renameTempFile(srcPath: Path, dstPath: Path, 
overwriteIfPossible: Boolean): Unit = {
         import Options.Rename._
    +
    +    try {
    +      // Receive interrupt here.
    +      Thread.sleep(1000)
    +    } catch {
    +      case e: InterruptedException =>
    +        // Restore the interrupt status cleared by Thread.sleep.
    +        Thread.currentThread.interrupt
    +    }
    +    println("Interrupt status before fc.rename is called: " + 
Thread.currentThread.isInterrupted)
         fc.rename(srcPath, dstPath, if (overwriteIfPossible) OVERWRITE else 
NONE)
    +    println("Interrupt status after fc.rename is called: " + 
Thread.currentThread.isInterrupted)
    ```
    
    And then, run the flaky test.
    ```
    
    $ build/sbt 'testOnly 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite -- -z 
"python foreachBatch proce
    ss: process terminates after query is stopped"'
    
    ...
    Interrupt status before fc.rename is called: true
    Interrupt status after fc.rename is called: false
    ...
    ```
    
    `QueryExecution.interruptAndAwaitExecutionThreadTermination()` is invoked 
from 
[MicroBatchExecution.stop()](https://github.com/apache/spark/blob/4995e6f68c424de361371ecfa6fa0cd87c83617f/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala#L475)
 so in the test above, if an interrupt status set through 
[query1.stop()](https://github.com/apache/spark/blob/4995e6f68c424de361371ecfa6fa0cd87c83617f/sql/connect/server/src/test/scala/org/apache/sp
 [...]
    
    You can reproduce this issue by modifying `MicroBatchExecution.scala` as 
well like as follows.
    ```
       override def stop(): Unit = {
         // Set the state to TERMINATED so that the batching thread knows that 
it was interrupted
         // intentionally
    +    Thread.sleep(500)
         state.set(TERMINATED)
         if (queryExecutionThread.isAlive) {
           sparkSession.sparkContext.cancelJobGroup(runId.toString,
    ```
    The `Thread.sleep` inserted is to ensure `state.set(TERMINATED)` is invoked 
after micro-batches start.
    And then, run the flaky test.
    ```
    $ build/sbt 'testOnly 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite -- -z 
"python foreachBatch process: process terminates after query is stopped"'
    
    ...
    
    [info] - python foreachBatch process: process terminates after query is 
stopped *** FAILED *** (2 minutes, 12 seconds)
    [info]   java.util.concurrent.TimeoutException: Stream Execution thread for 
stream foreachBatch_termination_test_q1_516328
    7691187916 [id = 3df450c6-5410-49fa-bc3b-0b938830fcac, runId = 
757cb7bf-5619-4afd-b82f-b8a3e63d2521] failed to stop within
     30000 milliseconds (specified by spark.sql.streaming.stopTimeout). See the 
cause on what was being executed in the stream
    ing query thread.
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.StreamExecution.interruptAndAwaitExecutionThreadTermination(S
    treamExecution.scala:510)
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.MicroBatchExecution.stop(MicroBatchExecution.scala:476)
    [info]   at 
org.apache.spark.sql.execution.streaming.runtime.StreamingQueryWrapper.stop(StreamingQueryWrapper.scala:61)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.runPythonForeachBatchTerminationTestBody(S
    parkConnectSessionHolderSuite.scala:387)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.$anonfun$new$16(SparkConnectSessionHolderS
    uite.scala:450)
    [info]   at 
org.apache.spark.sql.connect.service.SparkConnectSessionHolderSuite.$anonfun$awaitTestBodyInNewThread$1(SparkC
    onnectSessionHolderSuite.scala:281)
    [info]   at java.base/java.lang.Thread.run(Thread.java:840)
    [info]   Cause: org.apache.spark.SparkException: The stream thread was last 
executing:
    
    ...
    
    [info] Run completed in 2 minutes, 15 seconds.
    [info] Total number of tests run: 1
    [info] Suites: completed 1, aborted 0
    [info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0
    [info] *** 1 TEST FAILED ***
    ```
    
    The reason why `FileContext.rename()` clears interrupt statuses is not 
clear for now but this PR proposes to restore the interrupt status as a 
workaround.
    
    ### Why are the changes needed?
    To allow `StreamExecution.stop` can stop micro-batch streaming query by 
interruption expectedly.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. `StreamExecution.stop` will work as expected.
    
    ### How was this patch tested?
    Confirmed `python foreachBatch process: process terminates after query is 
stopped` in `SparkConnectSessionHolderSuite` passes even if `Thread.sleep`s are 
inserted like above.
    
    ```
    [info] - python foreachBatch process: process terminates after query is 
stopped (18 seconds, 765 milliseconds)
    [info] Passed: Total 0, Failed 0, Errors 0, Passed 0
    [info] No tests to run for sql / Test / testOnly
    [info] Run completed in 22 seconds, 214 milliseconds.
    [info] Total number of tests run: 1
    [info] Suites: completed 1, aborted 0
    [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
    [info] All tests passed.
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    No.
    
    Closes #57041 from sarutak/restore-interrupt-status-ss.
    
    Authored-by: Kousuke Saruta <[email protected]>
    Signed-off-by: Kousuke Saruta <[email protected]>
    (cherry picked from commit 93210604bdf8280c0e4b0b86dc47557eca6cb4a2)
    Signed-off-by: Kousuke Saruta <[email protected]>
---
 .../spark/sql/execution/streaming/runtime/MicroBatchExecution.scala  | 5 +++++
 1 file changed, 5 insertions(+)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala
index 17ab756421c8..6faa3884e080 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/runtime/MicroBatchExecution.scala
@@ -1044,6 +1044,11 @@ class MicroBatchExecution(
     setupStateStoreCommitTracking(execCtx)
 
     markMicroBatchExecutionStart(execCtx)
+    if (!isActive) {
+      // Workaround for the case the interrupt status is unexpectedly cleared. 
See SPARK-57963 for
+      // more details.
+      Thread.currentThread.interrupt
+    }
 
     if (trigger.isInstanceOf[RealTimeTrigger]) {
       RealTimeModeAllowlist.checkAllowedPhysicalOperator(


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

Reply via email to