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

ferenc-csaky pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-connector-aws.git


The following commit(s) were added to refs/heads/main by this push:
     new 75a4142  [FLINK-39137] Add configurable interval between non-empty 
`getRecords` calls
75a4142 is described below

commit 75a414288000569bd16ef477e52658b0fdf4691a
Author: Andreas Bube <[email protected]>
AuthorDate: Mon Aug 31 17:47:00 2026 +0200

    [FLINK-39137] Add configurable interval between non-empty `getRecords` calls
    
    Generated-by: Claude Code 2.1.233 (Claude Opus 5)
---
 docs/content.zh/docs/connectors/table/kinesis.md   | 652 ++++++++++++++++-----
 docs/content/docs/connectors/table/kinesis.md      |  16 +
 .../source/config/KinesisSourceConfigOptions.java  |  11 +-
 .../source/reader/KinesisShardSplitReaderBase.java |  69 ++-
 .../reader/KinesisShardSplitReaderBaseTest.java    |  98 ++++
 5 files changed, 680 insertions(+), 166 deletions(-)

diff --git a/docs/content.zh/docs/connectors/table/kinesis.md 
b/docs/content.zh/docs/connectors/table/kinesis.md
index 43fa007..bccd790 100644
--- a/docs/content.zh/docs/connectors/table/kinesis.md
+++ b/docs/content.zh/docs/connectors/table/kinesis.md
@@ -39,10 +39,72 @@ Dependencies
 
 Kinesis 连接器目前并不包含在 Flink 的二进制发行版中,请查阅[这里]({{< ref 
"docs/dev/configuration/overview" >}})了解如何在集群运行中引用 Kinesis 连接器。
 
+### Versioning
+
+There are two available Table API and SQL distributions for the Kinesis 
connector.
+This has resulted from an ongoing migration from the deprecated 
`SourceFunction` and `SinkFunction` interfaces to the new `Source` and `Sink` 
interfaces.
+
+The Table API and SQL interfaces in Flink only allow one TableFactory for each 
connector identifier.
+Only one TableFactory with identifier `kinesis` can be included in your 
application's dependencies.
+
+The following table clarifies the underlying interface that is used depending 
on the distribution selected:
+
+<table class="table table-bordered">
+    <thead>
+    <tr>
+      <th class="text-left" style="width: 40%">Dependency</th>
+      <th class="text-center" style="width: 10%">Connector Version</th>
+      <th class="text-center" style="width: 25%">Source connector identifier 
(interface)</th>
+      <th class="text-center" style="width: 25%">Sink connector identifier 
(interface)</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td><code>flink-sql-connector-aws-kinesis-streams</code></td>
+      <td><code>5.x</code> or later</td>
+      <td><code>kinesis</code>(<code>Source</code>)</td>
+      <td><code>kinesis</code>(<code>Sink</code>)</td>
+    </tr>
+    <tr>
+      <td><code>flink-sql-connector-aws-kinesis-streams</code></td>
+      <td><code>4.x</code> or earlier</td>
+      <td>N/A (no source packaged)</td>
+      <td><code>kinesis</code>(<code>Sink</code>)</td>
+    </tr>
+    <tr>
+      <td><code>flink-sql-connector-kinesis</code></td>
+      <td><code>5.x</code> or later</td>
+      <td><code>kinesis</code>(<code>Source</code>), 
<code>kinesis-legacy</code>(<code>SourceFunction</code>)</td>
+      <td><code>kinesis</code>(<code>Sink</code>)</td>
+    </tr>
+    <tr>
+      <td><code>flink-sql-connector-kinesis</code></td>
+      <td><code>4.x</code> or earlier</td>
+      <td><code>kinesis</code>(<code>SourceFunction</code>)</td>
+      <td><code>kinesis</code>(<code>Sink</code>)</td>
+    </tr>
+    </tbody>
+</table>
+
+{{< hint warning >}}
+Only include one artifact, either `flink-sql-connector-aws-kinesis-streams` or 
`flink-sql-connector-kinesis`. Including both will result in clashing 
TableFactory names.
+{{< /hint >}}
+
+These docs are targeted for versions 5.x onwards. The main configuration 
section targets `kinesis` identifier.
+For legacy configuration, please see [Configuration 
(`kinesis-legacy`)](#connector-options-kinesis-legacy)
+
+### Migrating from v4.x to v5.x
+
+There is no state compatibility between Table API and SQL API between 4.x and 
5.x.
+This is due to the underlying implementation being changed.
+
+Consider starting the job with v5.x `kinesis` table with 
`source.init.position` of `AT_TIMESTAMP` slightly before the time when the job 
with v4.x `kinesis` table was stopped.
+Note that this may result in some re-processed some records.
+
 How to create a Kinesis data stream table
 -----------------------------------------
 
-Follow the instructions from the [Amazon KDS Developer 
Guide](https://docs.aws.amazon.com/streams/latest/dev/learning-kinesis-module-one-create-stream.html)
 to set up a Kinesis stream.
+Follow the instructions from the [Amazon KDS Developer 
Guide](https://docs.aws.amazon.com/streams/latest/dev/introduction.html) to set 
up a Kinesis stream.
 The following example shows how to create a table backed by a Kinesis data 
stream:
 
 ```sql
@@ -56,9 +118,9 @@ CREATE TABLE KinesisTable (
 PARTITIONED BY (user_id, item_id)
 WITH (
   'connector' = 'kinesis',
-  'stream' = 'user_behavior',
-  'aws.region' = 'us-east-2',
-  'scan.stream.initpos' = 'LATEST',
+  'stream.arn' = 
'arn:aws:kinesis:us-east-1:012345678901:stream/my-stream-name',
+  'aws.region' = 'us-east-1',
+  'source.init.position' = 'LATEST',
   'format' = 'csv'
 );
 ```
@@ -66,7 +128,12 @@ WITH (
 Available Metadata
 ------------------
 
-The following metadata can be exposed as read-only (`VIRTUAL`) columns in a 
table definition.
+{{< hint warning >}}
+The `kinesis` table Source has a known bug that means `VIRTUAL` columns are 
not supported.
+Please use `kinesis-legacy` until [the 
fix](https://issues.apache.org/jira/browse/FLINK-36671) is completed.
+{{< /hint >}}
+
+The following metadata can be exposed as read-only (`VIRTUAL`) columns in a 
table definition. This is only available in the `kinesis-legacy` connector only.
 
 <table class="table table-bordered">
     <thead>
@@ -110,7 +177,7 @@ CREATE TABLE KinesisTable (
 )
 PARTITIONED BY (user_id, item_id)
 WITH (
-  'connector' = 'kinesis',
+  'connector' = 'kinesis-legacy',
   'stream' = 'user_behavior',
   'aws.region' = 'us-east-2',
   'scan.stream.initpos' = 'LATEST',
@@ -122,6 +189,446 @@ WITH (
 Connector Options
 -----------------
 
+<table class="table table-bordered">
+    <thead>
+    <tr>
+        <th class="text-left" style="width: 25%">Option</th>
+        <th class="text-center" style="width: 8%">Required</th>
+        <th class="text-center" style="width: 8%">Forwarded</th>
+        <th class="text-center" style="width: 7%">Default</th>
+        <th class="text-center" style="width: 10%">Type</th>
+        <th class="text-center" style="width: 42%">Description</th>
+    </tr>
+    <tr>
+      <th colspan="6" class="text-left" style="width: 100%">Common Options</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td><h5>connector</h5></td>
+      <td>required</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>Specify what connector to use. For Kinesis use 
<code>'kinesis'</code> or <code>'kinesis-legacy'</code>. See <a 
href="#versioning">Versioning</a> for details.</td>
+    </tr>
+    <tr>
+      <td><h5>stream.arn</h5></td>
+      <td>required</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>Name of the Kinesis data stream backing this table.</td>
+    </tr>
+    <tr>
+      <td><h5>format</h5></td>
+      <td>required</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>The format used to deserialize and serialize Kinesis data stream 
records. See <a href="#data-type-mapping">Data Type Mapping</a> for 
details.</td>
+    </tr>
+    <tr>
+      <td><h5>aws.region</h5></td>
+      <td>required</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>The AWS region where the stream is defined.</td>
+    </tr>
+    <tr>
+      <td><h5>aws.endpoint</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>The AWS endpoint for Kinesis (derived from the AWS region setting if 
not set).</td>
+    </tr>
+    <tr>
+      <td><h5>aws.trust.all.certificates</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">false</td>
+      <td>Boolean</td>
+      <td>If true accepts all SSL certificates. This is not recommended for 
production environments, but should only be used for testing purposes.</td>
+    </tr>
+    </tbody>
+    <thead>
+    <tr>
+      <th colspan="6" class="text-left" style="width: 100%">Authentication 
Options</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td><h5>aws.credentials.provider</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">AUTO</td>
+      <td>String</td>
+      <td>A credentials provider to use when authenticating against the 
Kinesis endpoint. See <a href="#authentication">Authentication</a> for 
details.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.basic.accesskeyid</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The AWS access key ID to use when setting credentials provider 
type to BASIC.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.basic.secretkey</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The AWS secret key to use when setting credentials provider type 
to BASIC.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.profile.path</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>Optional configuration for profile path if credential provider 
type is set to be PROFILE.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.profile.name</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>Optional configuration for profile name if credential provider 
type is set to be PROFILE.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.role.arn</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The role ARN to use when credential provider type is set to 
ASSUME_ROLE or WEB_IDENTITY_TOKEN.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.role.sessionName</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The role session name to use when credential provider type is set 
to ASSUME_ROLE or WEB_IDENTITY_TOKEN.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.role.externalId</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The external ID to use when credential provider type is set to 
ASSUME_ROLE.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.role.stsEndpoint</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The AWS endpoint for STS (derived from the AWS region setting if 
not set) to use when credential provider type is set to ASSUME_ROLE.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.role.provider</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The credentials provider that provides credentials for assuming 
the role when credential provider type is set to ASSUME_ROLE. Roles can be 
nested, so this value can again be set to ASSUME_ROLE</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.webIdentityToken.file</h5></td>
+         <td>optional</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The absolute path to the web identity token file that should be 
used if provider type is set to WEB_IDENTITY_TOKEN.</td>
+    </tr>
+    <tr>
+         <td><h5>aws.credentials.custom.class</h5></td>
+         <td>required only if credential provider is set to CUSTOM</td>
+      <td>no</td>
+         <td style="word-wrap: break-word;">(none)</td>
+         <td>String</td>
+         <td>The full path (in Java package notation) to the user provided
+      class to use if credential provider type is set to be CUSTOM e.g. 
org.user_company.auth.CustomAwsCredentialsProvider.</td>
+    </tr>
+    </tbody>
+    <thead>
+    <tr>
+      <th colspan="6" class="text-left" style="width: 100%">Source Options</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td><h5>source.init.position</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">LATEST</td>
+      <td>String</td>
+      <td>Initial position to be used when reading from the table. See <a 
href="#start-reading-position">Start Reading Position</a> for details.</td>
+    </tr>
+    <tr>
+      <td><h5>source.init.timestamp</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>The initial timestamp to start reading Kinesis stream from (when 
<code>scan.stream.initpos</code> is AT_TIMESTAMP). See <a 
href="#start-reading-position">Start Reading Position</a> for details.</td>
+    </tr>
+    <tr>
+      <td><h5>source.init.timestamp.format</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">yyyy-MM-dd'T'HH:mm:ss.SSSXXX</td>
+      <td>String</td>
+      <td>The date format of initial timestamp to start reading Kinesis stream 
from (when <code>scan.stream.initpos</code> is AT_TIMESTAMP). See <a 
href="#start-reading-position">Start Reading Position</a> for details.</td>
+    </tr>
+    <tr>
+      <td><h5>source.shard.discovery.interval</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">10 s</td>
+      <td>Duration</td>
+      <td>The interval between each attempt to discover new shards.</td>
+    </tr>
+    <tr>
+      <td><h5>source.reader.type</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">POLLING</td>
+      <td>String</td>
+      <td>The <code>ReaderType</code> to use for sources 
(<code>POLLING|EFO</code>).</td>
+    </tr>
+    <tr>
+      <td><h5>source.reader.empty-records-fetch-interval</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">250 ms</td>
+      <td>Duration</td>
+      <td>The interval to wait on a shard after a fetch returned no records 
before fetching from it again.</td>
+    </tr>
+    <tr>
+      <td><h5>source.reader.nonempty-records-fetch-interval</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">0 ms</td>
+      <td>Duration</td>
+      <td>The interval to wait on a shard after a fetch returned records 
before fetching from it again. Defaults to no interval.</td>
+    </tr>
+    <tr>
+      <td><h5>source.shard.get-records.max-record-count</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">10000</td>
+      <td>Integer</td>
+      <td>Only applicable to POLLING <code>ReaderType</code>. The maximum 
number of records to try to get each time we fetch records from a AWS Kinesis 
shard.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.consumer.name</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td>String</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. The name of the EFO 
consumer to register with KDS.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.lifecycle</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">JOB_MANAGED</td>
+      <td>String</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Determine if the EFO 
consumer is managed by the Flink job <code>JOB_MANAGED|SELF_MANAGED</code>.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.subscription.timeout</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">60 s</td>
+      <td>Duration</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Timeout for EFO 
Consumer subscription.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.deregister.timeout</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">10 s</td>
+      <td>Duration</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Timeout for consumer 
deregistration. When timeout is reached, code will continue as per normal.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.describe.retry-strategy.attempts.max</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">100</td>
+      <td>Integer</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Maximum number of 
attempts for the exponential backoff retry strategy when calling 
<code>DescribeStreamConsumer</code>.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.describe.retry-strategy.delay.min</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">2 s</td>
+      <td>Duration</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Base delay for the 
exponential backoff retry strategy when calling 
<code>DescribeStreamConsumer</code>.</td>
+    </tr>
+    <tr>
+      <td><h5>source.efo.describe.retry-strategy.delay.max</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">60 s</td>
+      <td>Duration</td>
+      <td>Only applicable to EFO <code>ReaderType</code>. Max delay for the 
exponential backoff retry strategy when calling 
<code>DescribeStreamConsumer</code>.</td>
+    </tr>
+    </tbody>
+    <thead>
+    <tr>
+      <th colspan="6" class="text-left" style="width: 100%">Sink Options</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td><h5>sink.partitioner</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">random or row-based</td>
+      <td>String</td>
+      <td>Optional output partitioning from Flink's partitions into Kinesis 
shards. See <a href="#sink-partitioning">Sink Partitioning</a> for details.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.partitioner-field-delimiter</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">|</td>
+      <td>String</td>
+      <td>Optional field delimiter for a fields-based partitioner derived from 
a PARTITION BY clause. See <a href="#sink-partitioning">Sink Partitioning</a> 
for details.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.producer.*</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">(none)</td>
+      <td></td>
+      <td>
+        Deprecated options previously used by the legacy connector.
+        Options with equivalant alternatives in 
<code>KinesisStreamsSink</code> are matched 
+        to their respective properties. Unsupported options are logged out to 
user as warnings.
+      </td>
+    </tr>
+    <tr>
+      <td><h5>sink.http-client.max-concurrency</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">10000</td>
+      <td>Integer</td>
+      <td>
+      Maximum number of allowed concurrent requests by 
<code>KinesisAsyncClient</code>.
+      </td>
+    </tr>
+    <tr>
+      <td><h5>sink.http-client.read-timeout</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">360000</td>
+      <td>Integer</td>
+      <td>
+        Maximum amount of time in ms for requests to be sent by 
<code>KinesisAsyncClient</code>.
+      </td>
+    </tr>
+    <tr>
+      <td><h5>sink.http-client.protocol.version</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">HTTP2</td>
+      <td>String</td>
+      <td>Http version used by Kinesis Client.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.batch.max-size</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">500</td>
+      <td>Integer</td>
+      <td>Maximum batch size of elements to be passed to 
<code>KinesisAsyncClient</code> to be written downstream.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.requests.max-inflight</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">16</td>
+      <td>Integer</td>
+      <td>Request threshold for uncompleted requests by 
<code>KinesisAsyncClient</code>before blocking new write requests and applying 
backpressure.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.requests.max-buffered</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">10000</td>
+      <td>String</td>
+      <td>Request buffer threshold for buffered requests by 
<code>KinesisAsyncClient</code> before blocking new write requests and applying 
backpressure.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.flush-buffer.size</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">5242880</td>
+      <td>Long</td>
+      <td>Threshold value in bytes for writer buffer in 
<code>KinesisAsyncClient</code> before flushing.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.flush-buffer.timeout</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">5000</td>
+      <td>Long</td>
+      <td>Threshold time in milliseconds for an element to be in a buffer 
of<code>KinesisAsyncClient</code> before flushing.</td>
+    </tr>
+    <tr>
+      <td><h5>sink.fail-on-error</h5></td>
+      <td>optional</td>
+      <td>yes</td>
+      <td style="word-wrap: break-word;">false</td>
+      <td>Boolean</td>
+      <td>Flag used for retrying failed requests. If set any request failure 
will not be retried and will fail the job.</td>
+    </tr>
+    </tbody>
+</table>
+
+Features
+--------
+
+{{< hint info >}}
+Refer to the [Kinesis Datastream API]({{< ref 
"docs/connectors/datastream/kinesis" >}}) documentation for more detailed 
description of features.
+{{< /hint >}}
+
+### Sink Partitioning
+
+Kinesis data streams consist of one or more shards, and the `sink.partitioner` 
option allows you to control how records written into a multi-shard 
Kinesis-backed table will be partitioned between its shards.
+Valid values are:
+
+* `fixed`: Kinesis `PartitionKey` values derived from the Flink subtask index, 
so each Flink partition ends up in at most one Kinesis partition (assuming that 
no re-sharding takes place at runtime).
+* `random`: Kinesis `PartitionKey` values are assigned randomly. This is the 
default value for tables not defined with a `PARTITION BY` clause.
+* Custom `FixedKinesisPartitioner` subclass: e.g. 
`'org.mycompany.MyPartitioner'`.
+
+{{< hint info >}}
+Records written into tables defining a `PARTITION BY` clause will always be 
partitioned based on a concatenated projection of the `PARTITION BY` fields.
+In this case, the `sink.partitioner` field cannot be used to modify this 
behavior (attempting to do this results in a configuration error).
+You can, however, use the `sink.partitioner-field-delimiter` option to set the 
delimiter of field values in the concatenated 
[PartitionKey](https://docs.aws.amazon.com/kinesis/latest/APIReference/API_PutRecord.html#Streams-PutRecord-request-PartitionKey)
 string (an empty string is also a valid delimiter).
+{{< /hint >}}
+
+# Data Type Mapping
+
+Kinesis stores records as Base64-encoded binary data objects, so it doesn't 
have a notion of internal record structure.
+Instead, Kinesis records are deserialized and serialized by formats, e.g. 
'avro', 'csv', or 'json'.
+To determine the data type of the messages in your Kinesis-backed tables, pick 
a suitable Flink format with the `format` keyword.
+Please refer to the [Formats]({{< ref "docs/connectors/table/formats/overview" 
>}}) pages for more details.
+
+Connector Options (`kinesis-legacy`)
+-----------------
+
 <table class="table table-bordered">
     <thead>
     <tr>
@@ -326,7 +833,7 @@ Connector Options
       <td>no</td>
       <td style="word-wrap: break-word;">POLLING</td>
       <td>String</td>
-      <td>The <code>RecordPublisher</code> type to use for sources. See <a 
href="#enhanced-fan-out">Enhanced Fan-Out</a> for details.</td>
+      <td>The <code>RecordPublisher</code> type to use for sources.</td>
     </tr>
     <tr>
       <td><h5>scan.stream.efo.consumername</h5></td>
@@ -334,7 +841,7 @@ Connector Options
       <td>no</td>
       <td style="word-wrap: break-word;">(none)</td>
       <td>String</td>
-      <td>The name of the EFO consumer to register with KDS. See <a 
href="#enhanced-fan-out">Enhanced Fan-Out</a> for details.</td>
+      <td>The name of the EFO consumer to register with KDS.</td>
     </tr>
     <tr>
       <td><h5>scan.stream.efo.registration</h5></td>
@@ -342,7 +849,7 @@ Connector Options
       <td>no</td>
       <td style="word-wrap: break-word;">LAZY</td>
       <td>String</td>
-      <td>Determine how and when consumer de-/registration is performed 
(LAZY|EAGER|NONE). See <a href="#enhanced-fan-out">Enhanced Fan-Out</a> for 
details.</td>
+      <td>Determine how and when consumer de-/registration is performed 
(LAZY|EAGER|NONE).</td>
     </tr>
     <tr>
       <td><h5>scan.stream.efo.consumerarn</h5></td>
@@ -350,7 +857,7 @@ Connector Options
       <td>no</td>
       <td style="word-wrap: break-word;">(none)</td>
       <td>String</td>
-      <td>The prefix of consumer ARN for a given stream. See <a 
href="#enhanced-fan-out">Enhanced Fan-Out</a> for details.</td>
+      <td>The prefix of consumer ARN for a given stream.</td>
     </tr>
     <tr>
       <td><h5>scan.stream.efo.http-client.max-concurrency</h5></td>
@@ -358,7 +865,7 @@ Connector Options
       <td>no</td>
       <td style="word-wrap: break-word;">10000</td>
       <td>Integer</td>
-      <td>Maximum number of allowed concurrent requests for the EFO client. 
See <a href="#enhanced-fan-out">Enhanced Fan-Out</a> for details.</td>
+      <td>Maximum number of allowed concurrent requests for the EFO 
client.</td>
     </tr>
     <tr>
       <td><h5>scan.shard-assigner</h5></td>
@@ -680,7 +1187,7 @@ Connector Options
       <td>Long</td>
       <td>The interval (in milliseconds) after which to consider a shard idle 
for purposes of watermark generation. A positive value will allow the watermark 
to progress even when some shards don't receive new records.</td>
     </tr>
-    <tr>
+   <tr>
       <td><h5>shard.consumer.error.recoverable[0].exception</h5></td>
       <td>optional</td>
       <td>no</td>
@@ -826,125 +1333,4 @@ Connector Options
     </tbody>
 </table>
 
-Features
---------
-
-### Authorization
-
-Make sure to [create an appropriate IAM 
policy](https://docs.aws.amazon.com/streams/latest/dev/controlling-access.html) 
to allow reading from / writing to the Kinesis data streams.
-
-### Authentication
-
-Depending on your deployment you would choose a different Credentials Provider 
to allow access to Kinesis.
-By default, the `AUTO` Credentials Provider is used.
-If the access key ID and secret key are set in the deployment configuration, 
this results in using the `BASIC` provider.
-
-A specific 
[AWSCredentialsProvider](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html?com/amazonaws/auth/AWSCredentialsProvider.html)
 can be **optionally** set using the `aws.credentials.provider` setting.
-Supported values are:
-
-* `AUTO` - Use the default AWS Credentials Provider chain that searches for 
credentials in the following order: `ENV_VARS`, `SYS_PROPS`, 
`WEB_IDENTITY_TOKEN`, `PROFILE`, and EC2/ECS credentials provider.
-* `BASIC` - Use access key ID and secret key supplied as configuration.
-* `ENV_VAR` - Use `AWS_ACCESS_KEY_ID` & `AWS_SECRET_ACCESS_KEY` environment 
variables.
-* `SYS_PROP` - Use Java system properties `aws.accessKeyId` and 
`aws.secretKey`.
-* `PROFILE` - Use an AWS credentials profile to create the AWS credentials.
-* `ASSUME_ROLE` - Create AWS credentials by assuming a role. The credentials 
for assuming the role must be supplied.
-* `WEB_IDENTITY_TOKEN` - Create AWS credentials by assuming a role using Web 
Identity Token.
-* `CUSTOM` - Provide a custom class that implements the interface 
`AWSCredentialsProvider` and has a constructor 
`MyCustomClass(java.util.Properties config)`. All connector properties will be 
passed down to this custom
-credential provider class via the constructor.
-
-### Start Reading Position
-
-You can configure table sources to start reading a table-backing Kinesis data 
stream from a specific position through the `scan.stream.initpos` option.
-Available values are:
-
-* `LATEST`: read shards starting from the latest record.
-* `TRIM_HORIZON`: read shards starting from the earliest record possible (data 
may be trimmed by Kinesis depending on the current retention settings of the 
backing stream).
-* `AT_TIMESTAMP`: read shards starting from a specified timestamp. The 
timestamp value should be specified through the `scan.stream.initpos-timestamp` 
in one of the following formats:
-   * A non-negative double value representing the number of seconds that has 
elapsed since the Unix epoch (for example, `1459799926.480`).
-   * A value conforming to a user-defined `SimpleDateFormat` specified at 
`scan.stream.initpos-timestamp-format`.
-     If a user does not define a format, the default pattern will be 
`yyyy-MM-dd'T'HH:mm:ss.SSSXXX`.
-     For example, timestamp value is `2016-04-04` and user-defined format is 
`yyyy-MM-dd`, or timestamp value is `2016-04-04T19:58:46.480-00:00` and a 
user-defined format is not provided.
-
-### Sink Partitioning
-
-Kinesis data streams consist of one or more shards, and the `sink.partitioner` 
option allows you to control how records written into a multi-shard 
Kinesis-backed table will be partitioned between its shards.
-Valid values are:
-
-* `fixed`: Kinesis `PartitionKey` values derived from the Flink subtask index, 
so each Flink partition ends up in at most one Kinesis partition (assuming that 
no re-sharding takes place at runtime).
-* `random`: Kinesis `PartitionKey` values are assigned randomly. This is the 
default value for tables not defined with a `PARTITION BY` clause.
-* Custom `FixedKinesisPartitioner` subclass: e.g. 
`'org.mycompany.MyPartitioner'`.
-
-{{< hint info >}}
-Records written into tables defining a `PARTITION BY` clause will always be 
partitioned based on a concatenated projection of the `PARTITION BY` fields.
-In this case, the `sink.partitioner` field cannot be used to modify this 
behavior (attempting to do this results in a configuration error).
-You can, however, use the `sink.partitioner-field-delimiter` option to set the 
delimiter of field values in the concatenated 
[PartitionKey](https://docs.aws.amazon.com/kinesis/latest/APIReference/API_PutRecord.html#Streams-PutRecord-request-PartitionKey)
 string (an empty string is also a valid delimiter).
-{{< /hint >}}
-
-### Enhanced Fan-Out
-
-[Enhanced Fan-Out 
(EFO)](https://aws.amazon.com/blogs/aws/kds-enhanced-fanout/) increases the 
maximum number of concurrent consumers per Kinesis data stream.
-Without EFO, all concurrent Kinesis consumers share a single read quota per 
shard.
-Using EFO, each consumer gets a distinct dedicated read quota per shard, 
allowing read throughput to scale with the number of consumers.
-
-<span class="label label-info">Note</span> Using EFO will [incur additional 
cost](https://aws.amazon.com/kinesis/data-streams/pricing/).
-
-You can enable and configure EFO with the following properties:
-
-* `scan.stream.recordpublisher`: Determines whether to use `EFO` or `POLLING`.
-* `scan.stream.efo.consumername`: A name to identify the consumer when the 
above value is `EFO`.
-* `scan.stream.efo.registration`: Strategy for (de-)registration  of `EFO` 
consumers with the name given by the `scan.stream.efo.consumername` value. 
Valid strategies are:
-  * `LAZY` (default): Stream consumers are registered when the Flink job 
starts running.
-    If the stream consumer already exists, it will be reused.
-    This is the preferred strategy for the majority of applications.
-    However, jobs with parallelism greater than 1 will result in tasks 
competing to register and acquire the stream consumer ARN.
-    For jobs with very large parallelism this can result in an increased 
start-up time.
-    The `DescribeStreamConsumer` operation has a limit of 20 [transactions per 
second](https://docs.aws.amazon.com/kinesis/latest/APIReference/API_DescribeStreamConsumer.html),
-    this means application startup time will increase by roughly 
`parallelism/20 seconds`.
-  * `EAGER`: Stream consumers are registered in the `FlinkKinesisConsumer` 
constructor.
-    If the stream consumer already exists, it will be reused.
-    This will result in registration occurring when the job is constructed,
-    either on the Flink Job Manager or client environment submitting the job.
-    Using this strategy results in a single thread registering and retrieving 
the stream consumer ARN,
-    reducing startup time over `LAZY` (with large parallelism).
-    However, consider that the client environment will require access to the 
AWS services.
-  * `NONE`: Stream consumer registration is not performed by 
`FlinkKinesisConsumer`.
-    Registration must be performed externally using the [AWS CLI or 
SDK](https://aws.amazon.com/tools/)
-    to invoke 
[RegisterStreamConsumer](https://docs.aws.amazon.com/kinesis/latest/APIReference/API_RegisterStreamConsumer.html).
-    Stream consumer ARNs should be provided to the job via the consumer 
configuration.
-* `scan.stream.efo.consumerarn.<stream-name>`: ARNs identifying externally 
registered ARN-consumers (substitute `<stream-name>` with the name of your 
stream in the parameter name).
-   Use this if you choose to use `NONE` as a `scan.stream.efo.registration` 
strategy.
-
-<span class="label label-info">Note</span> For a given Kinesis data stream, 
each EFO consumer must have a unique name.
-However, consumer names do not have to be unique across data streams.
-Reusing a consumer name will result in existing subscriptions being terminated.
-
-<span class="label label-info">Note</span> With the `LAZY` strategy, stream 
consumers are de-registered when the job is shutdown gracefully.
-In the event that a job terminates without executing the shutdown hooks, 
stream consumers will remain active.
-In this situation the stream consumers will be gracefully reused when the 
application restarts.
-With the `NONE` and `EAGER` strategies, stream consumer de-registration is not 
performed by `FlinkKinesisConsumer`.
-
-# Data Type Mapping
-
-
-Kinesis stores records as Base64-encoded binary data objects, so it doesn't 
have a notion of internal record structure.
-Instead, Kinesis records are deserialized and serialized by formats, e.g. 
'avro', 'csv', or 'json'.
-To determine the data type of the messages in your Kinesis-backed tables, pick 
a suitable Flink format with the `format` keyword.
-Please refer to the [Formats]({{< ref "docs/connectors/table/formats/overview" 
>}}) pages for more details.
-
-# Updates in 1.15
-
-Kinesis table API connector sink data stream depends on 
<code>FlinkKinesisProducer</code> till 1.14, with the introduction of 
<code>KinesisStreamsSink</code> in 1.15 kinesis table API sink connector has 
been migrated to the new <code>KinesisStreamsSink</code>. Authentication 
options have been migrated identically while sink configuration options are now 
compatible with <code>KinesisStreamsSink</code>. 
-
-Options configuring <code>FlinkKinesisProducer</code> are now deprecated with 
fallback support for common configuration options with 
<code>KinesisStreamsSink</code>. 
-
-<code>KinesisStreamsSink</code> uses <code>KinesisAsyncClient</code> to send 
records to kinesis, 
-which doesn't support aggregation. In consequence, table options configuring 
aggregation in the deprecated <code>FlinkKinesisProducer</code> 
-are now deprecated and will be ignored, this includes 
<code>sink.producer.aggregation-enabled</code> and
-<code>sink.producer.aggregation-count</code>.
-
-<span class="label label-info">Note</span> Migrating applications with 
deprecated options will result in the incompatible deprecated options being 
ignored and warned to users.
-
-Kinesis table API source connector still depends on 
<code>FlinkKinesisConsumer</code> with no change in configuration options.
-
-
 {{< top >}}
diff --git a/docs/content/docs/connectors/table/kinesis.md 
b/docs/content/docs/connectors/table/kinesis.md
index e7381e3..365d36a 100644
--- a/docs/content/docs/connectors/table/kinesis.md
+++ b/docs/content/docs/connectors/table/kinesis.md
@@ -404,6 +404,22 @@ Connector Options
       <td>String</td>
       <td>The <code>ReaderType</code> to use for sources 
(<code>POLLING|EFO</code>).</td>
     </tr>
+    <tr>
+      <td><h5>source.reader.empty-records-fetch-interval</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">250 ms</td>
+      <td>Duration</td>
+      <td>The interval to wait on a shard after a fetch returned no records 
before fetching from it again.</td>
+    </tr>
+    <tr>
+      <td><h5>source.reader.nonempty-records-fetch-interval</h5></td>
+      <td>optional</td>
+      <td>no</td>
+      <td style="word-wrap: break-word;">0 ms</td>
+      <td>Duration</td>
+      <td>The interval to wait on a shard after a fetch returned records 
before fetching from it again. Defaults to no interval.</td>
+    </tr>
     <tr>
       <td><h5>source.shard.get-records.max-record-count</h5></td>
       <td>optional</td>
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 c310d7c..ecc9c7e 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
@@ -90,7 +90,16 @@ public class KinesisSourceConfigOptions {
                     .durationType()
                     .defaultValue(Duration.ofMillis(250))
                     .withDescription(
-                            "The interval in milliseconds between fetches with 
empty records");
+                            "The interval to wait on a shard after a fetch 
returned no records "
+                                    + "before fetching from it again.");
+
+    public static final ConfigOption<Duration> 
READER_NON_EMPTY_RECORDS_FETCH_INTERVAL =
+            ConfigOptions.key("source.reader.nonempty-records-fetch-interval")
+                    .durationType()
+                    .defaultValue(Duration.ZERO)
+                    .withDescription(
+                            "The interval to wait on a shard after a fetch 
returned records "
+                                    + "before fetching from it again. Defaults 
to no interval.");
 
     public static final ConfigOption<ConsumerLifecycle> EFO_CONSUMER_LIFECYCLE 
=
             ConfigOptions.key("source.efo.lifecycle")
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 e96a910..911624c 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
@@ -37,10 +37,10 @@ import 
software.amazon.awssdk.services.kinesis.model.ResourceNotFoundException;
 import javax.annotation.Nullable;
 
 import java.io.IOException;
+import java.time.Instant;
 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;
@@ -65,8 +65,9 @@ public abstract class KinesisShardSplitReaderBase
     private final Map<String, KinesisShardMetrics> shardMetricGroupMap;
 
     private final long emptyRecordsIntervalMillis;
+    private final long nonEmptyRecordsIntervalMillis;
 
-    private final Map<KinesisShardSplitState, Long> scheduledFetchTimes = new 
WeakHashMap<>();
+    private final Map<KinesisShardSplitState, Long> fetchDeferredUntil = new 
WeakHashMap<>();
 
     protected KinesisShardSplitReaderBase(
             Map<String, KinesisShardMetrics> shardMetricGroupMap, 
Configuration configuration) {
@@ -75,6 +76,10 @@ public abstract class KinesisShardSplitReaderBase
                 configuration
                         
.get(KinesisSourceConfigOptions.READER_EMPTY_RECORDS_FETCH_INTERVAL)
                         .toMillis();
+        this.nonEmptyRecordsIntervalMillis =
+                configuration
+                        
.get(KinesisSourceConfigOptions.READER_NON_EMPTY_RECORDS_FETCH_INTERVAL)
+                        .toMillis();
     }
 
     @Override
@@ -86,7 +91,7 @@ public abstract class KinesisShardSplitReaderBase
             return INCOMPLETE_SHARD_EMPTY_RECORDS;
         }
 
-        if (skipUntilScheduledFetchTime(splitState)) {
+        if (skipWhileFetchDeferred(splitState)) {
             assignedSplits.add(splitState);
             return INCOMPLETE_SHARD_EMPTY_RECORDS;
         }
@@ -100,7 +105,10 @@ public abstract class KinesisShardSplitReaderBase
         RecordBatch recordBatch;
         try {
             recordBatch = fetchRecords(splitState);
-            scheduleNextFetchTime(splitState, recordBatch);
+            long deferIntervalMillis = getNextFetchDeferInterval(recordBatch);
+            if (deferIntervalMillis > 0) {
+                deferNextFetchBy(splitState, deferIntervalMillis);
+            }
         } catch (ResourceNotFoundException e) {
             LOG.warn(
                     "Failed to fetch records from shard {}: shard no longer 
exists. Marking split as complete",
@@ -159,19 +167,16 @@ public abstract class KinesisShardSplitReaderBase
         return false;
     }
 
-    private boolean skipUntilScheduledFetchTime(KinesisShardSplitState 
splitState)
-            throws IOException {
-        if (scheduledFetchTimes.containsKey(splitState)
-                && scheduledFetchTimes.get(splitState) > 
System.currentTimeMillis()) {
+    private boolean skipWhileFetchDeferred(KinesisShardSplitState splitState) 
throws IOException {
+        if (fetchDeferredUntil.containsKey(splitState)
+                && fetchDeferredUntil.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);
+                throw new IOException("Sleep was interrupted while skipping a 
deferred fetch", e);
             }
         }
 
@@ -179,28 +184,28 @@ public abstract class KinesisShardSplitReaderBase
     }
 
     /**
-     * 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()
+     * Returns how long the next fetch on the split should be deferred, based 
on whether the given
+     * batch returned records. Zero, the default for a non-empty batch, means 
the next fetch is not
+     * deferred and is performed at the first opportunity.
      */
-    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());
-            }
+    private long getNextFetchDeferInterval(RecordBatch recordBatch) {
+        boolean fetchWasEmpty = recordBatch == null || 
recordBatch.getRecords().isEmpty();
+        return fetchWasEmpty ? emptyRecordsIntervalMillis : 
nonEmptyRecordsIntervalMillis;
+    }
+
+    /**
+     * Defers the next fetch on the split until the given interval has 
elapsed. Until then, the
+     * fetcher thread will skip fetching (and have a small sleep) for the 
split.
+     */
+    private void deferNextFetchBy(KinesisShardSplitState splitState, long 
deferIntervalMillis) {
+        long deferredUntilMillis = System.currentTimeMillis() + 
deferIntervalMillis;
+        this.fetchDeferredUntil.put(splitState, deferredUntilMillis);
+        if (LOG.isDebugEnabled()) {
+            LOG.debug(
+                    "Deferring next fetch on split {} by {}ms until {}",
+                    splitState.getSplitId(),
+                    deferIntervalMillis,
+                    Instant.ofEpochMilli(deferredUntilMillis));
         }
     }
 
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
index 72dbb1e..c0e0b73 100644
--- 
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
@@ -25,9 +25,12 @@ 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.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import software.amazon.awssdk.services.kinesis.model.Record;
 
 import java.time.Duration;
 import java.util.ArrayList;
@@ -41,6 +44,7 @@ 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.READER_NON_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;
@@ -49,6 +53,12 @@ import static org.awaitility.Awaitility.await;
 
 class KinesisShardSplitReaderBaseTest {
 
+    /**
+     * Upper bound on how long ten undeferred fetches may take. Generous 
enough to absorb CI
+     * scheduling noise, while far below any interval a throttled reader would 
introduce.
+     */
+    private static final long UNTHROTTLED_FETCH_BUDGET_MILLIS = 250L;
+
     private static Configuration newConfigurationForTest() {
         return new Configuration().set(SHARD_GET_RECORDS_MAX, 50);
     }
@@ -103,6 +113,79 @@ class KinesisShardSplitReaderBaseTest {
                         });
     }
 
+    @ValueSource(longs = {250L, 1000L})
+    @ParameterizedTest
+    void testGetRecordsIntervalForNonEmptySource(long interval) {
+        Configuration configuration = newConfigurationForTest();
+        configuration.set(READER_NON_EMPTY_RECORDS_FETCH_INTERVAL, 
Duration.ofMillis(interval));
+
+        // Given a non-empty reader with a custom interval
+        List<KinesisShardSplit> shardSplits = createShardSplits(8);
+        Map<String, KinesisShardMetrics> metrics = 
getShardMetrics(shardSplits);
+        CountingReader reader =
+                buildReader(NonEmptyRecordReturningReader.class, 
configuration, metrics);
+
+        reader.handleSplitsChanges(new SplitsAddition<>(shardSplits));
+
+        // When 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 the interval between fetchRecord 
calls respects the
+                                // configured interval. Only the lower bound 
is asserted: an upper
+                                // bound would be flaky on loaded CI machines.
+                                assertThat(
+                                                fetchRecordsCallTimes.get(1)
+                                                        - 
fetchRecordsCallTimes.get(0))
+                                        .isGreaterThanOrEqualTo(interval);
+                            }
+
+                            
assertThat(reader.getFetchRecordsCallTimestamps().size()).isEqualTo(8);
+                        });
+    }
+
+    @Test
+    void testNoGetRecordsIntervalForNonEmptySourceByDefault() throws Exception 
{
+        // Given a non-empty reader without a configured non-empty records 
fetch interval
+        Configuration configuration = newConfigurationForTest();
+        
assertThat(configuration.getOptional(READER_NON_EMPTY_RECORDS_FETCH_INTERVAL)).isEmpty();
+
+        List<KinesisShardSplit> shardSplits = createShardSplits(1);
+        Map<String, KinesisShardMetrics> metrics = 
getShardMetrics(shardSplits);
+        CountingReader reader =
+                buildReader(NonEmptyRecordReturningReader.class, 
configuration, metrics);
+
+        reader.handleSplitsChanges(new SplitsAddition<>(shardSplits));
+
+        // When records are fetched repeatedly
+        for (int i = 0; i < 10; i++) {
+            reader.fetch();
+        }
+
+        // Then every fetch reaches fetchRecords() without being deferred, 
preserving the
+        // fetch-at-first-opportunity behaviour of an unconfigured reader
+        assertThat(reader.getFetchRecordsCallTimestamps().size()).isEqualTo(1);
+        List<Long> fetchRecordsCallTimes =
+                
reader.getFetchRecordsCallTimestamps().values().iterator().next();
+        assertThat(fetchRecordsCallTimes.size()).isEqualTo(10);
+
+        // Consecutive fetches are not spaced out by an interval. Ten 
undeferred fetches take
+        // microseconds, so any per-fetch throttling would blow this budget 
comfortably.
+        long elapsedMillis =
+                fetchRecordsCallTimes.get(fetchRecordsCallTimes.size() - 1)
+                        - fetchRecordsCallTimes.get(0);
+        assertThat(elapsedMillis).isLessThan(UNTHROTTLED_FETCH_BUDGET_MILLIS);
+    }
+
     private static Stream<Arguments> readerTypeAndShardCount() {
         return Stream.of(
                 Arguments.of(NullReturningReader.class, 1),
@@ -157,6 +240,8 @@ class KinesisShardSplitReaderBaseTest {
             return new NullReturningReader(metrics, configuration);
         } else if (readerClass == EmptyRecordReturningReader.class) {
             return new EmptyRecordReturningReader(metrics, configuration);
+        } else if (readerClass == NonEmptyRecordReturningReader.class) {
+            return new NonEmptyRecordReturningReader(metrics, configuration);
         }
 
         throw new RuntimeException(
@@ -224,6 +309,19 @@ class KinesisShardSplitReaderBaseTest {
         }
     }
 
+    static class NonEmptyRecordReturningReader extends CountingReader {
+        public NonEmptyRecordReturningReader(
+                Map<String, KinesisShardMetrics> shardMetricGroupMap, 
Configuration configuration) {
+            super(shardMetricGroupMap, configuration);
+        }
+
+        @Override
+        protected RecordBatch fetchRecords(KinesisShardSplitState splitState) {
+            super.fetchRecords(splitState);
+            return new 
RecordBatch(Collections.singletonList(Record.builder().build()), 0L, false);
+        }
+    }
+
     private static List<KinesisShardSplit> createShardSplits(int shardCount) {
         return IntStream.range(0, shardCount)
                 .mapToObj(shardId -> getTestSplit(generateShardId(shardId)))

Reply via email to