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

snuyanzin 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 c84cf68428d [FLINK-40925][rest] `JobVertexBackPressureHandler` throws 
`NoSuchElementException` when execution attempts are pruned concurrently
c84cf68428d is described below

commit c84cf68428d91c96ef6493ebe18b21e1d7f4998f
Author: Sergey Nuyanzin <[email protected]>
AuthorDate: Thu Oct 8 00:20:35 2026 +0200

    [FLINK-40925][rest] `JobVertexBackPressureHandler` throws 
`NoSuchElementException` when execution attempts are pruned concurrently
---
 .../handler/job/JobVertexBackPressureHandler.java  |   6 +-
 .../job/JobVertexBackPressureHandlerTest.java      | 127 +++++++++++++++++++--
 2 files changed, 123 insertions(+), 10 deletions(-)

diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandler.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandler.java
index cf292951b39..9ad6707e5dd 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandler.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandler.java
@@ -44,6 +44,7 @@ import javax.annotation.Nullable;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
@@ -114,8 +115,10 @@ public class JobVertexBackPressureHandler
         for (Map.Entry<Integer, SubtaskMetricStore> entry : 
subtaskMetricStores.entrySet()) {
             int subtaskIndex = entry.getKey();
             SubtaskMetricStore subtaskMetricStore = entry.getValue();
+            // Snapshot the live view over the concurrent attempts map so the 
size()/containsKey()/
+            // iterator()/get() calls below stay consistent if another thread 
mutates it.
             Map<Integer, ComponentMetricStore> allAttemptsMetricStores =
-                    subtaskMetricStore.getAllAttemptsMetricStores();
+                    new 
HashMap<>(subtaskMetricStore.getAllAttemptsMetricStores());
             if (allAttemptsMetricStores.isEmpty() || 
allAttemptsMetricStores.size() == 1) {
                 result.add(
                         createSubtaskAttemptBackpressureInfo(
@@ -126,7 +129,6 @@ public class JobVertexBackPressureHandler
                                 ? -1
                                 : 
representativeAttempts.getOrDefault(subtaskIndex, -1);
                 if 
(!allAttemptsMetricStores.containsKey(representativeAttempt)) {
-                    // allAttemptsMetricStores is not empty here
                     representativeAttempt = 
allAttemptsMetricStores.keySet().iterator().next();
                 }
                 List<SubtaskBackPressureInfo> otherConcurrentAttempts =
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandlerTest.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandlerTest.java
index 8850a065cbd..8c8b2589d85 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandlerTest.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/rest/handler/job/JobVertexBackPressureHandlerTest.java
@@ -27,6 +27,8 @@ import 
org.apache.flink.runtime.metrics.dump.QueryScopeInfo.TaskQueryScopeInfo;
 import org.apache.flink.runtime.rest.handler.HandlerRequest;
 import org.apache.flink.runtime.rest.handler.legacy.metrics.MetricFetcher;
 import org.apache.flink.runtime.rest.handler.legacy.metrics.MetricStore;
+import 
org.apache.flink.runtime.rest.handler.legacy.metrics.MetricStore.ComponentMetricStore;
+import 
org.apache.flink.runtime.rest.handler.legacy.metrics.MetricStore.SubtaskMetricStore;
 import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
 import org.apache.flink.runtime.rest.messages.JobIDPathParameter;
 import org.apache.flink.runtime.rest.messages.JobVertexBackPressureHeaders;
@@ -40,20 +42,25 @@ import 
org.apache.flink.runtime.webmonitor.TestingRestfulGateway;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.lang.reflect.Field;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
 import static 
org.apache.flink.runtime.rest.messages.JobVertexBackPressureInfo.VertexBackPressureLevel.HIGH;
 import static 
org.apache.flink.runtime.rest.messages.JobVertexBackPressureInfo.VertexBackPressureLevel.LOW;
 import static 
org.apache.flink.runtime.rest.messages.JobVertexBackPressureInfo.VertexBackPressureLevel.OK;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 
 /** Tests for {@link JobVertexBackPressureHandler}. */
 class JobVertexBackPressureHandlerTest {
@@ -120,7 +127,7 @@ class JobVertexBackPressureHandlerTest {
                 new JobVertexBackPressureHandler(
                         () -> 
CompletableFuture.completedFuture(restfulGateway),
                         Duration.ofSeconds(10),
-                        Collections.emptyMap(),
+                        Map.of(),
                         JobVertexBackPressureHeaders.getInstance(),
                         new MetricFetcher() {
                             private long updateCount = 0;
@@ -211,8 +218,8 @@ class JobVertexBackPressureHandlerTest {
                         EmptyRequestBody.getInstance(),
                         new JobVertexMessageParameters(),
                         pathParameters,
-                        Collections.emptyMap(),
-                        Collections.emptyList());
+                        Map.of(),
+                        List.of());
 
         final CompletableFuture<JobVertexBackPressureInfo>
                 jobVertexBackPressureInfoCompletableFuture =
@@ -266,8 +273,8 @@ class JobVertexBackPressureHandlerTest {
                         EmptyRequestBody.getInstance(),
                         new JobVertexMessageParameters(),
                         pathParameters,
-                        Collections.emptyMap(),
-                        Collections.emptyList());
+                        Map.of(),
+                        List.of());
 
         final CompletableFuture<JobVertexBackPressureInfo>
                 jobVertexBackPressureInfoCompletableFuture =
@@ -300,7 +307,7 @@ class JobVertexBackPressureHandlerTest {
                 new JobVertexBackPressureHandler(
                         () -> 
CompletableFuture.completedFuture(restfulGateway),
                         Duration.ofSeconds(10),
-                        Collections.emptyMap(),
+                        Map.of(),
                         JobVertexBackPressureHeaders.getInstance(),
                         new MetricFetcher() {
                             private long updateCount = 0;
@@ -331,8 +338,8 @@ class JobVertexBackPressureHandlerTest {
                         EmptyRequestBody.getInstance(),
                         new JobVertexMessageParameters(),
                         pathParameters,
-                        Collections.emptyMap(),
-                        Collections.emptyList());
+                        Map.of(),
+                        List.of());
 
         final CompletableFuture<JobVertexBackPressureInfo>
                 jobVertexBackPressureInfoCompletableFuture =
@@ -427,4 +434,108 @@ class JobVertexBackPressureHandlerTest {
                                 .collect(Collectors.toList()))
                 .containsExactly(0, 1);
     }
+
+    /**
+     * The handler must not throw when a subtask's attempts are pruned 
concurrently (by {@code
+     * SubtaskMetricStore.retainAttempts()}) between its {@code size()}/{@code 
containsKey()} checks
+     * and the {@code keySet().iterator().next()} read.
+     */
+    @Test
+    void testGetBackPressureWhenAttemptsRemovedConcurrently() throws Exception 
{
+        MetricStore multipleAttemptsMetricStore = new MetricStore();
+        for (MetricDump metricDump : getMultipleAttemptsMetricDumps()) {
+            multipleAttemptsMetricStore.add(metricDump);
+        }
+
+        // Empty subtask 0's attempts in the window between the handler's 
check and its iteration.
+        SubtaskMetricStore subtask =
+                multipleAttemptsMetricStore
+                        .getTaskMetricStore(
+                                
TEST_JOB_ID_BACK_PRESSURE_STATS_AVAILABLE.toString(),
+                                TEST_JOB_VERTEX_ID.toString())
+                        .getAllSubtaskMetricStores()
+                        .get(0);
+        Field attemptsField = 
SubtaskMetricStore.class.getDeclaredField("attempts");
+        attemptsField.setAccessible(true);
+
+        Map<Integer, ComponentMetricStore> originalAttempts =
+                (Map<Integer, ComponentMetricStore>) 
attemptsField.get(subtask);
+        attemptsField.set(subtask, new SelfEmptyingOnKeySet(originalAttempts));
+
+        JobVertexBackPressureHandler handler =
+                new JobVertexBackPressureHandler(
+                        () -> 
CompletableFuture.completedFuture(restfulGateway),
+                        Duration.ofSeconds(10),
+                        Map.of(),
+                        JobVertexBackPressureHeaders.getInstance(),
+                        new MetricFetcher() {
+                            @Override
+                            public MetricStore getMetricStore() {
+                                return multipleAttemptsMetricStore;
+                            }
+
+                            @Override
+                            public void update() {}
+
+                            @Override
+                            public long getLastUpdateTime() {
+                                return 0;
+                            }
+                        });
+
+        Map<String, String> pathParameters = new HashMap<>();
+        pathParameters.put(
+                JobIDPathParameter.KEY, 
TEST_JOB_ID_BACK_PRESSURE_STATS_AVAILABLE.toString());
+        pathParameters.put(JobVertexIdPathParameter.KEY, 
TEST_JOB_VERTEX_ID.toString());
+        HandlerRequest<EmptyRequestBody> request =
+                HandlerRequest.resolveParametersAndCreate(
+                        EmptyRequestBody.getInstance(),
+                        new JobVertexMessageParameters(),
+                        pathParameters,
+                        Map.of(),
+                        List.of());
+
+        assertThatCode(() -> handler.handleRequest(request, restfulGateway))
+                .doesNotThrowAnyException();
+    }
+
+    private static final class SelfEmptyingOnKeySet
+            extends ConcurrentHashMap<Integer, ComponentMetricStore> {
+        private boolean emptied;
+
+        private SelfEmptyingOnKeySet(Map<Integer, ComponentMetricStore> 
initial) {
+            super(initial);
+        }
+
+        private void emptyOnce() {
+            if (!emptied) {
+                emptied = true;
+                clear();
+            }
+        }
+
+        @Override
+        public KeySetView<Integer, ComponentMetricStore> keySet() {
+            emptyOnce();
+            return super.keySet();
+        }
+
+        @Override
+        public KeySetView<Integer, ComponentMetricStore> 
keySet(ComponentMetricStore mappedValue) {
+            emptyOnce();
+            return super.keySet(mappedValue);
+        }
+
+        @Override
+        public Collection<ComponentMetricStore> values() {
+            emptyOnce();
+            return super.values();
+        }
+
+        @Override
+        public Set<Entry<Integer, ComponentMetricStore>> entrySet() {
+            emptyOnce();
+            return super.entrySet();
+        }
+    }
 }

Reply via email to