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

XComp pushed a commit to branch release-2.3
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/release-2.3 by this push:
     new bda6d208c57 [FLINK-40003][runtime] Fix IOMetrics not visible to state 
transition listeners
bda6d208c57 is described below

commit bda6d208c571641f75391f13b3089859174b426c
Author: Chris Johnson <[email protected]>
AuthorDate: Fri Jun 26 15:47:57 2026 -0500

    [FLINK-40003][runtime] Fix IOMetrics not visible to state transition 
listeners
    
    Execution.markFinished() and processFail() updated accumulators and
    IOMetrics after transitionState(), so ExecutionStateUpdateListeners
    notified inline during the transition saw null from getIOMetrics().
    
    Add transitionToTerminalState() with a preCompletionAction callback that
    runs after the state is set but before terminalStateFuture is completed
    and listeners are notified. The metrics/accumulators update is passed as
    this callback so both terminalStateFuture consumers and listeners observe
    it. Applied to markFinished(), processFail(), completeCancelling() and
    recoverExecution().
    
    Co-Authored-By: Claude <[email protected]>
    (cherry picked from commit c0f774fdcefab05cf25c986effaad78fa5ecff2a)
---
 .../flink/runtime/executiongraph/Execution.java    |  68 +++++++++---
 .../DefaultExecutionGraphDeploymentTest.java       | 123 ++++++++++++++++++++-
 2 files changed, 168 insertions(+), 23 deletions(-)

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 a1301f604dc..a704e3290fd 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
@@ -95,6 +95,7 @@ import static 
org.apache.flink.runtime.execution.ExecutionState.FINISHED;
 import static org.apache.flink.runtime.execution.ExecutionState.INITIALIZING;
 import static org.apache.flink.runtime.execution.ExecutionState.RUNNING;
 import static org.apache.flink.runtime.execution.ExecutionState.SCHEDULED;
+import static org.apache.flink.util.Preconditions.checkArgument;
 import static org.apache.flink.util.Preconditions.checkNotNull;
 import static org.apache.flink.util.Preconditions.checkState;
 
@@ -486,9 +487,12 @@ public class Execution
         taskManagerLocationFuture.complete(location);
 
         try {
-            transitionState(this.state, FINISHED);
+            transitionToTerminalState(
+                    this.state,
+                    FINISHED,
+                    null,
+                    () -> updateAccumulatorsAndMetrics(userAccumulators, 
metrics));
             finishPartitionsAndUpdateConsumers();
-            updateAccumulatorsAndMetrics(userAccumulators, metrics);
             releaseAssignedResource(null);
             vertex.getExecutionGraphAccessor().deregisterExecution(this);
         } finally {
@@ -1166,10 +1170,13 @@ public class Execution
 
             if (current == INITIALIZING || current == RUNNING || current == 
DEPLOYING) {
 
-                if (transitionState(current, FINISHED)) {
+                if (transitionToTerminalState(
+                        current,
+                        FINISHED,
+                        null,
+                        () -> updateAccumulatorsAndMetrics(userAccumulators, 
metrics))) {
                     try {
                         finishPartitionsAndUpdateConsumers();
-                        updateAccumulatorsAndMetrics(userAccumulators, 
metrics);
                         releaseAssignedResource(null);
                         
vertex.getExecutionGraphAccessor().deregisterExecution(this);
                     } finally {
@@ -1251,9 +1258,11 @@ public class Execution
                     || current == INITIALIZING
                     || current == DEPLOYING) {
 
-                updateAccumulatorsAndMetrics(userAccumulators, metrics);
-
-                if (transitionState(current, CANCELED)) {
+                if (transitionToTerminalState(
+                        current,
+                        CANCELED,
+                        null,
+                        () -> updateAccumulatorsAndMetrics(userAccumulators, 
metrics))) {
                     finishCancellation(releasePartitions);
                     return;
                 }
@@ -1363,14 +1372,18 @@ public class Execution
             return;
         }
 
-        checkState(transitionState(current, FAILED, t));
-
-        // success (in a manner of speaking)
-        this.failureCause =
-                Optional.of(
-                        ErrorInfo.createErrorInfoWithNullableCause(t, 
getStateTimestamp(FAILED)));
-
-        updateAccumulatorsAndMetrics(userAccumulators, metrics);
+        checkState(
+                transitionToTerminalState(
+                        current,
+                        FAILED,
+                        t,
+                        () -> {
+                            updateAccumulatorsAndMetrics(userAccumulators, 
metrics);
+                            this.failureCause =
+                                    Optional.of(
+                                            
ErrorInfo.createErrorInfoWithNullableCause(
+                                                    t, 
getStateTimestamp(FAILED)));
+                        }));
 
         releaseAssignedResource(t);
         vertex.getExecutionGraphAccessor().deregisterExecution(this);
@@ -1636,11 +1649,29 @@ public class Execution
     }
 
     private boolean transitionState(ExecutionState currentState, 
ExecutionState targetState) {
-        return transitionState(currentState, targetState, null);
+        return transitionState(currentState, targetState, null, null);
+    }
+
+    /**
+     * Transitions to a terminal state, running {@code preCompletionAction} 
after the state is set
+     * but before {@link #terminalStateFuture} is completed and listeners are 
notified. Use this for
+     * storing metrics/accumulators so that both {@code terminalStateFuture} 
consumers and {@link
+     * ExecutionStateUpdateListener}s see them.
+     */
+    private boolean transitionToTerminalState(
+            ExecutionState currentState,
+            ExecutionState targetState,
+            @Nullable Throwable error,
+            Runnable preCompletionAction) {
+        checkArgument(targetState.isTerminal(), "targetState must be 
terminal");
+        return transitionState(currentState, targetState, error, 
preCompletionAction);
     }
 
     private boolean transitionState(
-            ExecutionState currentState, ExecutionState targetState, Throwable 
error) {
+            ExecutionState currentState,
+            ExecutionState targetState,
+            @Nullable Throwable error,
+            @Nullable Runnable preCompletionAction) {
         // sanity check
         if (currentState.isTerminal()) {
             throw new IllegalStateException(
@@ -1676,6 +1707,9 @@ public class Execution
             if (targetState == INITIALIZING || targetState == RUNNING) {
                 initializingOrRunningFuture.complete(null);
             } else if (targetState.isTerminal()) {
+                if (preCompletionAction != null) {
+                    preCompletionAction.run();
+                }
                 // complete the terminal state future
                 terminalStateFuture.complete(targetState);
             }
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/executiongraph/DefaultExecutionGraphDeploymentTest.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/executiongraph/DefaultExecutionGraphDeploymentTest.java
index 270f7f71c88..256a22cec5d 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/executiongraph/DefaultExecutionGraphDeploymentTest.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/executiongraph/DefaultExecutionGraphDeploymentTest.java
@@ -84,6 +84,7 @@ import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static 
org.apache.flink.runtime.util.JobVertexConnectionUtils.connectNewDataSetAsInput;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -403,6 +404,106 @@ class DefaultExecutionGraphDeploymentTest {
         assertThat(execution2.getUserAccumulators()).isEqualTo(accumulators);
     }
 
+    @Test
+    void testMetricsVisibleToListenersOnMarkFinished() throws Exception {
+        testMetricsVisibleToListenersDuringTerminalTransition(
+                (execution, accumulators, metrics) -> {
+                    // markFinished() only transitions from a running state.
+                    execution.transitionState(ExecutionState.RUNNING);
+                    execution.markFinished(accumulators, metrics);
+                });
+    }
+
+    @Test
+    void testMetricsVisibleToListenersOnMarkFailed() throws Exception {
+        testMetricsVisibleToListenersDuringTerminalTransition(
+                (execution, accumulators, metrics) ->
+                        // fromSchedulerNg=true to reach the FAILED transition.
+                        execution.markFailed(
+                                new Exception("test failure"),
+                                false,
+                                accumulators,
+                                metrics,
+                                false,
+                                true));
+    }
+
+    @Test
+    void testMetricsVisibleToListenersOnCompleteCancelling() throws Exception {
+        testMetricsVisibleToListenersDuringTerminalTransition(
+                (execution, accumulators, metrics) -> {
+                    // completeCancelling() transitions from CANCELING to 
CANCELED.
+                    execution.cancel();
+                    execution.completeCancelling(accumulators, metrics, true);
+                });
+    }
+
+    @Test
+    void testMetricsVisibleToListenersOnRecoverExecution() throws Exception {
+        testMetricsVisibleToListenersDuringTerminalTransition(
+                (execution, accumulators, metrics) ->
+                        // recoverExecution() transitions directly to FINISHED 
during JM failover
+                        // recovery.
+                        execution.recoverExecution(
+                                execution.getAttemptId(),
+                                new LocalTaskManagerLocation(),
+                                accumulators,
+                                metrics));
+    }
+
+    /**
+     * Verifies that IOMetrics and user accumulators are visible to {@link
+     * ExecutionStateUpdateListener}s at the time they are notified of a 
terminal state transition.
+     */
+    private void testMetricsVisibleToListenersDuringTerminalTransition(
+            TerminalStateTransition terminalStateTransition) throws Exception {
+        JobVertex v1 = new JobVertex("v1", new JobVertexID());
+
+        SchedulerBase scheduler = setupScheduler(v1, 1);
+        DefaultExecutionGraph graph = (DefaultExecutionGraph) 
scheduler.getExecutionGraph();
+
+        Execution execution = 
graph.getRegisteredExecutions().values().iterator().next();
+        ExecutionAttemptID attemptId = execution.getAttemptId();
+
+        // The listener receives an ExecutionAttemptID, not the Execution 
object, and the terminal
+        // transition deregisters the execution. Capture the metrics and 
accumulators directly from
+        // within the listener callback so we observe exactly what is visible 
at notification time.
+        AtomicReference<IOMetrics> metricsSeenByListener = new 
AtomicReference<>();
+        AtomicReference<Map<String, Accumulator<?, ?>>> 
accumulatorsSeenByListener =
+                new AtomicReference<>();
+        graph.registerExecutionStateUpdateListener(
+                (id, previousState, newState) -> {
+                    if (newState.isTerminal() && id.equals(attemptId)) {
+                        metricsSeenByListener.set(execution.getIOMetrics());
+                        
accumulatorsSeenByListener.set(execution.getUserAccumulators());
+                    }
+                });
+
+        IOMetrics ioMetrics = new IOMetrics(10, 20, 30, 40, 100, 200, 300);
+        Map<String, Accumulator<?, ?>> accumulators = new HashMap<>();
+        IntCounter counter = new IntCounter();
+        counter.add(42);
+        accumulators.put("acc", counter);
+
+        terminalStateTransition.transition(execution, accumulators, ioMetrics);
+
+        assertThat(metricsSeenByListener.get())
+                .as("IOMetrics visible to listener during terminal transition")
+                .isNotNull();
+        assertIOMetricsEqual(metricsSeenByListener.get(), ioMetrics);
+        assertThat(accumulatorsSeenByListener.get())
+                .as("User accumulators visible to listener during terminal 
transition")
+                .isEqualTo(accumulators);
+    }
+
+    @FunctionalInterface
+    private interface TerminalStateTransition {
+        void transition(
+                Execution execution,
+                Map<String, Accumulator<?, ?>> accumulators,
+                IOMetrics metrics);
+    }
+
     @Test
     void testRegistrationOfExecutionsCanceled() throws Exception {
 
@@ -501,20 +602,30 @@ class DefaultExecutionGraphDeploymentTest {
                 
.isEqualTo(CheckpointingOptions.MAX_RETAINED_CHECKPOINTS.defaultValue().intValue());
     }
 
+    private SchedulerBase setupScheduler(JobVertex v1, int dop1) throws 
Exception {
+        return setupScheduler(new JobVertex[] {v1}, new int[] {dop1});
+    }
+
     private SchedulerBase setupScheduler(JobVertex v1, int dop1, JobVertex v2, 
int dop2)
             throws Exception {
-        v1.setParallelism(dop1);
-        v2.setParallelism(dop2);
+        return setupScheduler(new JobVertex[] {v1, v2}, new int[] {dop1, 
dop2});
+    }
 
-        v1.setInvokableClass(BatchTask.class);
-        v2.setInvokableClass(BatchTask.class);
+    private SchedulerBase setupScheduler(JobVertex[] vertices, int[] 
parallelisms)
+            throws Exception {
+        int totalDop = 0;
+        for (int i = 0; i < vertices.length; i++) {
+            vertices[i].setParallelism(parallelisms[i]);
+            vertices[i].setInvokableClass(BatchTask.class);
+            totalDop += parallelisms[i];
+        }
 
         DirectScheduledExecutorService executorService = new 
DirectScheduledExecutorService();
 
         // execution graph that executes actions synchronously
         final SchedulerBase scheduler =
                 new DefaultSchedulerBuilder(
-                                JobGraphTestUtils.streamingJobGraph(v1, v2),
+                                JobGraphTestUtils.streamingJobGraph(vertices),
                                 
ComponentMainThreadExecutorServiceAdapter.forMainThread(),
                                 EXECUTOR_EXTENSION.getExecutor())
                         .setExecutionSlotAllocatorFactory(
@@ -530,7 +641,7 @@ class DefaultExecutionGraphDeploymentTest {
         scheduler.startScheduling();
 
         Map<ExecutionAttemptID, Execution> executions = 
eg.getRegisteredExecutions();
-        assertThat(executions).hasSize(dop1 + dop2);
+        assertThat(executions).hasSize(totalDop);
 
         return scheduler;
     }

Reply via email to