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 12360f853d33 CAMEL-25759: camel-aws2-eventbridge - keep one target on 
the rule for a queue given with queueUrl (#28074)
12360f853d33 is described below

commit 12360f853d332f6a996dc0a0d105876b1fd34059
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Oct 19 14:50:31 2026 +0530

    CAMEL-25759: camel-aws2-eventbridge - keep one target on the rule for a 
queue given with queueUrl (#28074)
    
    The consumer adds its SQS queue as a target of the EventBridge rule when it
    starts, with a new random target id each time, and removes that target when 
it
    stops. With a queue given by the user (queueUrl) this leaves the rule with 
two
    targets for the same queue when two consumers share the queue (two routes, 
or
    two instances of the application), or when a consumer was not stopped 
cleanly
    (the JVM was killed) and starts again. EventBridge sends every matching 
event
    to every target, so every event is put on the queue twice, and routed twice.
    
    The target of a user-provided queue now has an id derived from the queue 
ARN,
    so every start and every consumer of the queue updates the same target, and 
the
    target stays on the rule when the consumer stops: other consumers may still 
use
    the queue, and the queue keeps collecting the events of the rule while no
    consumer runs. The auto-created queue (one per consumer, deleted on stop) 
keeps
    its random target id and its clean-up.
    
    As suggested in the review: an instance on an earlier version and one on
    this version add two different targets for the queue, so events are put
    on the queue twice until the last instance on the earlier version stops.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../catalog/docs/aws2-eventbridge-component.adoc   |   4 +-
 .../src/main/docs/aws2-eventbridge-component.adoc  |   4 +-
 .../aws2/eventbridge/EventbridgeConsumer.java      |  32 +++-
 .../aws2/eventbridge/EventbridgeConsumerTest.java  |   6 +-
 .../EventbridgeConsumerUserQueueTargetTest.java    | 161 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 6 files changed, 207 insertions(+), 11 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-eventbridge-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-eventbridge-component.adoc
index 78a53b95aaec..ff540e28fcc4 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-eventbridge-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-eventbridge-component.adoc
@@ -612,7 +612,9 @@ This is the standard AWS pattern for programmatically 
consuming EventBridge even
 3. It adds the queue as a target of the EventBridge rule specified by 
`ruleName`.
 4. It polls the SQS queue on schedule, delivering each event into the Camel 
route.
 5. Successfully processed messages are deleted from the queue; failed messages 
remain for retry.
-6. On shutdown the target is removed and the auto-created queue is deleted 
(configurable).
+6. On shutdown the target of an auto-created queue is removed and the queue is 
deleted (configurable).
+A queue given with `queueUrl` keeps its target, so it collects the events 
while no consumer runs, and all the
+consumers that share it use the same target (its id is derived from the queue 
ARN).
 
 === Consumer Configuration Options
 
diff --git 
a/components/camel-aws/camel-aws2-eventbridge/src/main/docs/aws2-eventbridge-component.adoc
 
b/components/camel-aws/camel-aws2-eventbridge/src/main/docs/aws2-eventbridge-component.adoc
index 78a53b95aaec..ff540e28fcc4 100644
--- 
a/components/camel-aws/camel-aws2-eventbridge/src/main/docs/aws2-eventbridge-component.adoc
+++ 
b/components/camel-aws/camel-aws2-eventbridge/src/main/docs/aws2-eventbridge-component.adoc
@@ -612,7 +612,9 @@ This is the standard AWS pattern for programmatically 
consuming EventBridge even
 3. It adds the queue as a target of the EventBridge rule specified by 
`ruleName`.
 4. It polls the SQS queue on schedule, delivering each event into the Camel 
route.
 5. Successfully processed messages are deleted from the queue; failed messages 
remain for retry.
-6. On shutdown the target is removed and the auto-created queue is deleted 
(configurable).
+6. On shutdown the target of an auto-created queue is removed and the queue is 
deleted (configurable).
+A queue given with `queueUrl` keeps its target, so it collects the events 
while no consumer runs, and all the
+consumers that share it use the same target (its id is derived from the queue 
ARN).
 
 === Consumer Configuration Options
 
diff --git 
a/components/camel-aws/camel-aws2-eventbridge/src/main/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumer.java
 
b/components/camel-aws/camel-aws2-eventbridge/src/main/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumer.java
index abf4989734b2..54a3450a8dfb 100644
--- 
a/components/camel-aws/camel-aws2-eventbridge/src/main/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumer.java
+++ 
b/components/camel-aws/camel-aws2-eventbridge/src/main/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumer.java
@@ -16,6 +16,10 @@
  */
 package org.apache.camel.component.aws2.eventbridge;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -61,7 +65,7 @@ import 
software.amazon.awssdk.services.sqs.model.SetQueueAttributesRequest;
  *
  * <p>
  * The consumer can auto-create an SQS queue and wire it to the EventBridge 
rule, or use a user-provided queue. On
- * shutdown, auto-provisioned resources are cleaned up by default.
+ * shutdown, auto-provisioned resources are cleaned up by default. A 
user-provided queue keeps its target on the rule.
  * </p>
  */
 public class EventbridgeConsumer extends ScheduledBatchPollingConsumer {
@@ -184,8 +188,12 @@ public class EventbridgeConsumer extends 
ScheduledBatchPollingConsumer {
                     "Either queueUrl must be specified or autoCreateQueue must 
be enabled for EventBridge consumer");
         }
 
-        // Add queue as EventBridge target
-        targetId = "camel-eb-" + UUID.randomUUID().toString().substring(0, 8);
+        // Add queue as EventBridge target. The target of a user-provided 
queue has an id derived from the queue, so
+        // every start and every consumer that shares the queue updates the 
same target (putTargets replaces a target
+        // with the same id): two targets for one queue would each put a copy 
of every event on the queue.
+        targetId = queueAutoCreated
+                ? "camel-eb-" + UUID.randomUUID().toString().substring(0, 8)
+                : targetIdForQueue(queueArn);
         Target target = Target.builder()
                 .id(targetId)
                 .arn(queueArn)
@@ -211,8 +219,9 @@ public class EventbridgeConsumer extends 
ScheduledBatchPollingConsumer {
         EventbridgeConfiguration config = getEndpoint().getConfiguration();
 
         try {
-            // Remove target from EventBridge rule
-            if (ObjectHelper.isNotEmpty(targetId)) {
+            // Remove the target of an auto-created queue. The target of a 
user-provided queue is kept: other consumers
+            // can share the queue, and the queue keeps collecting the events 
of the rule while this consumer is stopped.
+            if (queueAutoCreated && ObjectHelper.isNotEmpty(targetId)) {
                 EventBridgeClient ebClient = 
getEndpoint().getEventbridgeClient();
                 ebClient.removeTargets(
                         RemoveTargetsRequest.builder()
@@ -265,6 +274,19 @@ public class EventbridgeConsumer extends 
ScheduledBatchPollingConsumer {
         }
     }
 
+    /**
+     * The EventBridge target id for a user-provided queue: the same for every 
start and every consumer of the queue.
+     */
+    static String targetIdForQueue(String queueArn) {
+        try {
+            byte[] digest = 
MessageDigest.getInstance("SHA-256").digest(queueArn.getBytes(StandardCharsets.UTF_8));
+            // a target id has at most 64 characters of [.-_A-Za-z0-9]
+            return "camel-eb-" + HexFormat.of().formatHex(digest, 0, 16);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("SHA-256 is not available", e);
+        }
+    }
+
     private String getQueueArn(String queueUrl) {
         GetQueueAttributesResponse response = sqsClient.getQueueAttributes(
                 GetQueueAttributesRequest.builder()
diff --git 
a/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerTest.java
 
b/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerTest.java
index e68b908a7042..32e254aaa1fd 100644
--- 
a/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerTest.java
+++ 
b/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerTest.java
@@ -371,15 +371,13 @@ public class EventbridgeConsumerTest extends 
CamelTestSupport {
     public void testUserProvidedQueueNotDeletedOnShutdown() throws Exception {
         stubGetQueueAttributes(USER_QUEUE_URL, USER_QUEUE_ARN);
         stubPutTargetsSuccess();
-        when(eventBridgeClient.removeTargets(any(RemoveTargetsRequest.class)))
-                .thenReturn(RemoveTargetsResponse.builder().build());
 
-        // deleteQueueOnShutdown=true, but queue was user-provided so should 
NOT be deleted
+        // deleteQueueOnShutdown=true, but queue was user-provided so should 
NOT be deleted, and its target stays
         EventbridgeConsumer consumer = createConsumer("test-rule", 
USER_QUEUE_URL, true, true);
         consumer.start();
         consumer.stop();
 
-        
verify(eventBridgeClient).removeTargets(any(RemoveTargetsRequest.class));
+        verify(eventBridgeClient, 
never()).removeTargets(any(RemoveTargetsRequest.class));
         verify(sqsClient, never()).deleteQueue(any(DeleteQueueRequest.class));
     }
 
diff --git 
a/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerUserQueueTargetTest.java
 
b/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerUserQueueTargetTest.java
new file mode 100644
index 000000000000..7c271a2f2824
--- /dev/null
+++ 
b/components/camel-aws/camel-aws2-eventbridge/src/test/java/org/apache/camel/component/aws2/eventbridge/EventbridgeConsumerUserQueueTargetTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.eventbridge;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import software.amazon.awssdk.services.eventbridge.EventBridgeClient;
+import software.amazon.awssdk.services.eventbridge.model.PutTargetsRequest;
+import software.amazon.awssdk.services.eventbridge.model.PutTargetsResponse;
+import software.amazon.awssdk.services.eventbridge.model.RemoveTargetsRequest;
+import software.amazon.awssdk.services.eventbridge.model.RemoveTargetsResponse;
+import software.amazon.awssdk.services.eventbridge.model.Target;
+import software.amazon.awssdk.services.sqs.SqsClient;
+import software.amazon.awssdk.services.sqs.model.CreateQueueRequest;
+import software.amazon.awssdk.services.sqs.model.CreateQueueResponse;
+import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest;
+import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse;
+import software.amazon.awssdk.services.sqs.model.QueueAttributeName;
+import software.amazon.awssdk.services.sqs.model.SetQueueAttributesRequest;
+import software.amazon.awssdk.services.sqs.model.SetQueueAttributesResponse;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The targets of the EventBridge rule when consumers use a user-provided 
queue. EventBridge puts a copy of every
+ * matching event on the queue of every target of the rule, so the rule must 
hold one target for the queue whatever the
+ * number of consumers that share it, and whether a consumer stopped cleanly 
or not. The rule is a map of target id to
+ * target ARN kept by the mocked EventBridge client, with the semantics of 
PutTargets (a target with the same id is
+ * replaced) and RemoveTargets.
+ */
+class EventbridgeConsumerUserQueueTargetTest extends CamelTestSupport {
+
+    private static final String USER_QUEUE_URL = 
"https://sqs.us-east-1.amazonaws.com/123456789/user-queue";;
+    private static final String USER_QUEUE_ARN = 
"arn:aws:sqs:us-east-1:123456789:user-queue";
+    private static final String AUTO_QUEUE_URL = 
"https://sqs.us-east-1.amazonaws.com/123456789/camel-eventbridge-auto";;
+    private static final String AUTO_QUEUE_ARN = 
"arn:aws:sqs:us-east-1:123456789:camel-eventbridge-auto";
+
+    private final Map<String, String> ruleTargets = new LinkedHashMap<>();
+    private SqsClient sqsClient;
+    private EventBridgeClient eventBridgeClient;
+
+    @BeforeEach
+    void setupClients() {
+        sqsClient = mock(SqsClient.class);
+        eventBridgeClient = mock(EventBridgeClient.class);
+        
when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class))).thenAnswer(inv
 -> {
+            GetQueueAttributesRequest req = inv.getArgument(0);
+            String arn = USER_QUEUE_URL.equals(req.queueUrl()) ? 
USER_QUEUE_ARN : AUTO_QUEUE_ARN;
+            return 
GetQueueAttributesResponse.builder().attributes(Map.of(QueueAttributeName.QUEUE_ARN,
 arn)).build();
+        });
+        when(sqsClient.createQueue(any(CreateQueueRequest.class)))
+                
.thenReturn(CreateQueueResponse.builder().queueUrl(AUTO_QUEUE_URL).build());
+        
when(sqsClient.setQueueAttributes(any(SetQueueAttributesRequest.class)))
+                .thenReturn(SetQueueAttributesResponse.builder().build());
+        
when(eventBridgeClient.putTargets(any(PutTargetsRequest.class))).thenAnswer(inv 
-> {
+            PutTargetsRequest req = inv.getArgument(0);
+            for (Target target : req.targets()) {
+                ruleTargets.put(target.id(), target.arn());
+            }
+            return PutTargetsResponse.builder().failedEntryCount(0).build();
+        });
+        
when(eventBridgeClient.removeTargets(any(RemoveTargetsRequest.class))).thenAnswer(inv
 -> {
+            RemoveTargetsRequest req = inv.getArgument(0);
+            req.ids().forEach(ruleTargets::remove);
+            return RemoveTargetsResponse.builder().build();
+        });
+    }
+
+    @Test
+    void twoConsumersSharingAQueueAddOneTarget() throws Exception {
+        EventbridgeConsumer first = createConsumer(USER_QUEUE_URL);
+        EventbridgeConsumer second = createConsumer(USER_QUEUE_URL);
+        first.start();
+        second.start();
+
+        // two targets for the queue would put every event on the queue twice
+        assertEquals(1, targetsFor(USER_QUEUE_ARN), "targets of the rule: " + 
ruleTargets);
+
+        second.stop();
+        // the consumer that still runs keeps receiving the events of the rule
+        assertEquals(1, targetsFor(USER_QUEUE_ARN), "targets of the rule: " + 
ruleTargets);
+        first.stop();
+    }
+
+    @Test
+    void restartAfterACrashAddsNoSecondTarget() throws Exception {
+        // the first consumer is never stopped, as when the JVM is killed: its 
target is left on the rule
+        EventbridgeConsumer crashed = createConsumer(USER_QUEUE_URL);
+        crashed.start();
+        EventbridgeConsumer restarted = createConsumer(USER_QUEUE_URL);
+        restarted.start();
+
+        assertEquals(1, targetsFor(USER_QUEUE_ARN), "targets of the rule: " + 
ruleTargets);
+        restarted.stop();
+    }
+
+    @Test
+    void userQueueKeepsCollectingEventsWhileTheConsumerIsStopped() throws 
Exception {
+        EventbridgeConsumer consumer = createConsumer(USER_QUEUE_URL);
+        consumer.start();
+        consumer.stop();
+
+        assertEquals(1, targetsFor(USER_QUEUE_ARN), "targets of the rule: " + 
ruleTargets);
+    }
+
+    @Test
+    void autoCreatedQueueTargetIsStillRemovedOnStop() throws Exception {
+        EventbridgeConsumer consumer = createConsumer(null);
+        consumer.start();
+        assertEquals(1, targetsFor(AUTO_QUEUE_ARN), "targets of the rule: " + 
ruleTargets);
+        consumer.stop();
+
+        assertTrue(ruleTargets.isEmpty(), "targets of the rule: " + 
ruleTargets);
+    }
+
+    private long targetsFor(String arn) {
+        return ruleTargets.values().stream().filter(arn::equals).count();
+    }
+
+    private EventbridgeConsumer createConsumer(String queueUrl) throws 
Exception {
+        EventbridgeComponent component = 
context.getComponent("aws2-eventbridge", EventbridgeComponent.class);
+        String uri = 
"aws2-eventbridge://default?accessKey=test&secretKey=test&ruleName=test-rule";
+        if (queueUrl != null) {
+            uri += "&queueUrl=" + queueUrl;
+        }
+        EventbridgeEndpoint endpoint = (EventbridgeEndpoint) 
component.createEndpoint(uri);
+        endpoint.getConfiguration().setEventbridgeClient(eventBridgeClient);
+        EventbridgeEndpoint spyEndpoint = Mockito.spy(endpoint);
+        doReturn(sqsClient).when(spyEndpoint).getSqsClient();
+        doReturn(eventBridgeClient).when(spyEndpoint).getEventbridgeClient();
+
+        EventbridgeConsumer consumer = new EventbridgeConsumer(spyEndpoint, 
exchange -> {
+        });
+        consumer.setStartScheduler(false);
+        return consumer;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 5b1e7e99e39d..e5ad3f95769e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1439,6 +1439,17 @@ Previously it returned only the first chunk of the event 
log (at most 500 events
 any sign. A wide `after`/`before` window now returns every event in one list: 
that is one request per 500 events plus
 one that finds the end of the log, and more memory. Narrow the window when the 
event log is large.
 
+=== camel-aws2-eventbridge - the consumer keeps one target for a queue given 
with queueUrl
+
+A consumer that uses its own queue (`queueUrl`) added a target with a new 
random id to the EventBridge rule at every
+start, and removed it on stop. Two consumers that share the queue, or a 
restart after a consumer was not stopped
+cleanly, left two targets for the queue, and EventBridge put every event on 
the queue twice. The target now has an id
+derived from the queue ARN, and stays on the rule when the consumer stops, so 
the queue also collects the events of the
+rule while no consumer runs. Remove the target from the rule yourself when you 
no longer use the queue, and remove
+targets with ids `camel-eb-` plus 8 characters that an earlier version left 
for the queue. An auto-created queue is
+not affected. During a rolling upgrade from an earlier version, events can be 
put on the queue twice until the last
+instance on the earlier version stops.
+
 === camel-chunk - message data printed with a direct tag is rendered literally
 
 Previously the `chunk` component bound the message body, headers and variables 
into the Chunk engine so that

Reply via email to