Repository: apache/flink-connector-aws
Pull request #195:  [FLINK-36947][Connectors/Kinesis] Fix issue where excessive GetRecords calls are made on idle source causing high CPU utilisation and throttling. [FLINK-36939][Connectors/Kinesis] Fix issue where excessive BlockingQueue.poll() in FanOutKinesisShardSplitReader and GetRecords in PollingKinesisShardSplitReader calls are made on idle source causing high CPU utilisation
Author: login: leekeiabstraction
Created: 2025-04-03T16:34:38Z | Merged: 2025-04-15T14:51:37Z

## Purpose of the change

[FLINK-36947][Connectors/Kinesis] Fix issue where excessive GetRecords in PollingKinesisShardSplitReader calls are made on idle source causing high CPU utilisation and throttling
[FLINK-36939][Connectors/Kinesis] Fix issue where excessive BlockingQueue.poll() in FanOutKinesisShardSplitReader and GetRecords in PollingKinesisShardSplitReader calls are made on idle source causing high CPU utilisation

Apologies, PR is recreated as I mistakenly deleted the PR branch. Earlier PR: https://github.com/apache/flink-connector-aws/pull/194

https://issues.apache.org/jira/browse/FLINK-36947
https://issues.apache.org/jira/browse/FLINK-36939

## Verifying this change

- *Added unit tests*
- *Manually verified by running the Kinesis connector on a local Flink cluster.*

## Significant changes
*(Please check any boxes [x] if the answer is "yes". You can first publish the PR and check them afterwards, for convenience.)*
- [ ] Dependencies have been added or upgraded
- [ ] Public API has been changed (Public API is any class annotated with `@Public(Evolving)`)
- [ ] Serializers have been changed
- [X] New feature has been introduced
  - If yes, how is this documented? (JavaDocs)



Diff:
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/pom.xml b/flink-connector-aws/flink-connector-aws-kinesis-streams/pom.xml
index 3ba20c63..836d3341 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/pom.xml
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/pom.xml
@@ -153,6 +153,12 @@ under the License.
             <artifactId>flink-architecture-tests-test</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/KinesisStreamsSource.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/KinesisStreamsSource.java
index 5fba71c6..e07add14 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/KinesisStreamsSource.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/KinesisStreamsSource.java
@@ -81,7 +81,6 @@
 import java.util.function.Supplier;
 
 import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_NAME;
-import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
 import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_DESCRIBE_CONSUMER_RETRY_STRATEGY_MAX_ATTEMPTS_OPTION;
 import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_DESCRIBE_CONSUMER_RETRY_STRATEGY_MAX_DELAY_OPTION;
 import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_DESCRIBE_CONSUMER_RETRY_STRATEGY_MIN_DELAY_OPTION;
@@ -228,7 +227,7 @@ private Supplier<SplitReader<Record, KinesisShardSplit>> getKinesisShardSplitRea
                                 createKinesisAsyncStreamProxy(streamArn, sourceConfig),
                                 consumerArn,
                                 shardMetricGroupMap,
-                                sourceConfig.get(EFO_CONSUMER_SUBSCRIPTION_TIMEOUT));
+                                sourceConfig);
             default:
                 throw new IllegalArgumentException("Unsupported reader type: " + readerType);
         }
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/config/KinesisSourceConfigOptions.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/config/KinesisSourceConfigOptions.java
index 92e18896..c310d7ce 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/config/KinesisSourceConfigOptions.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/config/KinesisSourceConfigOptions.java
@@ -85,6 +85,13 @@ public enum ConsumerLifecycle {
                     .defaultValue(ReaderType.POLLING)
                     .withDescription("The type of reader used to read from the Kinesis stream.");
 
+    public static final ConfigOption<Duration> READER_EMPTY_RECORDS_FETCH_INTERVAL =
+            ConfigOptions.key("source.reader.empty-records-fetch-interval")
+                    .durationType()
+                    .defaultValue(Duration.ofMillis(250))
+                    .withDescription(
+                            "The interval in milliseconds between fetches with empty records");
+
     public static final ConfigOption<ConsumerLifecycle> EFO_CONSUMER_LIFECYCLE =
             ConfigOptions.key("source.efo.lifecycle")
                     .enumType(ConsumerLifecycle.class)
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
index 3de94484..e96a9104 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
@@ -19,9 +19,11 @@
 package org.apache.flink.connector.kinesis.source.reader;
 
 import org.apache.flink.annotation.Internal;
+import org.apache.flink.configuration.Configuration;
 import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitsChange;
+import org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions;
 import org.apache.flink.connector.kinesis.source.metrics.KinesisShardMetrics;
 import org.apache.flink.connector.kinesis.source.split.KinesisShardSplit;
 import org.apache.flink.connector.kinesis.source.split.KinesisShardSplitState;
@@ -38,12 +40,14 @@
 import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.WeakHashMap;
 
 import static java.util.Collections.singleton;
 
@@ -60,8 +64,17 @@ public abstract class KinesisShardSplitReaderBase
     private final Set<String> pausedSplitIds = new HashSet<>();
     private final Map<String, KinesisShardMetrics> shardMetricGroupMap;
 
-    protected KinesisShardSplitReaderBase(Map<String, KinesisShardMetrics> shardMetricGroupMap) {
+    private final long emptyRecordsIntervalMillis;
+
+    private final Map<KinesisShardSplitState, Long> scheduledFetchTimes = new WeakHashMap<>();
+
+    protected KinesisShardSplitReaderBase(
+            Map<String, KinesisShardMetrics> shardMetricGroupMap, Configuration configuration) {
         this.shardMetricGroupMap = shardMetricGroupMap;
+        this.emptyRecordsIntervalMillis =
+                configuration
+                        .get(KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL)
+                        .toMillis();
     }
 
     @Override
@@ -69,7 +82,12 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
         KinesisShardSplitState splitState = assignedSplits.poll();
 
         // When there are no assigned splits, return quickly
-        if (splitState == null) {
+        if (skipWhenNoAssignedSplit(splitState)) {
+            return INCOMPLETE_SHARD_EMPTY_RECORDS;
+        }
+
+        if (skipUntilScheduledFetchTime(splitState)) {
+            assignedSplits.add(splitState);
             return INCOMPLETE_SHARD_EMPTY_RECORDS;
         }
 
@@ -82,6 +100,7 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
         RecordBatch recordBatch;
         try {
             recordBatch = fetchRecords(splitState);
+            scheduleNextFetchTime(splitState, recordBatch);
         } catch (ResourceNotFoundException e) {
             LOG.warn(
                     "Failed to fetch records from shard {}: shard no longer exists. Marking split as complete",
@@ -125,6 +144,66 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledFetchTimes.containsKey(splitState)
+                && scheduledFetchTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IOException(
+                        "Sleep was interrupted while skipping until scheduled fetch record time",
+                        e);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Schedules next fetch time, to be called immediately on the result of a fetchRecords() call.
+     *
+     * <p>If recordBatch does not contain records, next fetchRecords() is scheduled. Before
+     * scheduled time, fetcher thread will skip fetching (and have small sleep) for the split.
+     *
+     * <p>If recordBatch is not empty, next fetchRecords() time is not scheduled resulting in next
+     * fetch on the split is performed at first opportunity.
+     *
+     * @param splitState splitState on which the fetchRecords() was called on
+     * @param recordBatch recordBatch returned by fetchRecords()
+     */
+    private void scheduleNextFetchTime(KinesisShardSplitState splitState, RecordBatch recordBatch) {
+        if (recordBatch == null || recordBatch.getRecords().isEmpty()) {
+            long scheduledGetRecordTimeMillis =
+                    System.currentTimeMillis() + emptyRecordsIntervalMillis;
+            this.scheduledFetchTimes.put(splitState, scheduledGetRecordTimeMillis);
+            if (LOG.isDebugEnabled()) {
+                LOG.debug(
+                        "Fetched zero records from split {}, scheduling next fetch to {}",
+                        splitState.getSplitId(),
+                        new Date(scheduledGetRecordTimeMillis).toInstant());
+            }
+        }
+    }
+
     /**
      * Main method implementations must implement to fetch records from Kinesis.
      *
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java
index 8ae20293..c0aefee5 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java
@@ -19,6 +19,7 @@
 package org.apache.flink.connector.kinesis.source.reader.fanout;
 
 import org.apache.flink.annotation.Internal;
+import org.apache.flink.configuration.Configuration;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitsChange;
 import org.apache.flink.connector.kinesis.source.metrics.KinesisShardMetrics;
 import org.apache.flink.connector.kinesis.source.proxy.AsyncStreamProxy;
@@ -32,6 +33,8 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
+
 /**
  * An implementation of the KinesisShardSplitReader that consumes from Kinesis using Enhanced
  * Fan-Out and HTTP/2.
@@ -48,11 +51,11 @@ public FanOutKinesisShardSplitReader(
             AsyncStreamProxy asyncStreamProxy,
             String consumerArn,
             Map<String, KinesisShardMetrics> shardMetricGroupMap,
-            Duration subscriptionTimeout) {
-        super(shardMetricGroupMap);
+            Configuration configuration) {
+        super(shardMetricGroupMap, configuration);
         this.asyncStreamProxy = asyncStreamProxy;
         this.consumerArn = consumerArn;
-        this.subscriptionTimeout = subscriptionTimeout;
+        this.subscriptionTimeout = configuration.get(EFO_CONSUMER_SUBSCRIPTION_TIMEOUT);
     }
 
     @Override
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
index d2d6cd54..c3dca833 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
@@ -44,7 +44,7 @@ public PollingKinesisShardSplitReader(
             StreamProxy kinesisProxy,
             Map<String, KinesisShardMetrics> shardMetricGroupMap,
             Configuration configuration) {
-        super(shardMetricGroupMap);
+        super(shardMetricGroupMap, configuration);
         this.kinesis = kinesisProxy;
         this.configuration = configuration;
         this.maxRecordsToGet = configuration.get(KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX);
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBaseTest.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBaseTest.java
new file mode 100644
index 00000000..bc8b1b5f
--- /dev/null
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBaseTest.java
@@ -0,0 +1,229 @@
+package org.apache.flink.connector.kinesis.source.reader;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.base.source.reader.splitreader.SplitsAddition;
+import org.apache.flink.connector.kinesis.source.metrics.KinesisShardMetrics;
+import org.apache.flink.connector.kinesis.source.split.KinesisShardSplit;
+import org.apache.flink.connector.kinesis.source.split.KinesisShardSplitState;
+import org.apache.flink.metrics.testutils.MetricListener;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL;
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX;
+import static org.apache.flink.connector.kinesis.source.util.TestUtil.generateShardId;
+import static org.apache.flink.connector.kinesis.source.util.TestUtil.getTestSplit;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class KinesisShardSplitReaderBaseTest {
+
+    private static Configuration newConfigurationForTest() {
+        return new Configuration().set(SHARD_GET_RECORDS_MAX, 50);
+    }
+
+    private static Stream<Arguments> readerTypeAndInterval() {
+        return Stream.of(
+                Arguments.of(NullReturningReader.class, 0L),
+                Arguments.of(NullReturningReader.class, 250L),
+                Arguments.of(NullReturningReader.class, 1000L),
+                Arguments.of(EmptyRecordReturningReader.class, 0L),
+                Arguments.of(EmptyRecordReturningReader.class, 250L),
+                Arguments.of(EmptyRecordReturningReader.class, 1000L));
+    }
+
+    @MethodSource("readerTypeAndInterval")
+    @ParameterizedTest
+    public void testGetRecordsIntervalForIdleSource(
+            Class<? extends CountingReader> readerClass, long interval) {
+        Configuration configuration = newConfigurationForTest();
+        configuration.set(READER_EMPTY_RECORDS_FETCH_INTERVAL, Duration.ofMillis(interval));
+
+        // Given reader with custom interval
+        List<KinesisShardSplit> shardSplits = createShardSplits(8);
+        Map<String, KinesisShardMetrics> metrics = getShardMetrics(shardSplits);
+        CountingReader reader = buildReader(readerClass, configuration, metrics);
+
+        reader.handleSplitsChanges(new SplitsAddition<>(shardSplits));
+
+        // When (empty) records are fetched continuously
+        await().pollInSameThread()
+                .pollInterval(Duration.ofMillis(1))
+                .atMost(interval + 1000L, TimeUnit.MILLISECONDS)
+                .untilAsserted(
+                        () -> {
+                            reader.fetch();
+
+                            // Then call fetch record at intervals
+                            for (List<Long> fetchRecordsCallTimes :
+                                    reader.getFetchRecordsCallTimestamps().values()) {
+                                assertThat(fetchRecordsCallTimes.size()).isEqualTo(2);
+
+                                // Ensure interval between fetchRecord calls is between configured
+                                // interval and interval + 250 milliseconds (allowing 250
+                                // milliseconds of leeway)
+                                assertThat(
+                                                fetchRecordsCallTimes.get(1)
+                                                        - fetchRecordsCallTimes.get(0))
+                                        .isBetween(interval, interval + 250L);
+                            }
+
+                            assertThat(reader.getFetchRecordsCallTimestamps().size()).isEqualTo(8);
+                        });
+    }
+
+    private static Stream<Arguments> readerTypeAndShardCount() {
+        return Stream.of(
+                Arguments.of(NullReturningReader.class, 1),
+                Arguments.of(NullReturningReader.class, 8),
+                Arguments.of(NullReturningReader.class, 64),
+                Arguments.of(EmptyRecordReturningReader.class, 1),
+                Arguments.of(EmptyRecordReturningReader.class, 8),
+                Arguments.of(EmptyRecordReturningReader.class, 64));
+    }
+
+    @MethodSource("readerTypeAndShardCount")
+    @ParameterizedTest
+    public void testFetchRecordsIntervalForMultipleIdleSource(
+            Class<? extends CountingReader> readerClass, int shardCount) {
+        // Given reader with shard count
+        List<KinesisShardSplit> shardSplits = createShardSplits(shardCount);
+        Map<String, KinesisShardMetrics> metrics = getShardMetrics(shardSplits);
+        CountingReader reader = buildReader(readerClass, newConfigurationForTest(), metrics);
+
+        reader.handleSplitsChanges(new SplitsAddition<>(shardSplits));
+
+        // When (empty) records are fetched continuously
+        await().pollInSameThread()
+                .pollInterval(Duration.ofMillis(1))
+                .atMost(250L + 1000L, TimeUnit.MILLISECONDS)
+                .untilAsserted(
+                        () -> {
+                            reader.fetch();
+
+                            // Then call fetch record at intervals
+                            for (List<Long> fetchRecordCallTime :
+                                    reader.getFetchRecordsCallTimestamps().values()) {
+                                assertThat(fetchRecordCallTime.size()).isEqualTo(2);
+
+                                // Default READER_EMPTY_RECORDS_FETCH_INTERVAL value at 250
+                                // Ensure interval between fetchRecord calls is between 250 and 500
+                                // milliseconds (allowing 250 milliseconds of leeway)
+                                assertThat(fetchRecordCallTime.get(1) - fetchRecordCallTime.get(0))
+                                        .isBetween(250L, 250L + 250L);
+                            }
+
+                            assertThat(reader.getFetchRecordsCallTimestamps().size())
+                                    .isEqualTo(shardCount);
+                        });
+    }
+
+    private static CountingReader buildReader(
+            Class<? extends CountingReader> readerClass,
+            Configuration configuration,
+            Map<String, KinesisShardMetrics> metrics) {
+        if (readerClass == NullReturningReader.class) {
+            return new NullReturningReader(metrics, configuration);
+        } else if (readerClass == EmptyRecordReturningReader.class) {
+            return new EmptyRecordReturningReader(metrics, configuration);
+        }
+
+        throw new RuntimeException(
+                "No test implementation found for " + readerClass.getCanonicalName());
+    }
+
+    abstract static class CountingReader extends KinesisShardSplitReaderBase {
+
+        private Map<KinesisShardSplitState, List<Long>> fetchRecordsCallTimestamps;
+
+        protected CountingReader(
+                Map<String, KinesisShardMetrics> shardMetricGroupMap, Configuration configuration) {
+            super(shardMetricGroupMap, configuration);
+            fetchRecordsCallTimestamps = new HashMap<>();
+        }
+
+        @Override
+        protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+            recordFetchTimestamp(splitState);
+
+            return null;
+        }
+
+        private void recordFetchTimestamp(KinesisShardSplitState splitState) {
+            if (fetchRecordsCallTimestamps.containsKey(splitState)) {
+                fetchRecordsCallTimestamps.get(splitState).add(System.currentTimeMillis());
+            } else {
+                ArrayList<Long> fetchRecordCallTime = new ArrayList<>();
+                fetchRecordCallTime.add(System.currentTimeMillis());
+                fetchRecordsCallTimestamps.put(splitState, fetchRecordCallTime);
+            }
+        }
+
+        @Override
+        public void close() throws Exception {}
+
+        public Map<KinesisShardSplitState, List<Long>> getFetchRecordsCallTimestamps() {
+            return fetchRecordsCallTimestamps;
+        }
+    }
+
+    static class NullReturningReader extends CountingReader {
+        public NullReturningReader(
+                Map<String, KinesisShardMetrics> shardMetricGroupMap, Configuration configuration) {
+            super(shardMetricGroupMap, configuration);
+        }
+
+        @Override
+        protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+            super.fetchRecords(splitState);
+            return null;
+        }
+    }
+
+    static class EmptyRecordReturningReader extends CountingReader {
+        public EmptyRecordReturningReader(
+                Map<String, KinesisShardMetrics> shardMetricGroupMap, Configuration configuration) {
+            super(shardMetricGroupMap, configuration);
+        }
+
+        @Override
+        protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+            super.fetchRecords(splitState);
+            return new RecordBatch(Collections.emptyList(), 0L, false);
+        }
+    }
+
+    private static List<KinesisShardSplit> createShardSplits(int shardCount) {
+        return IntStream.range(0, shardCount)
+                .mapToObj(shardId -> getTestSplit(generateShardId(shardId)))
+                .collect(Collectors.toList());
+    }
+
+    private static Map<String, KinesisShardMetrics> getShardMetrics(
+            List<KinesisShardSplit> shardSplits) {
+        Map<String, KinesisShardMetrics> metrics = new HashMap<>();
+        MetricListener metricListener = new MetricListener();
+
+        shardSplits.forEach(
+                shardSplit ->
+                        metrics.put(
+                                shardSplit.splitId(),
+                                new KinesisShardMetrics(
+                                        shardSplit, metricListener.getMetricGroup())));
+
+        return metrics;
+    }
+}
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java
index 2308b670..e19cc70f 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java
@@ -57,25 +57,25 @@ class PollingKinesisShardSplitReaderTest {
     private TestKinesisStreamProxy testStreamProxy;
     private MetricListener metricListener;
     private Map<String, KinesisShardMetrics> shardMetricGroupMap;
-    private Configuration sourceConfig;
     private static final String TEST_SHARD_ID = TestUtil.generateShardId(1);
 
+    private Configuration newConfigurationForTest() {
+        return new Configuration().set(SHARD_GET_RECORDS_MAX, 50);
+    }
+
     @BeforeEach
     public void init() {
         testStreamProxy = getTestStreamProxy();
         metricListener = new MetricListener();
         shardMetricGroupMap = new ConcurrentHashMap<>();
 
-        sourceConfig = new Configuration();
-        sourceConfig.set(SHARD_GET_RECORDS_MAX, 50);
-
         shardMetricGroupMap.put(
                 TEST_SHARD_ID,
                 new KinesisShardMetrics(
                         TestUtil.getTestSplit(TEST_SHARD_ID), metricListener.getMetricGroup()));
         splitReader =
                 new PollingKinesisShardSplitReader(
-                        testStreamProxy, shardMetricGroupMap, sourceConfig);
+                        testStreamProxy, shardMetricGroupMap, newConfigurationForTest());
     }
 
     @Test
@@ -374,10 +374,11 @@ void testFetchUpdatesTheMillisBehindLatestMetric() throws IOException {
     @Test
     void testMaxRecordsToGetParameterPassed() throws IOException {
         int maxRecordsToGet = 2;
-        sourceConfig.set(SHARD_GET_RECORDS_MAX, maxRecordsToGet);
+        Configuration configuration = newConfigurationForTest();
+        configuration.set(SHARD_GET_RECORDS_MAX, maxRecordsToGet);
         splitReader =
                 new PollingKinesisShardSplitReader(
-                        testStreamProxy, shardMetricGroupMap, sourceConfig);
+                        testStreamProxy, shardMetricGroupMap, configuration);
         testStreamProxy.addShards(TEST_SHARD_ID);
         List<Record> sentRecords =
                 Stream.of(getTestRecord("data-1"), getTestRecord("data-2"), getTestRecord("data-3"))
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
index e065d4d2..fbaaf696 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.flink.connector.kinesis.source.reader.fanout;
 
+import org.apache.flink.configuration.Configuration;
 import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitsAddition;
@@ -40,6 +41,7 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.CONSUMER_ARN;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.getTestSplit;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
@@ -49,14 +51,17 @@
 /** Test for {@link FanOutKinesisShardSplitReader}. */
 public class FanOutKinesisShardSplitReaderTest {
     private static final String TEST_SHARD_ID = TestUtil.generateShardId(1);
-    private static final Duration TEST_SUBSCRIPTION_TIMEOUT = Duration.ofMillis(1000);
 
-    SplitReader<Record, KinesisShardSplit> splitReader;
+    FanOutKinesisShardSplitReader splitReader;
 
     private AsyncStreamProxy testAsyncStreamProxy;
     private Map<String, KinesisShardMetrics> shardMetricGroupMap;
     private MetricListener metricListener;
 
+    private Configuration newConfigurationForTest() {
+        return new Configuration().set(EFO_CONSUMER_SUBSCRIPTION_TIMEOUT, Duration.ofMillis(1000));
+    }
+
     @BeforeEach
     public void init() {
         metricListener = new MetricListener();
@@ -77,7 +82,7 @@ public void testNoAssignedSplitsHandledGracefully() throws Exception {
                         testAsyncStreamProxy,
                         CONSUMER_ARN,
                         shardMetricGroupMap,
-                        TEST_SUBSCRIPTION_TIMEOUT);
+                        newConfigurationForTest());
         RecordsWithSplitIds<Record> retrievedRecords = splitReader.fetch();
 
         assertThat(retrievedRecords.nextRecordFromSplit()).isNull();
@@ -94,7 +99,7 @@ public void testAssignedSplitHasNoRecordsHandledGracefully() throws Exception {
                         testAsyncStreamProxy,
                         CONSUMER_ARN,
                         shardMetricGroupMap,
-                        TEST_SUBSCRIPTION_TIMEOUT);
+                        newConfigurationForTest());
         splitReader.handleSplitsChanges(
                 new SplitsAddition<>(Collections.singletonList(getTestSplit(TEST_SHARD_ID))));
 
@@ -117,7 +122,7 @@ public void testSplitWithExpiredShardHandledAsCompleted() throws Exception {
                         testAsyncStreamProxy,
                         CONSUMER_ARN,
                         shardMetricGroupMap,
-                        TEST_SUBSCRIPTION_TIMEOUT);
+                        newConfigurationForTest());
         splitReader.handleSplitsChanges(
                 new SplitsAddition<>(Collections.singletonList(getTestSplit(TEST_SHARD_ID))));
 
@@ -138,7 +143,7 @@ public void testWakeUpIsNoOp() {
                         testAsyncStreamProxy,
                         CONSUMER_ARN,
                         shardMetricGroupMap,
-                        TEST_SUBSCRIPTION_TIMEOUT);
+                        newConfigurationForTest());
 
         // When wakeup is called
         // Then no exception is thrown and no-op
@@ -155,7 +160,7 @@ public void testCloseClosesStreamProxy() throws Exception {
                         trackCloseStreamProxy,
                         CONSUMER_ARN,
                         shardMetricGroupMap,
-                        TEST_SUBSCRIPTION_TIMEOUT);
+                        newConfigurationForTest());
 
         // When split reader is not closed
         // Then stream proxy is still open
diff --git a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/util/KinesisStreamProxyProvider.java b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/util/KinesisStreamProxyProvider.java
index 5b794300..863b855c 100644
--- a/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/util/KinesisStreamProxyProvider.java
+++ b/flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/util/KinesisStreamProxyProvider.java
@@ -94,6 +94,8 @@ public static class TestKinesisStreamProxy implements StreamProxy {
         private final Map<String, Set<String>> efoConsumerRegistration = new HashMap<>();
         private final Set<String> consumersCurrentlyDeleting = new HashSet<>();
 
+        private int totalGetRecordsCall = 0;
+
         @Override
         public StreamDescriptionSummary getStreamDescriptionSummary(String streamArn) {
             return StreamDescriptionSummary.builder()
@@ -137,6 +139,7 @@ public GetRecordsResponse getRecords(
                 String shardId,
                 StartingPosition startingPosition,
                 int maxRecordsToGet) {
+            totalGetRecordsCall++;
             ShardHandle shardHandle = new ShardHandle(streamArn, shardId);
 
             if (getRecordsExceptionSupplier != null) {
@@ -350,6 +353,10 @@ public boolean isClosed() {
             return closed;
         }
 
+        public int getTotalGetRecordsCall() {
+            return totalGetRecordsCall;
+        }
+
         private static class ShardHandle {
             private final String streamArn;
             private final String shardId;
diff --git a/flink-connector-aws/flink-connector-dynamodb/pom.xml b/flink-connector-aws/flink-connector-dynamodb/pom.xml
index 3cbb7e08..06ae5cd4 100644
--- a/flink-connector-aws/flink-connector-dynamodb/pom.xml
+++ b/flink-connector-aws/flink-connector-dynamodb/pom.xml
@@ -154,7 +154,6 @@ under the License.
         <dependency>
             <groupId>org.awaitility</groupId>
             <artifactId>awaitility</artifactId>
-            <version>4.3.0</version>
             <scope>test</scope>
         </dependency>
 
diff --git a/pom.xml b/pom.xml
index d52f695e..5f7c5564 100644
--- a/pom.xml
+++ b/pom.xml
@@ -412,6 +412,12 @@ under the License.
                 <artifactId>annotations</artifactId>
                 <version>17.0.0</version>
             </dependency>
+            <dependency>
+                <groupId>org.awaitility</groupId>
+                <artifactId>awaitility</artifactId>
+                <scope>test</scope>
+                <version>4.3.0</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 


Reviews:
user: login: gguptp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:50:20Z
user: login: gguptp
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:55:55Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T10:36:31Z
user: login: gguptp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T12:04:25Z
user: login: darenwkt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:42:27Z
user: login: gguptp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:44:03Z
user: login: darenwkt
body: Thanks Keith, added comment
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:47:44Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T11:43:21Z
user: login: darenwkt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:03:11Z
user: login: darenwkt
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T13:03:38Z
user: login: Mr-Wang119
state: APPROVED
author_association: NONE
submitted_at: 2025-04-09T18:52:05Z
user: login: hlteoh37
body: Thanks for the contribution @leekeiabstraction ! It was nice to read. Added some comments.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:35:23Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:14:39Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:14:55Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:15:22Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:15:44Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:16:31Z
user: login: hlteoh37
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T11:33:31Z
user: login: leekeiabstraction
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:02:30Z
user: login: hlteoh37
body: LGTM. Thanks @leekeiabstraction 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:15:30Z

Review comments:
diff_hunk: @@ -40,6 +47,14 @@ public class PollingKinesisShardSplitReader extends KinesisShardSplitReaderBase
     private final Configuration configuration;
     private final int maxRecordsToGet;
 
+    private final long getRecordsIntervalMillis;
+
+    private final long idleSourceGetRecordsIntervalMillis;
+
+    private long scheduledGetRecordTimeMillis = 0;
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: gguptp
body: hmm, given `PollingKinesisShardSplitReader` is a child class of `KinesisShardSplitReaderBase` doesnt this variable get shared between all the shards rather than just one particular shard?

Won't the behaviour become like this:

Shard 1 - empty poll (scheduledGetRecordTimeMillis = T+250 millis)
Shard 2 - empty poll (scheduledGetRecordTimeMillis = T1+250 millis)
...
and so on
now theres a chance that shard 1 comes up later and although it should have been called with getrecords but since n shards came and the new scheduledGetRecordTimeMillis becomes much more than the actual shceduledGetRecordTimeMillis for shard 1 and this might lead to starvation (no getrecords calls at all?) of all the shards?

We should probably write a UT for this case as well i believe
created_at: 2025-04-04T07:53:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +47,14 @@ public class PollingKinesisShardSplitReader extends KinesisShardSplitReaderBase
     private final Configuration configuration;
     private final int maxRecordsToGet;
 
+    private final long getRecordsIntervalMillis;
+
+    private final long idleSourceGetRecordsIntervalMillis;
+
+    private long scheduledGetRecordTimeMillis = 0;
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: leekeiabstraction
body: Well spotted, I had the wrong assumption that we use a thread per split. Addressed in latest commit
created_at: 2025-04-04T10:36:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,21 +64,65 @@ public PollingKinesisShardSplitReader(
         this.kinesis = kinesisProxy;
         this.configuration = configuration;
         this.maxRecordsToGet = configuration.get(KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX);
+        this.getRecordsIntervalMillis = configuration.get(SHARD_GET_RECORDS_INTERVAL).toMillis();
+        this.idleSourceGetRecordsIntervalMillis =
+                configuration.get(SHARD_GET_RECORDS_IDLE_SOURCE_INTERVAL).toMillis();
     }
 
     @Override
-    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) throws IOException {
+        if (skipUntilScheduledGetRecordTime(splitState)) {
+            return null;
+        }
+
         GetRecordsResponse getRecordsResponse =
                 kinesis.getRecords(
                         splitState.getStreamArn(),
                         splitState.getShardId(),
                         splitState.getNextStartingPosition(),
                         this.maxRecordsToGet);
+
+        scheduleNextGetRecord(splitState, getRecordsResponse);
+
         boolean isCompleted = getRecordsResponse.nextShardIterator() == null;
         return new RecordBatch(
                 getRecordsResponse.records(), getRecordsResponse.millisBehindLatest(), isCompleted);
     }
 
+    private boolean skipUntilScheduledGetRecordTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledGetRecordTimes.containsKey(splitState)
+                && scheduledGetRecordTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                Thread.sleep(1);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: darenwkt
body: Q: Why do we add a 1ms sleep here?
created_at: 2025-04-07T10:42:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,21 +64,65 @@ public PollingKinesisShardSplitReader(
         this.kinesis = kinesisProxy;
         this.configuration = configuration;
         this.maxRecordsToGet = configuration.get(KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX);
+        this.getRecordsIntervalMillis = configuration.get(SHARD_GET_RECORDS_INTERVAL).toMillis();
+        this.idleSourceGetRecordsIntervalMillis =
+                configuration.get(SHARD_GET_RECORDS_IDLE_SOURCE_INTERVAL).toMillis();
     }
 
     @Override
-    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) throws IOException {
+        if (skipUntilScheduledGetRecordTime(splitState)) {
+            return null;
+        }
+
         GetRecordsResponse getRecordsResponse =
                 kinesis.getRecords(
                         splitState.getStreamArn(),
                         splitState.getShardId(),
                         splitState.getNextStartingPosition(),
                         this.maxRecordsToGet);
+
+        scheduleNextGetRecord(splitState, getRecordsResponse);
+
         boolean isCompleted = getRecordsResponse.nextShardIterator() == null;
         return new RecordBatch(
                 getRecordsResponse.records(), getRecordsResponse.millisBehindLatest(), isCompleted);
     }
 
+    private boolean skipUntilScheduledGetRecordTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledGetRecordTimes.containsKey(splitState)
+                && scheduledGetRecordTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                Thread.sleep(1);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: gguptp
body: if you dont add this, flink calls fetch function infinitely causing 100% CPU
created_at: 2025-04-07T10:44:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,21 +64,65 @@ public PollingKinesisShardSplitReader(
         this.kinesis = kinesisProxy;
         this.configuration = configuration;
         this.maxRecordsToGet = configuration.get(KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX);
+        this.getRecordsIntervalMillis = configuration.get(SHARD_GET_RECORDS_INTERVAL).toMillis();
+        this.idleSourceGetRecordsIntervalMillis =
+                configuration.get(SHARD_GET_RECORDS_IDLE_SOURCE_INTERVAL).toMillis();
     }
 
     @Override
-    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) throws IOException {
+        if (skipUntilScheduledGetRecordTime(splitState)) {
+            return null;
+        }
+
         GetRecordsResponse getRecordsResponse =
                 kinesis.getRecords(
                         splitState.getStreamArn(),
                         splitState.getShardId(),
                         splitState.getNextStartingPosition(),
                         this.maxRecordsToGet);
+
+        scheduleNextGetRecord(splitState, getRecordsResponse);
+
         boolean isCompleted = getRecordsResponse.nextShardIterator() == null;
         return new RecordBatch(
                 getRecordsResponse.records(), getRecordsResponse.millisBehindLatest(), isCompleted);
     }
 
+    private boolean skipUntilScheduledGetRecordTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledGetRecordTimes.containsKey(splitState)
+                && scheduledGetRecordTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                Thread.sleep(1);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: leekeiabstraction
body: To elaborate on Abhi's point further. This is to prevent KinesisShardSplitReaderBase from immediately looping through fetch() for each split in the case where we have idle sources, causing very high CPU usage.

Adding a 1ms pause ensures lower CPU usage at the cost of at most 1ms wait time per split. There is an edge case where a source with large number of idle assigned split will not loop through all its assigned split in a timely manner adding latency to GetRecords from a minority of non-idle shards, but this would require upwards of 1000 assigned splits to incur a 1 second delay. Assigning upwards of 1000 active splits to a source subtask is an unlikely and impractical scenario (app should really have much higher parallelism).
created_at: 2025-04-07T11:43:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,21 +64,65 @@ public PollingKinesisShardSplitReader(
         this.kinesis = kinesisProxy;
         this.configuration = configuration;
         this.maxRecordsToGet = configuration.get(KinesisSourceConfigOptions.SHARD_GET_RECORDS_MAX);
+        this.getRecordsIntervalMillis = configuration.get(SHARD_GET_RECORDS_INTERVAL).toMillis();
+        this.idleSourceGetRecordsIntervalMillis =
+                configuration.get(SHARD_GET_RECORDS_IDLE_SOURCE_INTERVAL).toMillis();
     }
 
     @Override
-    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+    protected RecordBatch fetchRecords(KinesisShardSplitState splitState) throws IOException {
+        if (skipUntilScheduledGetRecordTime(splitState)) {
+            return null;
+        }
+
         GetRecordsResponse getRecordsResponse =
                 kinesis.getRecords(
                         splitState.getStreamArn(),
                         splitState.getShardId(),
                         splitState.getNextStartingPosition(),
                         this.maxRecordsToGet);
+
+        scheduleNextGetRecord(splitState, getRecordsResponse);
+
         boolean isCompleted = getRecordsResponse.nextShardIterator() == null;
         return new RecordBatch(
                 getRecordsResponse.records(), getRecordsResponse.millisBehindLatest(), isCompleted);
     }
 
+    private boolean skipUntilScheduledGetRecordTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledGetRecordTimes.containsKey(splitState)
+                && scheduledGetRecordTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                Thread.sleep(1);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/polling/PollingKinesisShardSplitReader.java
user: login: darenwkt
body: Got it, thank you for the context
created_at: 2025-04-09T13:03:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledFetchTimes.containsKey(splitState)
+                && scheduledFetchTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException(
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: hlteoh37
body: Same as above, we should propagate the interrupted status up the thread.
created_at: 2025-04-14T17:22:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: hlteoh37
body: We should propagate the interrupted status up the thread.
```
Thread.interrupt()
```
created_at: 2025-04-14T17:23:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledFetchTimes.containsKey(splitState)
+                && scheduledFetchTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException(
+                        "Sleep was interrupted while skipping until scheduled fetch record time",
+                        e);
+            }
+        }
+
+        return false;
+    }
+
+    private void scheduleNextFetchTime(KinesisShardSplitState splitState, RecordBatch recordBatch) {
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: hlteoh37
body: Let's add a javadoc to explain how we expect "scheduledFetchTimes" to be used.

1/ If split was not active, we will schedule fetch time. We will wait until next fetch time before polling.
2/ If split was active, we will NOT schedule a fetch time. We should immediately poll.

(Num. 2 is not intuitive)
created_at: 2025-04-14T17:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -395,6 +399,79 @@ void testMaxRecordsToGetParameterPassed() throws IOException {
         assertThat(records.size()).isEqualTo(maxRecordsToGet);
     }
 
+    @ValueSource(longs = {0L, 250L, 1000L})
+    @ParameterizedTest
+    public void testGetRecordsIntervalForIdleSource(long interval) {
+        Configuration configuration = newConfigurationForTest();
+        configuration.set(READER_EMPTY_RECORDS_FETCH_INTERVAL, Duration.ofMillis(interval));
+
+        splitReader =
+                new PollingKinesisShardSplitReader(
+                        testStreamProxy, shardMetricGroupMap, configuration);
+
+        testStreamProxy.addShards(TEST_SHARD_ID);
+        splitReader.handleSplitsChanges(
+                new SplitsAddition<>(Collections.singletonList(getTestSplit(TEST_SHARD_ID))));
+
+        await().atLeast(interval, TimeUnit.MILLISECONDS)
+                .atMost(interval + 250, TimeUnit.MILLISECONDS)
+                .pollInSameThread()
+                .pollInterval(Duration.ofMillis(1))
+                .untilAsserted(
+                        () -> {
+                            assertThat(splitReader.fetch().nextRecordFromSplit()).isNull();
+                            assertThat(testStreamProxy.getTotalGetRecordsCall()).isEqualTo(2);
+                        });
+    }
+
+    @ValueSource(ints = {1, 8, 64})
+    @ParameterizedTest
+    public void testGetRecordsIntervalForMultipleIdleSource(int shardCount) {
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java
user: login: hlteoh37
body: easy to read :) 
created_at: 2025-04-14T17:32:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,32 +32,48 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import software.amazon.awssdk.services.kinesis.model.Record;
 
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.CONSUMER_ARN;
+import static org.apache.flink.connector.kinesis.source.util.TestUtil.generateShardId;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.getTestSplit;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
 import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
user: login: hlteoh37
body: We shouldn't be using Mockito - Flink best practices is to implement test classes to mimic interface behavior
created_at: 2025-04-14T17:33:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,32 +32,48 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import software.amazon.awssdk.services.kinesis.model.Record;
 
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.CONSUMER_ARN;
+import static org.apache.flink.connector.kinesis.source.util.TestUtil.generateShardId;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.getTestSplit;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
 import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
user: login: hlteoh37
body: Why do we need to test the await behaviour here? Since we are testing the behaviour of the `KinesisShardSplitReaderBase`, can we instead 

1/ Implement a no-op test implementation of the base, and test that? That way we scope it directly to the abstract class being tested.
created_at: 2025-04-14T17:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -395,6 +399,79 @@ void testMaxRecordsToGetParameterPassed() throws IOException {
         assertThat(records.size()).isEqualTo(maxRecordsToGet);
     }
 
+    @ValueSource(longs = {0L, 250L, 1000L})
+    @ParameterizedTest
+    public void testGetRecordsIntervalForIdleSource(long interval) {
+        Configuration configuration = newConfigurationForTest();
+        configuration.set(READER_EMPTY_RECORDS_FETCH_INTERVAL, Duration.ofMillis(interval));
+
+        splitReader =
+                new PollingKinesisShardSplitReader(
+                        testStreamProxy, shardMetricGroupMap, configuration);
+
+        testStreamProxy.addShards(TEST_SHARD_ID);
+        splitReader.handleSplitsChanges(
+                new SplitsAddition<>(Collections.singletonList(getTestSplit(TEST_SHARD_ID))));
+
+        await().atLeast(interval, TimeUnit.MILLISECONDS)
+                .atMost(interval + 250, TimeUnit.MILLISECONDS)
+                .pollInSameThread()
+                .pollInterval(Duration.ofMillis(1))
+                .untilAsserted(
+                        () -> {
+                            assertThat(splitReader.fetch().nextRecordFromSplit()).isNull();
+                            assertThat(testStreamProxy.getTotalGetRecordsCall()).isEqualTo(2);
+                        });
+    }
+
+    @ValueSource(ints = {1, 8, 64})
+    @ParameterizedTest
+    public void testGetRecordsIntervalForMultipleIdleSource(int shardCount) {
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/PollingKinesisShardSplitReaderTest.java
user: login: leekeiabstraction
body: Thank you!
created_at: 2025-04-15T09:14:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledFetchTimes.containsKey(splitState)
+                && scheduledFetchTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException(
+                        "Sleep was interrupted while skipping until scheduled fetch record time",
+                        e);
+            }
+        }
+
+        return false;
+    }
+
+    private void scheduleNextFetchTime(KinesisShardSplitState splitState, RecordBatch recordBatch) {
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: leekeiabstraction
body: Good callout. Added javadoc in latest commits
created_at: 2025-04-15T09:14:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: leekeiabstraction
body: Nice catch! Added Thread.currentThread().interrupt() in latest commits.
created_at: 2025-04-15T09:15:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +144,52 @@ public RecordsWithSplitIds<Record> fetch() throws IOException {
                 recordBatch.isCompleted());
     }
 
+    private boolean skipWhenNoAssignedSplit(KinesisShardSplitState splitState) throws IOException {
+        if (splitState == null) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException("Sleep was interrupted while skipping no assigned split", e);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState splitState)
+            throws IOException {
+        if (scheduledFetchTimes.containsKey(splitState)
+                && scheduledFetchTimes.get(splitState) > System.currentTimeMillis()) {
+            try {
+                // Small sleep to prevent busy polling
+                Thread.sleep(1);
+                return true;
+            } catch (InterruptedException e) {
+                throw new IOException(
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/KinesisShardSplitReaderBase.java
user: login: leekeiabstraction
body: Nice catch! Added Thread.currentThread().interrupt() in latest commits.
created_at: 2025-04-15T09:15:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,32 +32,48 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import software.amazon.awssdk.services.kinesis.model.Record;
 
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.EFO_CONSUMER_SUBSCRIPTION_TIMEOUT;
+import static org.apache.flink.connector.kinesis.source.config.KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.CONSUMER_ARN;
+import static org.apache.flink.connector.kinesis.source.util.TestUtil.generateShardId;
 import static org.apache.flink.connector.kinesis.source.util.TestUtil.getTestSplit;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
 import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/test/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReaderTest.java
user: login: leekeiabstraction
body: Really good suggestion! Implemented no-op ReaderBase and testing ReaderBase instead of polling/EFO.
created_at: 2025-04-15T09:16:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +92,11 @@ public void handleSplitsChanges(SplitsChange<KinesisShardSplit> splitsChanges) {
         }
     }
 
+    @VisibleForTesting
+    Map<String, FanOutKinesisShardSubscription> getSplitSubscriptions() {
+        return splitSubscriptions;
+    }
+
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java
user: login: hlteoh37
body: This shouldn't be needed anymore! Let's remove it
created_at: 2025-04-15T11:33:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +92,11 @@ public void handleSplitsChanges(SplitsChange<KinesisShardSplit> splitsChanges) {
         }
     }
 
+    @VisibleForTesting
+    Map<String, FanOutKinesisShardSubscription> getSplitSubscriptions() {
+        return splitSubscriptions;
+    }
+
path: flink-connector-aws/flink-connector-aws-kinesis-streams/src/main/java/org/apache/flink/connector/kinesis/source/reader/fanout/FanOutKinesisShardSplitReader.java
user: login: leekeiabstraction
body: Well spotted. Removed in latest commits.
created_at: 2025-04-15T12:02:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: hlteoh37
created_at: 2025-04-14T17:36:39Z
body: EFO test is failing CI

```
Error:  Tests run: 11, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 4.615 s <<< FAILURE! - in org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest
Error:  org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest.testSubscriptionNextEventIntervalForMultipleIdleSource(int)[3]  Time elapsed: 0.603 s  <<< ERROR!
org.awaitility.core.ConditionTimeoutException: 
Assertion condition defined as a Lambda expression in org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest 
fanOutKinesisShardSubscription.nextEvent();
Wanted 2 times:
-> at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSubscription.nextEvent(FanOutKinesisShardSubscription.java:210)
But was 1 time:
-> at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReader.fetchRecords(FanOutKinesisShardSplitReader.java:67)
 within 500 milliseconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
	at org.awaitility.core.ConditionFactory.untilAsserted(ConditionFactory.java:790)
	at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest.testSubscriptionNextEventIntervalForMultipleIdleSource(FanOutKinesisShardSplitReaderTest.java:253)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestTemplateMethod(TimeoutExtension.java:92)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask$DefaultDynamicTestExecutor.execute(NodeTestTask.java:226)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask$DefaultDynamicTestExecutor.execute(NodeTestTask.java:204)
	at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:139)
	at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.lambda$execute$2(TestTemplateTestDescriptor.java:107)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:276)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:107)
	at org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor.execute(TestTemplateTestDescriptor.java:42)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.lambda$execute$1(JUnitPlatformProvider.java:199)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:193)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:154)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:120)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:428)
	at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
	at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:562)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:548)
Caused by: org.mockito.exceptions.verification.TooFewActualInvocations: 
fanOutKinesisShardSubscription.nextEvent();
Wanted 2 times:
-> at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSubscription.nextEvent(FanOutKinesisShardSubscription.java:210)
But was 1 time:
-> at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReader.fetchRecords(FanOutKinesisShardSplitReader.java:67)

	at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSubscription.nextEvent(FanOutKinesisShardSubscription.java:210)
	at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest.lambda$testSubscriptionNextEventIntervalForMultipleIdleSource$1(FanOutKinesisShardSplitReaderTest.java:266)
	at org.awaitility.core.AssertionCondition.lambda$new$0(AssertionCondition.java:53)
	at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:248)
	at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:235)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at org.awaitility.core.SameThreadExecutorService.execute(SameThreadExecutorService.java:79)
	at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:101)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
	at org.awaitility.core.ConditionFactory.untilAsserted(ConditionFactory.java:790)
	at org.apache.flink.connector.kinesis.source.reader.fanout.FanOutKinesisShardSplitReaderTest.testSubscriptionNextEventIntervalForMultipleIdleSource(FanOutKinesisShardSplitReaderTest.java:253)
	... 119 more


```
author_association: CONTRIBUTOR
user: login: leekeiabstraction
created_at: 2025-04-15T09:18:37Z
body: > EFO test is failing CI

Have addressed this by changing the test implementation such that we do not rely on assertion is fulfilled within tight time range defined by await().atMost() which is more susceptible to CPU performance differences and shard count. Now assertting on the diff between timestamps of fetchRecords call.
author_association: CONTRIBUTOR
user: login: hlteoh37
created_at: 2025-04-15T12:33:15Z
body: Ci test failing... Might be due to time constraints?

https://github.com/apache/flink-connector-aws/actions/runs/14469136706/job/40578458194?pr=195

```
Error:  Tests run: 13, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.479 s <<< FAILURE! - in org.apache.flink.connector.kinesis.source.reader.PollingKinesisShardSplitReaderTest
Error:  org.apache.flink.connector.kinesis.source.reader.PollingKinesisShardSplitReaderTest.testPauseOrResumeSplitsOnlyPauseReadsFromSpecifiedSplits  Time elapsed: 0.303 s  <<< ERROR!
org.awaitility.core.ConditionTimeoutException: 
Assertion condition defined as a lambda expression in org.apache.flink.connector.kinesis.source.reader.PollingKinesisShardSplitReaderTest 
expected: Record(ApproximateArrivalTimestamp=2025-04-15T12:30:53.767Z, Data=SdkBytes(bytes=0x73706c69742d322d646174612d31))
 but was: null within 250 milliseconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
	at org.awaitility.core.ConditionFactory.untilAsserted(ConditionFactory.java:790)
	at org.apache.flink.connector.kinesis.source.reader.PollingKinesisShardSplitReaderTest.testPauseOrResumeSplitsOnlyPauseReadsFromSpecifiedSplits(PollingKinesisShardSplitReaderTest.java:335)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.util.ArrayList.forEach(ArrayList.java:1259)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.lambda$execute$1(JUnitPlatformProvider.java:199)
	at java.util.Iterator.forEachRemaining(Iterator.java:116)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:193)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:154)
	at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:120)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:428)
	at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
	at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:562)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:548)
Caused by: org.opentest4j.AssertionFailedError: 
expected: Record(ApproximateArrivalTimestamp=2025-04-15T12:30:53.767Z, Data=SdkBytes(bytes=0x73706c69742d322d646174612d31))
 but was: null
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at org.apache.flink.connector.kinesis.source.reader.PollingKinesisShardSplitReaderTest.lambda$testPauseOrResumeSplitsOnlyPauseReadsFromSpecifiedSplits$4(PollingKinesisShardSplitReaderTest.java:338)
	at org.awaitility.core.AssertionCondition.lambda$new$0(AssertionCondition.java:53)
	at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:248)
	at org.awaitility.core.ConditionAwaiter$ConditionPoller.call(ConditionAwaiter.java:235)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:750)
```
author_association: CONTRIBUTOR
user: login: hlteoh37
created_at: 2025-04-15T14:51:23Z
body: All tests pass except Python - which is failing for different reason. Will fix in a separate PR
author_association: CONTRIBUTOR
user: login: boring-cyborg[bot]
created_at: 2025-04-15T14:51:40Z
body: Awesome work, congrats on your first merged pull request!

author_association: NONE
user: login: saholman
created_at: 2025-05-06T15:15:57Z
body: When will a new released version of flink-connector-aws be tagged that includes this fix?
author_association: NONE
user: login: saholman
created_at: 2025-05-06T20:07:42Z
body: @hlteoh37 or @darenwkt could you create a new tag that includes this fix? The latest version in Maven still has this bug.
author_association: NONE
user: login: darenwkt
created_at: 2025-05-07T08:23:46Z
body: @saholman Thanks for reaching out, I do not have access to create the tag, @hlteoh37 or @dannycranmer appreciate if you could help create:

- branch v5.1
- tag 5.1.0, 5.1.0-rc1

pointing at commit [9d6746b](https://github.com/apache/flink-connector-aws/commit/9d6746b2e45fb836d0e6f90ae85f33c493952e2b)

Thank you
author_association: CONTRIBUTOR
user: login: j4k4
created_at: 2025-05-08T13:59:57Z
body: Getting a release / tag with this fix would be super nice indeed.  
This issue kind of block us with upgrading to the new connector.
author_association: NONE
user: login: MarijaVujcic
created_at: 2025-06-17T07:36:17Z
body: Hi all, random but at my job we experienced this behaviour, can I ask when this release will happen or at least do you have any workaround?
author_association: NONE
user: login: leekeiabstraction
created_at: 2025-06-19T06:24:12Z
body: @MarijaVujcic We are currently working towards getting this released with help from Flink PMC/Committer.

In the mean time, as a workaround you may consider compiling snapshot version locally and depending on snapshot version to unblock your critical pipelines.
author_association: CONTRIBUTOR
user: login: mtillman-drivosity
created_at: 2025-09-12T10:55:00Z
body: I know this was asked already, but any chance this can be put into a release soon? I would like to avoid having to use a snapshot version as it can cause other issues. Thanks!
author_association: NONE
user: login: ys2843
created_at: 2025-10-09T16:49:08Z
body: Many of teams rely on Kinesis EFO for low latency ML feature generation, this fix is critical. also want to follow up when can we release 5.1.0 ?
author_association: NONE
user: login: sporadic-a
created_at: 2025-11-13T22:42:12Z
body: @leekeiabstraction sorry to bother you; while this fix was merged months ago, it looks like 5.1.0 is still unreleased. Are you able to "encourage" that -rc to get a release?
author_association: NONE
user: login: vfrico
created_at: 2025-11-14T13:49:20Z
body: This bug is also affecting to our team on the 5.0 version.
We've tried to compile the branch 5.1 manually, but seems to have some problems with aws authentication inside Apache Flink cluster:

```
[2025-11-14 13:18:15.274000] Unable to load credentials from SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.274000] Unable to load credentials from EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.275000] Unable to load credentials from WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set.
[2025-11-14 13:18:15.276000] Unable to load credentials from ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile 'default': ProfileFile(sections=[])
[2025-11-14 13:18:15.347000] Unable to load credentials from ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set.
[2025-11-14 13:18:15.539000] Unable to load credentials from EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.539000] Unable to load credentials from SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.540000] Unable to load credentials from WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set.
[2025-11-14 13:18:15.540000] Unable to load credentials from ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile 'default': ProfileFile(sections=[])
[2025-11-14 13:18:15.553000] Unable to load credentials from ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set.
[2025-11-14 13:18:15.657000] Unable to load credentials from ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile 'default': ProfileFile(sections=[])
[2025-11-14 13:18:15.657000] Unable to load credentials from WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set.
[2025-11-14 13:18:15.657000] Unable to load credentials from EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.657000] Unable to load credentials from SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.681000] Unable to load credentials from ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set.
[2025-11-14 13:18:15.743000] Unable to load credentials from SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.777000] Unable to load credentials from EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId).
[2025-11-14 13:18:15.778000] Unable to load credentials from WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set.
[2025-11-14 13:18:15.795000] Unable to load credentials from ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile 'default': ProfileFile(sections=[])
[2025-11-14 13:18:15.797000] Unable to load credentials from ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set.
```

The only solution for us is to wait for the new release.

Edit: Following these steps work for us to compile well.

**Steps to follow to build 5.1.0**
1) use java 11 to build the project -- For example, if using amazon-corretto-11.jdk, we can set the JAVA_HOME to

"export JAVA_HOME=/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home"

2) build the project and install flink-connector-aws 5.1

````
git clone [https://github.com/apache/flink-connector-aws.git ](https://github.com/apache/flink-connector-aws.git)

cd flink-connector-aws
git checkout tags/v5.1.0-rc1
mvn clean install -DskipTests -Dcheckstyle.skip=true -Dcheckstyle.excludes="**/target/**"

````
3) Update connector version to 5.1 in the main project and follow the regular step to build the application Jar

````
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-aws-kinesis-streams</artifactId>
            <version>5.1.0</version>
        </dependency>
````
author_association: NONE
user: login: alek-petr-scacap
created_at: 2025-11-26T09:04:55Z
body: Hello folks, this fix is very critical for us. Flink Sources using Enhanced Fan Out Consumer cause the whole job to have cpu pinned at 100%. Can you provide us with any information on when v5.1 with this fix will be released ?
Note: we tried to compile and use 5.1-rc and it solved the cpu usage problem.

author_association: NONE
user: login: AndyShoeshim
created_at: 2025-12-10T18:19:22Z
body: Hello folks, I would also like to know when the official release for this fix will be done, as the current available version  rules out using autoscaling for AWS Managed Flink
author_association: NONE