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 43cd5f0e70d9 CAMEL-24357: camel-aws2-s3-vectors - consumer sends a
valid topK, honors delay, and no longer loses vectors on failure
43cd5f0e70d9 is described below
commit 43cd5f0e70d9274a83aecfdde346d9b3c66c9d29
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Aug 6 12:32:42 2026 +0200
CAMEL-24357: camel-aws2-s3-vectors - consumer sends a valid topK, honors
delay, and no longer loses vectors on failure
The aws2-s3-vectors consumer had three defects rooted in consumer options
shadowing the base scheduled-poll options:
- poll() sent topK(0) because maxMessagesPerPoll was never wired (default
0),
so the consumer delivered zero messages. Now wired from configuration and
treats non-positive cap as "unlimited".
- The delay option bound to a configuration field the consumer never read;
now propagated to the consumer's scheduler.
- Vectors were marked processed before routing, so a failed exchange was
skipped forever; the de-dup set now only applies when
deleteAfterRead=false
and drops ids on failure for retry.
Adds AWS2S3VectorsConsumerTest covering the topK and delay fixes.
Closes #25369
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
components/camel-aws/camel-aws2-s3-vectors/pom.xml | 5 ++
.../aws2/s3vectors/AWS2S3VectorsConsumer.java | 41 ++++++++++--
.../aws2/s3vectors/AWS2S3VectorsEndpoint.java | 4 ++
.../aws2/s3vectors/AWS2S3VectorsConsumerTest.java | 75 ++++++++++++++++++++++
4 files changed, 121 insertions(+), 4 deletions(-)
diff --git a/components/camel-aws/camel-aws2-s3-vectors/pom.xml
b/components/camel-aws/camel-aws2-s3-vectors/pom.xml
index a39f057666b1..e8fd76d1969d 100644
--- a/components/camel-aws/camel-aws2-s3-vectors/pom.xml
+++ b/components/camel-aws/camel-aws2-s3-vectors/pom.xml
@@ -70,6 +70,11 @@
<artifactId>camel-test-spring-junit6</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- test infra -->
<dependency>
diff --git
a/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumer.java
b/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumer.java
index 9267d901a483..3a7811d62dc0 100644
---
a/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumer.java
+++
b/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumer.java
@@ -83,7 +83,7 @@ public class AWS2S3VectorsConsumer extends
ScheduledBatchPollingConsumer {
.vectorBucketName(vectorBucketName)
.indexName(vectorIndexName)
.queryVector(VectorData.builder().float32(queryVector).build())
- .topK(Math.min(getMaxMessagesPerPoll(),
getConfiguration().getTopK()));
+ .topK(resolveTopK());
// Add metadata filter if configured
String metadataFilter =
getConfiguration().getConsumerMetadataFilter();
@@ -121,15 +121,18 @@ public class AWS2S3VectorsConsumer extends
ScheduledBatchPollingConsumer {
message.setHeader(AWS2S3VectorsConstants.VECTOR_BUCKET_NAME, vectorBucketName);
message.setHeader(AWS2S3VectorsConstants.VECTOR_INDEX_NAME, vectorIndexName);
- // Add to processed set
- processedVectorIds.add(vectorId);
-
// Add delete callback if deleteAfterRead is enabled
if (getConfiguration().isDeleteAfterRead()) {
exchange.getExchangeExtension().addOnCompletion(
new VectorDeleteSynchronization(
getS3VectorsClient(),
vectorBucketName, vectorIndexName,
vectorId));
+ } else {
+ // Track for de-duplication only when we are not
deleting (a deleted vector cannot be
+ // returned again). Mark it now to avoid re-delivering
it on overlapping polls, but drop it
+ // again if the exchange fails so it can be retried on
a subsequent poll.
+ processedVectorIds.add(vectorId);
+ exchange.getExchangeExtension().addOnCompletion(new
VectorDedupSynchronization(vectorId));
}
exchanges.add(exchange);
@@ -200,6 +203,18 @@ public class AWS2S3VectorsConsumer extends
ScheduledBatchPollingConsumer {
return getEndpoint().getS3VectorsClient();
}
+ /**
+ * Resolves the number of nearest vectors to request from the similarity
search. The configured {@code topK} is
+ * capped by {@code maxMessagesPerPoll} only when that is a positive
value; a non-positive
+ * {@code maxMessagesPerPoll} (the "unlimited" default) leaves the
configured {@code topK} unchanged. AWS S3 Vectors
+ * requires {@code topK >= 1}.
+ */
+ private int resolveTopK() {
+ int topK = getConfiguration().getTopK();
+ int max = getMaxMessagesPerPoll();
+ return max > 0 ? Math.min(max, topK) : topK;
+ }
+
/**
* Parse comma-separated vector string into List<Float>
*
@@ -268,4 +283,22 @@ public class AWS2S3VectorsConsumer extends
ScheduledBatchPollingConsumer {
LOG.trace("Exchange failed, not deleting vector [{}]", vectorId);
}
}
+
+ /**
+ * Synchronization callback that drops a vector id from the de-duplication
set when the exchange fails, so the
+ * vector can be reprocessed on a subsequent poll instead of being skipped
forever.
+ */
+ private class VectorDedupSynchronization extends SynchronizationAdapter {
+
+ private final String vectorId;
+
+ VectorDedupSynchronization(String vectorId) {
+ this.vectorId = vectorId;
+ }
+
+ @Override
+ public void onFailure(Exchange exchange) {
+ processedVectorIds.remove(vectorId);
+ }
+ }
}
diff --git
a/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsEndpoint.java
b/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsEndpoint.java
index 5c6be4a6a2cd..23dea141b1fd 100644
---
a/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsEndpoint.java
+++
b/components/camel-aws/camel-aws2-s3-vectors/src/main/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsEndpoint.java
@@ -85,6 +85,10 @@ public class AWS2S3VectorsEndpoint extends
ScheduledPollEndpoint implements Endp
public Consumer createConsumer(Processor processor) throws Exception {
AWS2S3VectorsConsumer consumer = new AWS2S3VectorsConsumer(this,
processor);
configureConsumer(consumer);
+ consumer.setMaxMessagesPerPoll(configuration.getMaxMessagesPerPoll());
+ // the delay is a configuration option (it would otherwise shadow
ScheduledPollEndpoint#delay and be
+ // ignored), so propagate it to the consumer's scheduler
+ consumer.setDelay(configuration.getDelay());
return consumer;
}
diff --git
a/components/camel-aws/camel-aws2-s3-vectors/src/test/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumerTest.java
b/components/camel-aws/camel-aws2-s3-vectors/src/test/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumerTest.java
new file mode 100644
index 000000000000..285bc62dc5bc
--- /dev/null
+++
b/components/camel-aws/camel-aws2-s3-vectors/src/test/java/org/apache/camel/component/aws2/s3vectors/AWS2S3VectorsConsumerTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.s3vectors;
+
+import java.util.List;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import software.amazon.awssdk.services.s3vectors.S3VectorsClient;
+import software.amazon.awssdk.services.s3vectors.model.QueryVectorsRequest;
+import software.amazon.awssdk.services.s3vectors.model.QueryVectorsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+class AWS2S3VectorsConsumerTest {
+
+ @Test
+ void consumerSendsPositiveTopK() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ S3VectorsClient client = Mockito.mock(S3VectorsClient.class);
+ ArgumentCaptor<QueryVectorsRequest> captor =
ArgumentCaptor.forClass(QueryVectorsRequest.class);
+ when(client.queryVectors(captor.capture()))
+
.thenReturn(QueryVectorsResponse.builder().vectors(List.of()).build());
+
+ AWS2S3VectorsEndpoint endpoint = context.getEndpoint(
+
"aws2-s3-vectors://test-bucket?vectorIndexName=test-index&accessKey=test&secretKey=test",
+ AWS2S3VectorsEndpoint.class);
+ endpoint.getConfiguration().setConsumerQueryVector("0.1,0.2,0.3");
+ endpoint.setS3VectorsClient(client);
+
+ AWS2S3VectorsConsumer consumer = (AWS2S3VectorsConsumer)
endpoint.createConsumer(exchange -> {
+ });
+
+ consumer.poll();
+
+ // Before the fix the topK was Math.min(maxMessagesPerPoll, topK)
where maxMessagesPerPoll was the
+ // unwired base-class field (0), so topK(0) was sent and AWS
returned nothing. It must now be >= 1.
+ assertThat(captor.getValue().topK())
+ .as("topK sent to AWS S3 Vectors must be >= 1")
+ .isEqualTo(10);
+ }
+ }
+
+ @Test
+ void delayOptionDrivesTheConsumerPollInterval() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ AWS2S3VectorsEndpoint endpoint = context.getEndpoint(
+
"aws2-s3-vectors://test-bucket?vectorIndexName=test-index&delay=1234&accessKey=test&secretKey=test",
+ AWS2S3VectorsEndpoint.class);
+
+ AWS2S3VectorsConsumer consumer = (AWS2S3VectorsConsumer)
endpoint.createConsumer(exchange -> {
+ });
+
+ // delay used to be ignored (a shadow field on the configuration);
it must now drive the actual poll interval
+ assertThat(consumer.getDelay()).isEqualTo(1234L);
+ }
+ }
+}