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

MartijnVisser 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 f63fac7fed3 [FLINK-36146][connector-base] Fix 
SingleThreadFetcherManager race condition
f63fac7fed3 is described below

commit f63fac7fed3ca011ee0c58958695a0af7697f832
Author: Kim Gräsman <[email protected]>
AuthorDate: Mon Sep 16 08:50:16 2024 +0200

    [FLINK-36146][connector-base] Fix SingleThreadFetcherManager race condition
    
    This issue triggered when running a highly parallel batch job with
    FileSource pointed at a large S3 bucket. The visible symptom was:
    
        java.util.NoSuchElementException
            at 
java.base/java.util.concurrent.ConcurrentHashMap$ValueIterator.next(ConcurrentHashMap.java:3471)
            at 
org.apache.flink.connector.base.source.reader.fetcher.SingleThreadFetcherManager.getRunningFetcher(SingleThreadFetcherManager.java:94)
            at 
org.apache.flink.connector.base.source.reader.fetcher.SingleThreadFetcherManager.addSplits(SingleThreadFetcherManager.java:82)
            at 
org.apache.flink.connector.base.source.reader.SourceReaderBase.addSplits(SourceReaderBase.java:242)
            at 
org.apache.flink.streaming.api.operators.SourceOperator.handleOperatorEvent(SourceOperator.java:428)
            ...
    
    While the exact sequence of events is unclear, there's an obvious
    time-of-check/time-of-use bug in getRunningFetcher where fetchers may be
    cleared between the time it's checked for empty and the time we try to
    take the first element.
    
    Take the fetcher from a single iterator instead. ConcurrentHashMap's
    iterator reads the next entry ahead, so next() cannot throw once
    hasNext() returned true, even if the fetcher is removed concurrently.
    
    Add a test.
    
    Co-authored-by: Sergey Nuyanzin <[email protected]>
    Co-authored-by: Martijn Visser 
<[email protected]>
    Generated-by: Claude Code (Claude Opus 5.5)
---
 .../reader/fetcher/SingleThreadFetcherManager.java |  4 ++-
 .../reader/fetcher/SplitFetcherManagerTest.java    | 36 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git 
a/flink-connectors/flink-connector-base/src/main/java/org/apache/flink/connector/base/source/reader/fetcher/SingleThreadFetcherManager.java
 
b/flink-connectors/flink-connector-base/src/main/java/org/apache/flink/connector/base/source/reader/fetcher/SingleThreadFetcherManager.java
index adb903cbcd6..f13c523686f 100644
--- 
a/flink-connectors/flink-connector-base/src/main/java/org/apache/flink/connector/base/source/reader/fetcher/SingleThreadFetcherManager.java
+++ 
b/flink-connectors/flink-connector-base/src/main/java/org/apache/flink/connector/base/source/reader/fetcher/SingleThreadFetcherManager.java
@@ -27,6 +27,7 @@ import 
org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
 import javax.annotation.Nullable;
 
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -122,6 +123,7 @@ public class SingleThreadFetcherManager<E, SplitT extends 
SourceSplit>
     }
 
     protected SplitFetcher<E, SplitT> getRunningFetcher() {
-        return fetchers.isEmpty() ? null : fetchers.values().iterator().next();
+        Iterator<SplitFetcher<E, SplitT>> iter = fetchers.values().iterator();
+        return iter.hasNext() ? iter.next() : null;
     }
 }
diff --git 
a/flink-connectors/flink-connector-base/src/test/java/org/apache/flink/connector/base/source/reader/fetcher/SplitFetcherManagerTest.java
 
b/flink-connectors/flink-connector-base/src/test/java/org/apache/flink/connector/base/source/reader/fetcher/SplitFetcherManagerTest.java
index bcf8bb934de..4a603dd0411 100644
--- 
a/flink-connectors/flink-connector-base/src/test/java/org/apache/flink/connector/base/source/reader/fetcher/SplitFetcherManagerTest.java
+++ 
b/flink-connectors/flink-connector-base/src/test/java/org/apache/flink/connector/base/source/reader/fetcher/SplitFetcherManagerTest.java
@@ -44,14 +44,18 @@ import org.slf4j.MDC;
 import javax.annotation.Nullable;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.time.Duration;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 import static org.apache.flink.test.util.TestUtils.waitUntil;
@@ -309,6 +313,38 @@ class SplitFetcherManagerTest {
         }
     }
 
+    @Test
+    void testGetRunningFetcherSurvivesFetcherRemovalInToctouWindow() throws 
Exception {
+        final SingleThreadFetcherManager<Object, TestingSourceSplit> 
fetcherManager =
+                new SingleThreadFetcherManager<>(TestingSplitReader::new, new 
Configuration());
+
+        final SplitFetcher<Object, TestingSourceSplit> fetcher =
+                fetcherManager.createSplitFetcher();
+
+        final Map<Integer, SplitFetcher<Object, TestingSourceSplit>> 
racyFetchers =
+                new ConcurrentHashMap<Integer, SplitFetcher<Object, 
TestingSourceSplit>>() {
+                    private boolean removed;
+
+                    @Override
+                    public Collection<SplitFetcher<Object, 
TestingSourceSplit>> values() {
+                        // The shutdown callback (fetchers.remove(...)) fires 
here: after
+                        // getRunningFetcher()'s isEmpty() check saw the 
entry, before it is read.
+                        if (!removed) {
+                            removed = true;
+                            clear();
+                        }
+                        return super.values();
+                    }
+                };
+        racyFetchers.put(0, fetcher);
+
+        final Field fetchersField = 
SplitFetcherManager.class.getDeclaredField("fetchers");
+        fetchersField.setAccessible(true);
+        fetchersField.set(fetcherManager, racyFetchers);
+
+        assertThat(fetcherManager.getRunningFetcher()).isNull();
+    }
+
     // the final modifier is important so that '@SafeVarargs' is accepted on 
Java 8
     @SuppressWarnings("FinalPrivateMethod")
     @SafeVarargs

Reply via email to