This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 48a28da60ef1 CAMEL-24197: camel-aws2-kinesis - Fix fixed-shardId
DescribeStream on every poll and batch partition key (#24878)
48a28da60ef1 is described below
commit 48a28da60ef1ad6cdfee4a80bdb3eb93a4ba5a26
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 18 07:28:48 2026 +0200
CAMEL-24197: camel-aws2-kinesis - Fix fixed-shardId DescribeStream on every
poll and batch partition key (#24878)
CAMEL-24197: camel-aws2-kinesis - Fix fixed-shardId DescribeStream on every
poll and batch partition key
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/components/aws2-kinesis.json | 5 +-
.../camel/component/aws2/kinesis/aws2-kinesis.json | 5 +-
.../component/aws2/kinesis/Kinesis2Constants.java | 8 +
.../component/aws2/kinesis/Kinesis2Consumer.java | 108 ++++++--------
.../component/aws2/kinesis/Kinesis2Producer.java | 26 +++-
.../aws2/kinesis/Kinesis2ProducerTest.java | 166 +++++++++++++++++++++
.../KinesisConsumerClosedShardWithSilentTest.java | 17 +--
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 20 +++
.../dsl/Kinesis2EndpointBuilderFactory.java | 16 ++
9 files changed, 290 insertions(+), 81 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws2-kinesis.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws2-kinesis.json
index d804f6e48420..a6ebc21581e0 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws2-kinesis.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/aws2-kinesis.json
@@ -68,8 +68,9 @@
"CamelMessageTimestamp": { "index": 3, "kind": "header", "displayName":
"", "group": "common", "label": "", "required": false, "javaType": "long",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The timestamp of the message", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#MESSAGE_TIMESTAMP" },
"CamelKinesisDbResumeAction": { "index": 4, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The resume action to execute when
resuming.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RESUME_ACTION" },
"CamelAwsKinesisShardId": { "index": 5, "kind": "header", "displayName":
"", "group": "common", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The shard ID of the shard where the data record was
placed.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#SHARD_ID" },
- "CamelAwsKinesisFailedRecordCount": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of records that failed in a
batch put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#FAILED_RECORD_COUNT"
},
- "CamelAwsKinesisRecordCount": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The total number of records in a batch
put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RECORD_COUNT" }
+ "CamelAwsKinesisPartitionKeys": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "List<String>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "A list of partition keys
for batch operations. When set, each record in the batch is assigned the
partition key at the corresponding index, allowing records to be routed to
different shards. If not set, the single Camel [...]
+ "CamelAwsKinesisFailedRecordCount": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of records that failed in a
batch put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#FAILED_RECORD_COUNT"
},
+ "CamelAwsKinesisRecordCount": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The total number of records in a batch
put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RECORD_COUNT" }
},
"properties": {
"streamName": { "index": 0, "kind": "path", "displayName": "Stream Name",
"group": "common", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.aws2.kinesis.Kinesis2Configuration",
"configurationField": "configuration", "description": "Name of the stream" },
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/generated/resources/META-INF/org/apache/camel/component/aws2/kinesis/aws2-kinesis.json
b/components/camel-aws/camel-aws2-kinesis/src/generated/resources/META-INF/org/apache/camel/component/aws2/kinesis/aws2-kinesis.json
index d804f6e48420..a6ebc21581e0 100644
---
a/components/camel-aws/camel-aws2-kinesis/src/generated/resources/META-INF/org/apache/camel/component/aws2/kinesis/aws2-kinesis.json
+++
b/components/camel-aws/camel-aws2-kinesis/src/generated/resources/META-INF/org/apache/camel/component/aws2/kinesis/aws2-kinesis.json
@@ -68,8 +68,9 @@
"CamelMessageTimestamp": { "index": 3, "kind": "header", "displayName":
"", "group": "common", "label": "", "required": false, "javaType": "long",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The timestamp of the message", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#MESSAGE_TIMESTAMP" },
"CamelKinesisDbResumeAction": { "index": 4, "kind": "header",
"displayName": "", "group": "consumer", "label": "consumer", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The resume action to execute when
resuming.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RESUME_ACTION" },
"CamelAwsKinesisShardId": { "index": 5, "kind": "header", "displayName":
"", "group": "common", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The shard ID of the shard where the data record was
placed.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#SHARD_ID" },
- "CamelAwsKinesisFailedRecordCount": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of records that failed in a
batch put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#FAILED_RECORD_COUNT"
},
- "CamelAwsKinesisRecordCount": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The total number of records in a batch
put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RECORD_COUNT" }
+ "CamelAwsKinesisPartitionKeys": { "index": 6, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "List<String>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "A list of partition keys
for batch operations. When set, each record in the batch is assigned the
partition key at the corresponding index, allowing records to be routed to
different shards. If not set, the single Camel [...]
+ "CamelAwsKinesisFailedRecordCount": { "index": 7, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of records that failed in a
batch put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#FAILED_RECORD_COUNT"
},
+ "CamelAwsKinesisRecordCount": { "index": 8, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The total number of records in a batch
put operation.", "constantName":
"org.apache.camel.component.aws2.kinesis.Kinesis2Constants#RECORD_COUNT" }
},
"properties": {
"streamName": { "index": 0, "kind": "path", "displayName": "Stream Name",
"group": "common", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.aws2.kinesis.Kinesis2Configuration",
"configurationField": "configuration", "description": "Name of the stream" },
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Constants.java
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Constants.java
index 5473cef5a58f..f8cbb3473238 100644
---
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Constants.java
+++
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Constants.java
@@ -41,6 +41,14 @@ public interface Kinesis2Constants {
@Metadata(description = "The shard ID of the shard where the data record
was placed.", javaType = "String")
String SHARD_ID = "CamelAwsKinesisShardId";
+ @Metadata(label = "producer",
+ description = "A list of partition keys for batch operations.
When set, each record in the batch "
+ + "is assigned the partition key at the
corresponding index, allowing records to be routed "
+ + "to different shards. If not set, the single
CamelAwsKinesisPartitionKey header is used "
+ + "for all records.",
+ javaType = "List<String>")
+ String PARTITION_KEYS = "CamelAwsKinesisPartitionKeys";
+
// Batch operation response metadata
@Metadata(label = "producer",
description = "The number of records that failed in a batch put
operation.", javaType = "Integer")
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Consumer.java
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Consumer.java
index c08263fd6b13..c8ce34fa8dd3 100644
---
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Consumer.java
+++
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Consumer.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.aws2.kinesis;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@@ -42,13 +43,12 @@ import org.apache.camel.util.CastUtils;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.services.kinesis.model.DescribeStreamRequest;
-import software.amazon.awssdk.services.kinesis.model.DescribeStreamResponse;
import software.amazon.awssdk.services.kinesis.model.GetRecordsRequest;
import software.amazon.awssdk.services.kinesis.model.GetRecordsResponse;
import software.amazon.awssdk.services.kinesis.model.GetShardIteratorRequest;
import software.amazon.awssdk.services.kinesis.model.GetShardIteratorResponse;
import software.amazon.awssdk.services.kinesis.model.ListShardsRequest;
+import software.amazon.awssdk.services.kinesis.model.ListShardsResponse;
import software.amazon.awssdk.services.kinesis.model.Record;
import software.amazon.awssdk.services.kinesis.model.Shard;
import software.amazon.awssdk.services.kinesis.model.ShardIteratorType;
@@ -105,40 +105,19 @@ public class Kinesis2Consumer extends
ScheduledBatchPollingConsumer implements R
return 0;
}
- var request = DescribeStreamRequest
- .builder()
-
.streamName(getEndpoint().getConfiguration().getStreamName())
- .build();
- DescribeStreamResponse response;
- if (getEndpoint().getConfiguration().isAsyncClient()) {
- try {
- response = connection
- .getAsyncClient(getEndpoint())
- .describeStream(request)
- .get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return 0;
- } catch (ExecutionException e) {
- throw new RuntimeException(e);
- }
- } else {
- response = connection
- .getClient(getEndpoint())
- .describeStream(request);
- }
-
- var shard = response
- .streamDescription()
- .shards()
- .stream()
- .filter(shardItem -> shardItem
- .shardId()
- .equalsIgnoreCase(getEndpoint()
- .getConfiguration()
- .getShardId()))
+ // Use the cached shard list from ShardMonitor instead of calling
DescribeStream
+ // on every poll. ShardMonitor uses ListShards (paginated, 100 TPS
limit) vs
+ // DescribeStream (unpaginated, 10 TPS limit).
+ Shard shard = getCurrentShardList().stream()
+ .filter(s -> s.shardId().equalsIgnoreCase(shardId))
.findFirst()
- .orElseThrow(() -> new IllegalStateException("The shard
can't be found"));
+ .orElse(null);
+
+ if (shard == null) {
+ LOG.warn("Configured shardId {} not found in shard list for
stream {}",
+ shardId,
getEndpoint().getConfiguration().getStreamName());
+ return 0;
+ }
fetchAndPrepareRecordsForCamel(shard, connection,
processedExchangeCount);
@@ -471,33 +450,42 @@ public class Kinesis2Consumer extends
ScheduledBatchPollingConsumer implements R
}
private List<Shard> getShardList(final KinesisConnection
kinesisConnection) {
- var request = ListShardsRequest
- .builder()
-
.streamName(getEndpoint().getConfiguration().getStreamName())
- .build();
-
- List<Shard> shardList;
- if (getEndpoint().getConfiguration().isAsyncClient()) {
- try {
- shardList = kinesisConnection
- .getAsyncClient(getEndpoint())
- .listShards(request)
- .get()
- .shards();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException(e);
- } catch (ExecutionException e) {
- throw new RuntimeException(e);
+ List<Shard> allShards = new ArrayList<>();
+ String nextToken = null;
+
+ do {
+ ListShardsRequest.Builder requestBuilder =
ListShardsRequest.builder();
+ if (nextToken != null) {
+ requestBuilder.nextToken(nextToken);
+ } else {
+
requestBuilder.streamName(getEndpoint().getConfiguration().getStreamName());
}
- } else {
- shardList = kinesisConnection
- .getClient(getEndpoint())
- .listShards(request)
- .shards();
- }
+ ListShardsRequest request = requestBuilder.build();
+
+ ListShardsResponse response;
+ if (getEndpoint().getConfiguration().isAsyncClient()) {
+ try {
+ response = kinesisConnection
+ .getAsyncClient(getEndpoint())
+ .listShards(request)
+ .get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ response = kinesisConnection
+ .getClient(getEndpoint())
+ .listShards(request);
+ }
+
+ allShards.addAll(response.shards());
+ nextToken = response.nextToken();
+ } while (nextToken != null);
- return shardList;
+ return allShards;
}
}
}
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Producer.java
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Producer.java
index 54a8f884cadc..0858b1d022f2 100644
---
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Producer.java
+++
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Producer.java
@@ -73,10 +73,17 @@ public class Kinesis2Producer extends DefaultProducer {
}
}
+ @SuppressWarnings("unchecked")
private void sendBatchRecords(Exchange exchange) {
+ List<String> partitionKeys =
exchange.getIn().getHeader(Kinesis2Constants.PARTITION_KEYS, List.class);
Object partitionKey =
exchange.getIn().getHeader(Kinesis2Constants.PARTITION_KEY);
- ensurePartitionKeyNotNull(partitionKey);
- List<List<PutRecordsRequestEntry>> requestBatchList =
createRequestBatchList(exchange, partitionKey);
+ if (partitionKeys == null) {
+ ensurePartitionKeyNotNull(partitionKey);
+ } else if (partitionKeys.isEmpty()) {
+ throw new IllegalArgumentException("CamelAwsKinesisPartitionKeys
header must not be empty");
+ }
+ List<List<PutRecordsRequestEntry>> requestBatchList
+ = createRequestBatchList(exchange, partitionKey,
partitionKeys);
int totalRecordCount = 0;
int totalFailedCount = 0;
for (List<PutRecordsRequestEntry> requestBatch : requestBatchList) {
@@ -98,9 +105,11 @@ public class Kinesis2Producer extends DefaultProducer {
message.setHeader(Kinesis2Constants.FAILED_RECORD_COUNT,
totalFailedCount);
}
- private List<List<PutRecordsRequestEntry>> createRequestBatchList(Exchange
exchange, Object partitionKey) {
+ private List<List<PutRecordsRequestEntry>> createRequestBatchList(
+ Exchange exchange, Object partitionKey, List<String>
partitionKeys) {
List<List<PutRecordsRequestEntry>> requestBatchList = new
ArrayList<>();
List<PutRecordsRequestEntry> requestBatch = new
ArrayList<>(MAX_BATCH_SIZE);
+ int index = 0;
for (Object record : exchange.getIn().getBody(Iterable.class)) {
SdkBytes sdkBytes;
if (record instanceof byte[] bytes) {
@@ -116,15 +125,24 @@ public class Kinesis2Producer extends DefaultProducer {
"Record type not supported. Must be byte[],
ByteBuffer, InputStream or UTF-8 String");
}
+ String key;
+ if (partitionKeys != null && index < partitionKeys.size()) {
+ key = partitionKeys.get(index);
+ } else {
+ ensurePartitionKeyNotNull(partitionKey);
+ key = partitionKey.toString();
+ }
+
PutRecordsRequestEntry putRecordsRequestEntry =
PutRecordsRequestEntry.builder()
.data(sdkBytes)
- .partitionKey(partitionKey.toString())
+ .partitionKey(key)
.build();
requestBatch.add(putRecordsRequestEntry);
if (requestBatch.size() == MAX_BATCH_SIZE) {
requestBatchList.add(requestBatch);
requestBatch = new ArrayList<>(MAX_BATCH_SIZE);
}
+ index++;
}
if (!requestBatch.isEmpty()) {
requestBatchList.add(requestBatch);
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/Kinesis2ProducerTest.java
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/Kinesis2ProducerTest.java
new file mode 100644
index 000000000000..4282e96a320b
--- /dev/null
+++
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/Kinesis2ProducerTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.aws2.kinesis;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.awssdk.services.kinesis.KinesisClient;
+import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest;
+import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry;
+import software.amazon.awssdk.services.kinesis.model.PutRecordsResponse;
+import software.amazon.awssdk.services.kinesis.model.PutRecordsResultEntry;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class Kinesis2ProducerTest {
+
+ @Mock
+ private KinesisClient kinesisClient;
+
+ private final CamelContext context = new DefaultCamelContext();
+ private final Kinesis2Component component = new Kinesis2Component(context);
+ private Kinesis2Producer producer;
+
+ @BeforeEach
+ public void setup() {
+ component.start();
+
+ Kinesis2Configuration configuration = new Kinesis2Configuration();
+ configuration.setAmazonKinesisClient(kinesisClient);
+ configuration.setStreamName("testStream");
+
+ Kinesis2Endpoint endpoint = new
Kinesis2Endpoint("aws2-kinesis:testStream", configuration, component);
+ endpoint.start();
+ producer = new Kinesis2Producer(endpoint);
+ producer.setConnection(component.getConnection());
+ producer.start();
+ }
+
+ @Test
+ public void batchWithSinglePartitionKeyAppliesSameKeyToAll() throws
Exception {
+ when(kinesisClient.putRecords(any(PutRecordsRequest.class)))
+ .thenReturn(PutRecordsResponse.builder()
+ .failedRecordCount(0)
+ .records(PutRecordsResultEntry.builder().build(),
+ PutRecordsResultEntry.builder().build())
+ .build());
+
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEY,
"shared-key");
+ exchange.getIn().setBody(Arrays.asList("record1", "record2"));
+
+ producer.process(exchange);
+
+ ArgumentCaptor<PutRecordsRequest> captor =
ArgumentCaptor.forClass(PutRecordsRequest.class);
+ verify(kinesisClient).putRecords(captor.capture());
+
+ List<PutRecordsRequestEntry> entries = captor.getValue().records();
+ assertEquals(2, entries.size());
+ assertEquals("shared-key", entries.get(0).partitionKey());
+ assertEquals("shared-key", entries.get(1).partitionKey());
+ }
+
+ @Test
+ public void batchWithPerRecordPartitionKeys() throws Exception {
+ when(kinesisClient.putRecords(any(PutRecordsRequest.class)))
+ .thenReturn(PutRecordsResponse.builder()
+ .failedRecordCount(0)
+ .records(PutRecordsResultEntry.builder().build(),
+ PutRecordsResultEntry.builder().build(),
+ PutRecordsResultEntry.builder().build())
+ .build());
+
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEYS,
+ Arrays.asList("key-A", "key-B", "key-C"));
+ exchange.getIn().setBody(Arrays.asList("record1", "record2",
"record3"));
+
+ producer.process(exchange);
+
+ ArgumentCaptor<PutRecordsRequest> captor =
ArgumentCaptor.forClass(PutRecordsRequest.class);
+ verify(kinesisClient).putRecords(captor.capture());
+
+ List<PutRecordsRequestEntry> entries = captor.getValue().records();
+ assertEquals(3, entries.size());
+ assertEquals("key-A", entries.get(0).partitionKey());
+ assertEquals("key-B", entries.get(1).partitionKey());
+ assertEquals("key-C", entries.get(2).partitionKey());
+ }
+
+ @Test
+ public void batchWithFewerPartitionKeysFallsBackToSingleKey() throws
Exception {
+ when(kinesisClient.putRecords(any(PutRecordsRequest.class)))
+ .thenReturn(PutRecordsResponse.builder()
+ .failedRecordCount(0)
+ .records(PutRecordsResultEntry.builder().build(),
+ PutRecordsResultEntry.builder().build(),
+ PutRecordsResultEntry.builder().build())
+ .build());
+
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEY,
"fallback-key");
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEYS,
+ Arrays.asList("key-A"));
+ exchange.getIn().setBody(Arrays.asList("record1", "record2",
"record3"));
+
+ producer.process(exchange);
+
+ ArgumentCaptor<PutRecordsRequest> captor =
ArgumentCaptor.forClass(PutRecordsRequest.class);
+ verify(kinesisClient).putRecords(captor.capture());
+
+ List<PutRecordsRequestEntry> entries = captor.getValue().records();
+ assertEquals(3, entries.size());
+ assertEquals("key-A", entries.get(0).partitionKey());
+ assertEquals("fallback-key", entries.get(1).partitionKey());
+ assertEquals("fallback-key", entries.get(2).partitionKey());
+ }
+
+ @Test
+ public void batchWithFewerPartitionKeysAndNoFallbackThrows() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEYS,
+ Arrays.asList("key-A"));
+ exchange.getIn().setBody(Arrays.asList("record1", "record2",
"record3"));
+
+ assertThrows(IllegalArgumentException.class, () ->
producer.process(exchange));
+ }
+
+ @Test
+ public void batchWithEmptyPartitionKeysListThrows() {
+ Exchange exchange = new DefaultExchange(context);
+ exchange.getIn().setHeader(Kinesis2Constants.PARTITION_KEYS,
List.of());
+ exchange.getIn().setBody(Arrays.asList("record1"));
+
+ assertThrows(IllegalArgumentException.class, () ->
producer.process(exchange));
+ }
+}
diff --git
a/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisConsumerClosedShardWithSilentTest.java
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisConsumerClosedShardWithSilentTest.java
index eb438c82f72f..b1f996362a64 100644
---
a/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisConsumerClosedShardWithSilentTest.java
+++
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisConsumerClosedShardWithSilentTest.java
@@ -35,7 +35,6 @@ import org.mockito.quality.Strictness;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kinesis.KinesisClient;
import software.amazon.awssdk.services.kinesis.model.DescribeStreamRequest;
-import software.amazon.awssdk.services.kinesis.model.DescribeStreamResponse;
import software.amazon.awssdk.services.kinesis.model.GetRecordsRequest;
import software.amazon.awssdk.services.kinesis.model.GetRecordsResponse;
import software.amazon.awssdk.services.kinesis.model.GetShardIteratorRequest;
@@ -46,12 +45,12 @@ import software.amazon.awssdk.services.kinesis.model.Record;
import software.amazon.awssdk.services.kinesis.model.SequenceNumberRange;
import software.amazon.awssdk.services.kinesis.model.Shard;
import software.amazon.awssdk.services.kinesis.model.ShardIteratorType;
-import software.amazon.awssdk.services.kinesis.model.StreamDescription;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -127,21 +126,13 @@ public class KinesisConsumerClosedShardWithSilentTest {
}
@Test
- public void itDoesNotMakeADescribeStreamRequestIfShardIdIsSet() throws
Exception {
-
- SequenceNumberRange range =
SequenceNumberRange.builder().endingSequenceNumber("20").build();
- Shard shard =
Shard.builder().shardId("shardId").sequenceNumberRange(range).build();
- ArrayList<Shard> shardList = new ArrayList<>();
- shardList.add(shard);
-
- when(kinesisClient.describeStream(any(DescribeStreamRequest.class)))
- .thenReturn(DescribeStreamResponse.builder()
-
.streamDescription(StreamDescription.builder().shards(shardList).build()).build());
-
+ public void itUsesShardMonitorCacheNotDescribeStreamForFixedShardId()
throws Exception {
underTest.getEndpoint().getConfiguration().setShardId("shardId");
underTest.poll();
+ verify(kinesisClient,
never()).describeStream(any(DescribeStreamRequest.class));
+
final ArgumentCaptor<GetShardIteratorRequest> getShardIteratorReqCap
= ArgumentCaptor.forClass(GetShardIteratorRequest.class);
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 88c5a3f180c4..2d79112cac9f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -414,6 +414,26 @@ The `camel infra list` table now sizes its `DESCRIPTION`
column to the terminal
the `IMPLEMENTATION` and `SERVICE_DATA` columns with an ellipsis instead of
letting the raw service
data overflow the terminal. The complete, structured service data remains
available via `--json`.
+=== camel-aws2-kinesis - Fixed-shardId consumer no longer calls DescribeStream
on every poll
+
+The Kinesis consumer with a configured `shardId` previously called the
`DescribeStream` API on every
+poll cycle to locate the shard, risking AWS rate limits (10 TPS per account)
and failing for streams
+with more than 100 shards (no pagination). The consumer now uses the same
cached shard list from the
+`ShardMonitor` background thread that the multi-shard path already uses. The
`ShardMonitor` uses the
+`ListShards` API (paginated, 100 TPS limit) and now paginates through all
pages, supporting streams
+with any number of shards.
+
+This is a transparent performance improvement with no configuration changes
required.
+
+=== camel-aws2-kinesis - Per-record partition keys in batch producer
+
+The batch producer (body is an `Iterable`) previously applied a single
`CamelAwsKinesisPartitionKey`
+header to all records, routing them all to the same shard. A new
`CamelAwsKinesisPartitionKeys` header
+(`List<String>`) is now supported: when set, each record in the batch is
assigned the partition key at
+the corresponding index. If the list has fewer entries than records, the
remaining records fall back to
+the single `CamelAwsKinesisPartitionKey` header. The existing single-key
behavior is unchanged when
+the new header is not set.
+
=== camel-azure-storage-blob / camel-azure-storage-datalake - download
contained within fileDir
When `fileDir` is configured, the Azure Storage Blob and DataLake consumers
now ensure the downloaded
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Kinesis2EndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Kinesis2EndpointBuilderFactory.java
index f5ee78963c33..1c731d648672 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Kinesis2EndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Kinesis2EndpointBuilderFactory.java
@@ -2889,6 +2889,22 @@ public interface Kinesis2EndpointBuilderFactory {
public String awsKinesisShardId() {
return "CamelAwsKinesisShardId";
}
+ /**
+ * A list of partition keys for batch operations. When set, each record
+ * in the batch is assigned the partition key at the corresponding
+ * index, allowing records to be routed to different shards. If not
set,
+ * the single CamelAwsKinesisPartitionKey header is used for all
+ * records.
+ *
+ * The option is a: {@code List<String>} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code AwsKinesisPartitionKeys}.
+ */
+ public String awsKinesisPartitionKeys() {
+ return "CamelAwsKinesisPartitionKeys";
+ }
/**
* The number of records that failed in a batch put operation.
*