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

1996fanrui pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 57de4676f3b [FLINK-40482][runtime] Abort checkpoint unless all tasks 
are RUNNING or FINISHED
57de4676f3b is described below

commit 57de4676f3b6c6e67675ef3c4cbbacee445a1267
Author: Rui Fan <[email protected]>
AuthorDate: Thu Aug 27 15:27:26 2026 +0200

    [FLINK-40482][runtime] Abort checkpoint unless all tasks are RUNNING or 
FINISHED
    
    DefaultCheckpointPlanCalculator used Execution.isFinished(), which returns
    state.isTerminal(), to decide whether a task is finished. This treats any
    terminal task -- FINISHED, FAILED or CANCELED -- as finished. During a
    failover, FAILED/CANCELED tasks were therefore excluded from tasksToWaitFor,
    producing a checkpoint plan that expects fewer acks than it should.
    
    - calculateCheckpointPlan: replace checkAllTasksInitiated() with
      checkAllTasksRunningOrFinished(), which requires every task to have an
      attached Execution in RUNNING or FINISHED state, the only states a
      checkpoint can be based on. Any other state aborts the checkpoint with a
      graceful CheckpointException instead of building a plan from a transient
      state.
    - collectTaskRunningStatus: classify explicitly; a non-terminal task is
      running, and a terminal task must be genuinely FINISHED.
    - Remove the misleading Execution.isFinished() (its only caller).
    
    The check is condition-based rather than mode-based, so it is correct for 
both
    streaming and batch. calculateCheckpointPlan and all task state transitions 
run
    on the JobManager main thread, so the pre-check and plan computation are 
atomic.
---
 .../DefaultCheckpointPlanCalculator.java           | 37 ++++++++++++++++++----
 .../flink/runtime/executiongraph/Execution.java    |  4 ---
 .../DefaultCheckpointPlanCalculatorTest.java       | 33 +++++++++++++++++++
 3 files changed, 63 insertions(+), 11 deletions(-)

diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculator.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculator.java
index a6874f17062..99bc5603cad 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculator.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculator.java
@@ -42,6 +42,7 @@ import java.util.concurrent.CompletionException;
 import java.util.stream.Collectors;
 
 import static org.apache.flink.util.Preconditions.checkNotNull;
+import static org.apache.flink.util.Preconditions.checkState;
 
 /**
  * Default implementation for {@link CheckpointPlanCalculator}. If all tasks 
are running, it
@@ -95,7 +96,7 @@ public class DefaultCheckpointPlanCalculator implements 
CheckpointPlanCalculator
                                     
CheckpointFailureReason.NOT_ALL_REQUIRED_TASKS_RUNNING);
                         }
 
-                        checkAllTasksInitiated();
+                        checkAllTasksRunningOrFinished();
 
                         CheckpointPlan result =
                                 context.hasFinishedTasks()
@@ -113,20 +114,32 @@ public class DefaultCheckpointPlanCalculator implements 
CheckpointPlanCalculator
     }
 
     /**
-     * Checks if all tasks are attached with the current Execution already. 
This method should be
-     * called from JobMaster main thread executor.
+     * Checks that every task is attached with the current Execution and that 
this Execution is
+     * either RUNNING or FINISHED, the only states a checkpoint can be based 
on. This method should
+     * be called from JobMaster main thread executor.
      *
-     * @throws CheckpointException if some tasks do not have attached 
Execution.
+     * @throws CheckpointException if some task has no attached Execution or 
is neither RUNNING nor
+     *     FINISHED.
      */
-    private void checkAllTasksInitiated() throws CheckpointException {
+    private void checkAllTasksRunningOrFinished() throws CheckpointException {
         for (ExecutionVertex task : allTasks) {
-            if (task.getCurrentExecutionAttempt() == null) {
+            Execution attempt = task.getCurrentExecutionAttempt();
+            if (attempt == null) {
                 throw new CheckpointException(
                         String.format(
                                 "task %s of job %s is not being executed at 
the moment. Aborting checkpoint.",
                                 task.getTaskNameWithSubtaskIndex(), jobId),
                         
CheckpointFailureReason.NOT_ALL_REQUIRED_TASKS_RUNNING);
             }
+
+            ExecutionState state = attempt.getState();
+            if (state != ExecutionState.RUNNING && state != 
ExecutionState.FINISHED) {
+                throw new CheckpointException(
+                        String.format(
+                                "task %s of job %s is in %s state instead of 
RUNNING or FINISHED. Aborting checkpoint.",
+                                task.getTaskNameWithSubtaskIndex(), jobId, 
state),
+                        
CheckpointFailureReason.NOT_ALL_REQUIRED_TASKS_RUNNING);
+            }
         }
     }
 
@@ -318,8 +331,18 @@ public class DefaultCheckpointPlanCalculator implements 
CheckpointPlanCalculator
             BitSet runningTasks = new BitSet(vertex.getTaskVertices().length);
 
             for (int i = 0; i < vertex.getTaskVertices().length; ++i) {
-                if 
(!vertex.getTaskVertices()[i].getCurrentExecutionAttempt().isFinished()) {
+                Execution attempt = 
vertex.getTaskVertices()[i].getCurrentExecutionAttempt();
+                ExecutionState state = attempt.getState();
+                if (state == ExecutionState.RUNNING) {
                     runningTasks.set(i);
+                } else {
+                    // A non-RUNNING task must be FINISHED; every other state 
is already rejected
+                    // by checkAllTasksRunningOrFinished().
+                    checkState(
+                            state == ExecutionState.FINISHED,
+                            "Task %s is %s, not FINISHED",
+                            attempt.getAttemptId(),
+                            state);
                 }
             }
 
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/executiongraph/Execution.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/executiongraph/Execution.java
index 430cd9ee76d..46d5a8d60b0 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/executiongraph/Execution.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/executiongraph/Execution.java
@@ -376,10 +376,6 @@ public class Execution
         return this.stateEndTimestamps[state.ordinal()];
     }
 
-    public boolean isFinished() {
-        return state.isTerminal();
-    }
-
     @Nullable
     public JobManagerTaskRestore getTaskRestore() {
         return taskRestore;
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculatorTest.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculatorTest.java
index faa42c20b32..cbe68312ecc 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculatorTest.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/checkpoint/DefaultCheckpointPlanCalculatorTest.java
@@ -37,6 +37,8 @@ import 
org.apache.flink.testutils.executor.TestExecutorExtension;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -166,6 +168,37 @@ class DefaultCheckpointPlanCalculatorTest {
         // then: The plan failed because one task didn't have RUNNING state.
     }
 
+    @ParameterizedTest
+    @EnumSource(
+            value = ExecutionState.class,
+            names = {"FAILED", "CANCELED", "CANCELING"})
+    void 
testPlanAbortedWhenTaskIsFailingOverWhileOthersFinished(ExecutionState 
failoverState)
+            throws Exception {
+        ExecutionGraph graph =
+                createExecutionGraph(
+                        Arrays.asList(
+                                new VertexDeclaration(1, of(0)), new 
VertexDeclaration(1, of())),
+                        Collections.emptyList());
+
+        chooseJobVertex(graph, 1)
+                .getTaskVertices()[0]
+                .getCurrentExecutionAttempt()
+                .transitionState(failoverState);
+
+        DefaultCheckpointPlanCalculator planCalculator = 
createCheckpointPlanCalculator(graph);
+
+        assertThatFuture(planCalculator.calculateCheckpointPlan())
+                .eventuallyFailsWith(ExecutionException.class)
+                .havingCause()
+                .isInstanceOfSatisfying(
+                        CheckpointException.class,
+                        e ->
+                                assertThat(e.getCheckpointFailureReason())
+                                        .isEqualTo(
+                                                CheckpointFailureReason
+                                                        
.NOT_ALL_REQUIRED_TASKS_RUNNING));
+    }
+
     private void runWithNotRunningTask(
             boolean isRunningVertexSource, boolean isNotRunningVertexSource) 
throws Exception {
         for (ExecutionState notRunningState : 
complementOf(EnumSet.of(ExecutionState.RUNNING))) {

Reply via email to