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

Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 0c5adeaff12 [Spark] Resolve the structured streaming pipeline state 
from the execution, cancel() returns the observed state (#40787)
0c5adeaff12 is described below

commit 0c5adeaff12d46edffa88adbb43fd4ea8fad0f26
Author: Tobias Kaymak <[email protected]>
AuthorDate: Wed Oct 28 23:11:17 2026 +0100

    [Spark] Resolve the structured streaming pipeline state from the execution, 
cancel() returns the observed state (#40787)
    
    * [Spark] Resolve the structured streaming pipeline state from the 
execution, cancel() returns the observed state
---
 ...eam_PostCommit_Java_ValidatesRunner_Spark4.json |  2 +-
 ...a_ValidatesRunner_SparkStructuredStreaming.json |  1 +
 .../streaming/StreamingPipelineLifecycleTest.java  |  8 ++--
 .../SparkStructuredStreamingPipelineResult.java    | 36 ++++++++++-----
 ...SparkStructuredStreamingPipelineResultTest.java | 30 +++++++++++--
 .../StructuredStreamingPipelineStateTest.java      | 52 ++++++----------------
 6 files changed, 70 insertions(+), 59 deletions(-)

diff --git 
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json 
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json
index f1ba03a243e..455144f02a3 100644
--- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json
+++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json
@@ -1,4 +1,4 @@
 {
   "comment": "Modify this file in a trivial way to cause this test suite to 
run",
-  "modification": 5
+  "modification": 6
 }
diff --git 
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json
 
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json
index 65728e1581d..d6e4fe75353 100644
--- 
a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json
+++ 
b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json
@@ -10,6 +10,7 @@
   "https://github.com/apache/beam/pull/35159": "moving WindowedValue and 
making an interface",
   "https://github.com/apache/beam/pull/39793": "noting that PR #39793 should 
run this test",
   "https://github.com/apache/beam/pull/40103": "noting that PR #40103 should 
run this test",
+  "https://github.com/apache/beam/issues/40120": "cancel() returns the 
observed state",
   "https://github.com/apache/beam/issues/40427": "Spark ValidatesRunner tests 
run serially",
   "https://github.com/apache/beam/pull/40281": "noting that PR #40281 should 
run this test",
   "https://github.com/apache/beam/issues/36841": "Spark 4 streaming Impulse 
and PAssert"
diff --git 
a/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/streaming/StreamingPipelineLifecycleTest.java
 
b/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/streaming/StreamingPipelineLifecycleTest.java
index fd897db515a..63a9f5ccbda 100644
--- 
a/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/streaming/StreamingPipelineLifecycleTest.java
+++ 
b/runners/spark/4/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/streaming/StreamingPipelineLifecycleTest.java
@@ -117,8 +117,8 @@ public class StreamingPipelineLifecycleTest implements 
Serializable {
 
     awaitActiveQueries(count -> count > 0, "no streaming query started");
 
-    PipelineResult.State cancelledState = result.cancel();
-    assertEquals(PipelineResult.State.CANCELLED, cancelledState);
+    result.cancel();
+    assertEquals(PipelineResult.State.CANCELLED, 
StreamingTestUtils.waitUntilFinish(result));
     assertEquals(PipelineResult.State.CANCELLED, result.getState());
 
     awaitActiveQueries(count -> count == 0, "the streaming query did not stop 
after cancel");
@@ -148,8 +148,8 @@ public class StreamingPipelineLifecycleTest implements 
Serializable {
       assertEquals(PipelineResult.State.RUNNING, stateAfterTimeout);
       assertEquals(PipelineResult.State.RUNNING, result.getState());
 
-      PipelineResult.State cancelledState = result.cancel();
-      assertEquals(PipelineResult.State.CANCELLED, cancelledState);
+      result.cancel();
+      assertEquals(PipelineResult.State.CANCELLED, 
StreamingTestUtils.waitUntilFinish(result));
       assertEquals(PipelineResult.State.CANCELLED, result.getState());
     } finally {
       if (result.getState() == PipelineResult.State.RUNNING) {
diff --git 
a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java
 
b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java
index c483bbcf3cf..c666e0b9a2b 100644
--- 
a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java
+++ 
b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResult.java
@@ -21,6 +21,7 @@ import static 
org.apache.beam.runners.core.metrics.MetricsContainerStepMap.asAtt
 import static 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull;
 
 import java.io.IOException;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -33,6 +34,7 @@ import org.apache.beam.sdk.Pipeline;
 import org.apache.beam.sdk.PipelineResult;
 import org.apache.beam.sdk.metrics.MetricResults;
 import org.apache.beam.sdk.util.UserCodeException;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Futures;
 import org.apache.spark.SparkException;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.joda.time.Duration;
@@ -92,6 +94,18 @@ public class SparkStructuredStreamingPipelineResult 
implements PipelineResult {
 
   @Override
   public PipelineResult.State getState() {
+    if (!state.isTerminal() && pipelineExecution.isDone()) {
+      if (cancelRequested.get()) {
+        state = State.CANCELLED;
+      } else {
+        try {
+          Futures.getDone(pipelineExecution);
+          state = State.DONE;
+        } catch (ExecutionException | CancellationException e) {
+          state = State.FAILED;
+        }
+      }
+    }
     return state;
   }
 
@@ -108,17 +122,13 @@ public class SparkStructuredStreamingPipelineResult 
implements PipelineResult {
   public State waitUntilFinish(final Duration duration) {
     try {
       pipelineExecution.get(duration.getMillis(), TimeUnit.MILLISECONDS);
-      state = cancelRequested.get() ? State.CANCELLED : State.DONE;
     } catch (final TimeoutException e) {
       // ignore.
     } catch (final ExecutionException e) {
-      if (cancelRequested.get()) {
-        LOG.warn("Pipeline execution failed after cancel", e.getCause());
-        state = State.CANCELLED;
-        return state;
+      if (getState() != State.CANCELLED) {
+        throw unwrapCause(firstNonNull(e.getCause(), e));
       }
-      state = State.FAILED;
-      throw unwrapCause(firstNonNull(e.getCause(), e));
+      LOG.warn("Pipeline execution failed after cancel", e.getCause());
     } catch (final InterruptedException e) {
       Thread.currentThread().interrupt();
       state = State.FAILED;
@@ -128,7 +138,7 @@ public class SparkStructuredStreamingPipelineResult 
implements PipelineResult {
       throw unwrapCause(e);
     }
 
-    return state;
+    return getState();
   }
 
   @Override
@@ -136,17 +146,19 @@ public class SparkStructuredStreamingPipelineResult 
implements PipelineResult {
     return asAttemptedOnlyMetricResults(metrics.value());
   }
 
-  /** Requests cancellation of the pipeline and returns immediately. */
+  /**
+   * Requests cancellation and returns the current state. The state becomes 
CANCELLED once the
+   * execution ends.
+   */
   @Override
   public PipelineResult.State cancel() throws IOException {
-    if (!state.isTerminal() && cancelRequested.compareAndSet(false, true)) {
+    if (!pipelineExecution.isDone() && cancelRequested.compareAndSet(false, 
true)) {
       EvaluationContext ctx = evaluationContext.get();
       if (ctx != null) {
         ctx.stop();
       }
       cancelSparkJobs.run();
-      state = State.CANCELLED;
     }
-    return state;
+    return getState();
   }
 }
diff --git 
a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResultTest.java
 
b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResultTest.java
index 4cd4858f000..cfcf8a36852 100644
--- 
a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResultTest.java
+++ 
b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/SparkStructuredStreamingPipelineResultTest.java
@@ -20,13 +20,16 @@ package org.apache.beam.runners.spark.structuredstreaming;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import 
org.apache.beam.runners.spark.structuredstreaming.metrics.MetricsAccumulator;
+import org.apache.beam.sdk.Pipeline;
 import org.apache.beam.sdk.PipelineResult.State;
+import org.apache.beam.sdk.util.UserCodeException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -49,8 +52,8 @@ public class SparkStructuredStreamingPipelineResultTest {
   @Test
   public void testCancelRunsJobCancelHookOnce() throws Exception {
     SparkStructuredStreamingPipelineResult result = result(new 
CompletableFuture<>());
-    assertThat(result.cancel(), is(State.CANCELLED));
-    assertThat(result.cancel(), is(State.CANCELLED));
+    assertThat(result.cancel(), is(State.RUNNING));
+    assertThat(result.cancel(), is(State.RUNNING));
     assertThat(cancelSparkJobsCalls.get(), is(1));
   }
 
@@ -58,9 +61,30 @@ public class SparkStructuredStreamingPipelineResultTest {
   public void testCancelIsAsynchronous() throws Exception {
     CompletableFuture<Void> execution = new CompletableFuture<>();
     SparkStructuredStreamingPipelineResult result = result(execution);
-    assertThat(result.cancel(), is(State.CANCELLED));
+    assertThat(result.cancel(), is(State.RUNNING));
     assertFalse(execution.isDone());
     execution.completeExceptionally(new IllegalStateException("job 
cancelled"));
+    assertThat(result.getState(), is(State.CANCELLED));
     assertThat(result.waitUntilFinish(), is(State.CANCELLED));
   }
+
+  @Test
+  public void testCancelAfterNormalCompletionReturnsDone() throws Exception {
+    CompletableFuture<Void> execution = 
CompletableFuture.completedFuture(null);
+    SparkStructuredStreamingPipelineResult result = result(execution);
+    assertThat(result.cancel(), is(State.DONE));
+    assertThat(cancelSparkJobsCalls.get(), is(0));
+  }
+
+  @Test
+  public void testCancelAfterExceptionalCompletionReturnsFailed() throws 
Exception {
+    CompletableFuture<Void> execution = new CompletableFuture<>();
+    execution.completeExceptionally(
+        UserCodeException.wrap(new IllegalStateException("pipeline failed")));
+    SparkStructuredStreamingPipelineResult result = result(execution);
+    assertThat(result.cancel(), is(State.FAILED));
+    assertThat(cancelSparkJobsCalls.get(), is(0));
+    assertThat(result.getState(), is(State.FAILED));
+    assertThrows(Pipeline.PipelineExecutionException.class, 
result::waitUntilFinish);
+  }
 }
diff --git 
a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/StructuredStreamingPipelineStateTest.java
 
b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/StructuredStreamingPipelineStateTest.java
index b883af5eb00..a7ee55b67b4 100644
--- 
a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/StructuredStreamingPipelineStateTest.java
+++ 
b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/StructuredStreamingPipelineStateTest.java
@@ -40,9 +40,7 @@ import org.apache.beam.sdk.transforms.SimpleFunction;
 import org.apache.spark.TaskContext;
 import org.apache.spark.sql.SparkSession;
 import org.joda.time.Duration;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -60,8 +58,6 @@ public class StructuredStreamingPipelineStateTest implements 
Serializable {
   private final transient SparkStructuredStreamingPipelineOptions options =
       
PipelineOptionsFactory.create().as(SparkStructuredStreamingPipelineOptions.class);
 
-  @Rule public transient TestName testName = new TestName();
-
   private static final String FAILED_THE_BATCH_INTENTIONALLY = "Failed the 
batch intentionally";
 
   private static final long DEADLINE_SECONDS = 60;
@@ -108,16 +104,6 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
     return options;
   }
 
-  private Pipeline getPipeline(final SparkStructuredStreamingPipelineOptions 
options) {
-
-    final Pipeline pipeline = Pipeline.create(options);
-    final String name = testName.getMethodName();
-
-    pipeline.apply(Create.of("one", 
"two")).setCoder(StringUtf8Coder.of()).apply(printParDo(name));
-
-    return pipeline;
-  }
-
   private void testFailedPipeline(final 
SparkStructuredStreamingPipelineOptions options)
       throws Exception {
 
@@ -152,13 +138,9 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
     fail("An injected failure did not affect the pipeline as expected.");
   }
 
-  private void testTimeoutPipeline(final 
SparkStructuredStreamingPipelineOptions options)
-      throws Exception {
+  private void testTimeoutPipeline() throws Exception {
 
-    final Pipeline pipeline = getPipeline(options);
-
-    final SparkStructuredStreamingPipelineResult result =
-        (SparkStructuredStreamingPipelineResult) pipeline.run();
+    final SparkStructuredStreamingPipelineResult result = 
runBlockingPipeline();
 
     result.waitUntilFinish(Duration.millis(1));
 
@@ -168,27 +150,19 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
     assertThat(result.waitUntilFinish(), is(PipelineResult.State.CANCELLED));
   }
 
-  private void testCanceledPipeline(final 
SparkStructuredStreamingPipelineOptions options)
-      throws Exception {
+  private void testCanceledPipeline() throws Exception {
 
-    final Pipeline pipeline = getPipeline(options);
-
-    final SparkStructuredStreamingPipelineResult result =
-        (SparkStructuredStreamingPipelineResult) pipeline.run();
+    final SparkStructuredStreamingPipelineResult result = 
runBlockingPipeline();
 
     result.cancel();
 
-    assertThat(result.getState(), is(PipelineResult.State.CANCELLED));
     assertThat(result.waitUntilFinish(), is(PipelineResult.State.CANCELLED));
+    assertThat(result.getState(), is(PipelineResult.State.CANCELLED));
   }
 
-  private void testRunningPipeline(final 
SparkStructuredStreamingPipelineOptions options)
-      throws Exception {
+  private void testRunningPipeline() throws Exception {
 
-    final Pipeline pipeline = getPipeline(options);
-
-    final SparkStructuredStreamingPipelineResult result =
-        (SparkStructuredStreamingPipelineResult) pipeline.run();
+    final SparkStructuredStreamingPipelineResult result = 
runBlockingPipeline();
 
     assertThat(result.getState(), is(PipelineResult.State.RUNNING));
 
@@ -198,12 +172,12 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
 
   @Test
   public void testBatchPipelineRunningState() throws Exception {
-    testRunningPipeline(getBatchOptions());
+    testRunningPipeline();
   }
 
   @Test
   public void testBatchPipelineCanceledState() throws Exception {
-    testCanceledPipeline(getBatchOptions());
+    testCanceledPipeline();
   }
 
   @Test
@@ -213,13 +187,13 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
 
   @Test
   public void testBatchPipelineTimeoutState() throws Exception {
-    testTimeoutPipeline(getBatchOptions());
+    testTimeoutPipeline();
   }
 
   @Test
   public void testBatchCancelStopsRunningJob() throws Exception {
     SparkStructuredStreamingPipelineResult result = runBlockingPipeline();
-    assertThat(result.cancel(), is(PipelineResult.State.CANCELLED));
+    result.cancel();
     assertThat(result.waitUntilFinish(), is(PipelineResult.State.CANCELLED));
     assertTrue("owned session not stopped", 
SparkSession.getDefaultSession().isEmpty());
   }
@@ -229,7 +203,7 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
     SparkSession session = 
SparkSessionFactory.sessionBuilder("local[1]").getOrCreate();
     try {
       SparkStructuredStreamingPipelineResult result = runBlockingPipeline();
-      assertThat(result.cancel(), is(PipelineResult.State.CANCELLED));
+      result.cancel();
       assertThat(result.waitUntilFinish(), is(PipelineResult.State.CANCELLED));
       assertFalse("shared session stopped", 
session.sparkContext().isStopped());
     } finally {
@@ -241,7 +215,7 @@ public class StructuredStreamingPipelineStateTest 
implements Serializable {
   @Test
   public void testCancelFollowedImmediatelyBySecondPipeline() throws Exception 
{
     SparkStructuredStreamingPipelineResult first = runBlockingPipeline();
-    assertThat(first.cancel(), is(PipelineResult.State.CANCELLED));
+    first.cancel();
     Pipeline secondPipeline = Pipeline.create(getBatchOptions());
     secondPipeline.apply(Create.of("a", "b")).apply(printParDo("second"));
     SparkStructuredStreamingPipelineResult second =

Reply via email to