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 4ee3a7e1dd08 CAMEL-25909: camel-aws2-kinesis - each endpoint uses and 
closes its own client (#28433)
4ee3a7e1dd08 is described below

commit 4ee3a7e1dd08af911dd1dd51ed1f7dec1f75f15c
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 25 13:01:42 2026 +0530

    CAMEL-25909: camel-aws2-kinesis - each endpoint uses and closes its own 
client (#28433)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../component/aws2/kinesis/Kinesis2Component.java  |   4 +
 .../component/aws2/kinesis/Kinesis2Endpoint.java   |  21 ++-
 .../component/aws2/kinesis/KinesisConnection.java  |  47 +++++--
 .../aws2/kinesis/KinesisEndpointClientTest.java    | 151 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  10 ++
 5 files changed, 210 insertions(+), 23 deletions(-)

diff --git 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Component.java
 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Component.java
index aef81b12aba6..765dbbb32749 100644
--- 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Component.java
+++ 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Component.java
@@ -82,6 +82,10 @@ public class Kinesis2Component extends HealthCheckComponent {
         IOHelper.close(connection);
     }
 
+    /**
+     * The connection with the client set on this component (if any). The 
endpoints do not use it: each endpoint has a
+     * connection of its own, with the client of its own configuration.
+     */
     public KinesisConnection getConnection() {
         return connection;
     }
diff --git 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Endpoint.java
 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Endpoint.java
index 85746c41217b..f4a299963947 100644
--- 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Endpoint.java
+++ 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Endpoint.java
@@ -62,6 +62,9 @@ public class Kinesis2Endpoint extends ScheduledPollEndpoint 
implements EndpointS
     @UriParam
     private Kinesis2Configuration configuration;
 
+    // the clients of this endpoint: a client created from the configuration 
of another endpoint has another region or
+    // other credentials, and stopping another endpoint must not close the 
client of this one
+    private final KinesisConnection kinesisConnection = new 
KinesisConnection();
     private KinesisClient kinesisClient;
     private KinesisAsyncClient kinesisAsyncClient;
 
@@ -74,8 +77,6 @@ public class Kinesis2Endpoint extends ScheduledPollEndpoint 
implements EndpointS
     protected void doStart() throws Exception {
         super.doStart();
 
-        var kinesisConnection = getComponent().getConnection();
-
         if (!configuration.isCborEnabled()) {
             System.setProperty(CBOR_ENABLED.property(), "false");
         }
@@ -97,13 +98,11 @@ public class Kinesis2Endpoint extends ScheduledPollEndpoint 
implements EndpointS
 
     @Override
     public void doStop() throws Exception {
-        if (ObjectHelper.isEmpty(configuration.getAmazonKinesisClient())) {
-            if (ObjectHelper.isNotEmpty(kinesisClient)) {
-                kinesisClient.close();
-            } else if (ObjectHelper.isNotEmpty(kinesisAsyncClient)) {
-                kinesisAsyncClient.close();
-            }
-        }
+        // closes only the clients this endpoint created, not a client given 
with amazonKinesisClient or
+        // amazonKinesisAsyncClient
+        kinesisConnection.close();
+        kinesisClient = null;
+        kinesisAsyncClient = null;
         if (!configuration.isCborEnabled()) {
             System.clearProperty(CBOR_ENABLED.property());
         }
@@ -113,7 +112,7 @@ public class Kinesis2Endpoint extends ScheduledPollEndpoint 
implements EndpointS
     @Override
     public Producer createProducer() throws Exception {
         Kinesis2Producer producer = new Kinesis2Producer(this);
-        producer.setConnection(getComponent().getConnection());
+        producer.setConnection(kinesisConnection);
         return producer;
     }
 
@@ -121,7 +120,7 @@ public class Kinesis2Endpoint extends ScheduledPollEndpoint 
implements EndpointS
     public Consumer createConsumer(Processor processor) throws Exception {
         if (!getConfiguration().isUseKclConsumers()) {
             final Kinesis2Consumer consumer = new Kinesis2Consumer(this, 
processor);
-            consumer.setConnection(getComponent().getConnection());
+            consumer.setConnection(kinesisConnection);
             consumer.setSchedulerProperties(getSchedulerProperties());
             configureConsumer(consumer);
             return consumer;
diff --git 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/KinesisConnection.java
 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/KinesisConnection.java
index 64d32eaf34a1..7ac848eb1f41 100644
--- 
a/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/KinesisConnection.java
+++ 
b/components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/KinesisConnection.java
@@ -33,15 +33,21 @@ public class KinesisConnection implements Closeable {
 
     private KinesisClient kinesisClient;
     private KinesisAsyncClient kinesisAsyncClient;
+    // whether the clients were created here (and are closed here), rather 
than given by the user
+    private boolean kinesisClientCreated;
+    private boolean kinesisAsyncClientCreated;
     private final Lock lock = new ReentrantLock();
 
     public KinesisClient getClient(final Kinesis2Endpoint endpoint) {
         lock.lock();
         try {
             if (ObjectHelper.isEmpty(kinesisClient)) {
-                kinesisClient = 
ObjectHelper.isNotEmpty(endpoint.getConfiguration().getAmazonKinesisClient())
-                        ? endpoint.getConfiguration().getAmazonKinesisClient()
-                        : 
KinesisClientFactory.getKinesisClient(endpoint.getConfiguration());
+                if 
(ObjectHelper.isNotEmpty(endpoint.getConfiguration().getAmazonKinesisClient())) 
{
+                    kinesisClient = 
endpoint.getConfiguration().getAmazonKinesisClient();
+                } else {
+                    kinesisClient = 
KinesisClientFactory.getKinesisClient(endpoint.getConfiguration());
+                    kinesisClientCreated = true;
+                }
             }
             return kinesisClient;
         } finally {
@@ -53,9 +59,12 @@ public class KinesisConnection implements Closeable {
         lock.lock();
         try {
             if (ObjectHelper.isEmpty(kinesisAsyncClient)) {
-                kinesisAsyncClient = 
ObjectHelper.isNotEmpty(endpoint.getConfiguration().getAmazonKinesisAsyncClient())
-                        ? 
endpoint.getConfiguration().getAmazonKinesisAsyncClient()
-                        : 
KinesisClientFactory.getKinesisAsyncClient(endpoint.getConfiguration());
+                if 
(ObjectHelper.isNotEmpty(endpoint.getConfiguration().getAmazonKinesisAsyncClient()))
 {
+                    kinesisAsyncClient = 
endpoint.getConfiguration().getAmazonKinesisAsyncClient();
+                } else {
+                    kinesisAsyncClient = 
KinesisClientFactory.getKinesisAsyncClient(endpoint.getConfiguration());
+                    kinesisAsyncClientCreated = true;
+                }
             }
             return kinesisAsyncClient;
         } finally {
@@ -64,25 +73,39 @@ public class KinesisConnection implements Closeable {
     }
 
     public void setKinesisClient(final KinesisClient kinesisClient) {
-        this.kinesisClient = kinesisClient;
+        lock.lock();
+        try {
+            this.kinesisClient = kinesisClient;
+            this.kinesisClientCreated = false;
+        } finally {
+            lock.unlock();
+        }
     }
 
     public void setKinesisAsyncClient(final KinesisAsyncClient 
kinesisAsyncClient) {
-        this.kinesisAsyncClient = kinesisAsyncClient;
+        lock.lock();
+        try {
+            this.kinesisAsyncClient = kinesisAsyncClient;
+            this.kinesisAsyncClientCreated = false;
+        } finally {
+            lock.unlock();
+        }
     }
 
     @Override
     public void close() throws IOException {
         lock.lock();
         try {
-            if (ObjectHelper.isNotEmpty(kinesisClient)) {
+            if (kinesisClientCreated && 
ObjectHelper.isNotEmpty(kinesisClient)) {
                 kinesisClient.close();
-                kinesisClient = null;
             }
-            if (ObjectHelper.isNotEmpty(kinesisAsyncClient)) {
+            if (kinesisAsyncClientCreated && 
ObjectHelper.isNotEmpty(kinesisAsyncClient)) {
                 kinesisAsyncClient.close();
-                kinesisAsyncClient = null;
             }
+            kinesisClient = null;
+            kinesisAsyncClient = null;
+            kinesisClientCreated = false;
+            kinesisAsyncClientCreated = false;
         } finally {
             lock.unlock();
         }
diff --git 
a/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisEndpointClientTest.java
 
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisEndpointClientTest.java
new file mode 100644
index 000000000000..69ce67583c3d
--- /dev/null
+++ 
b/components/camel-aws/camel-aws2-kinesis/src/test/java/org/apache/camel/component/aws2/kinesis/KinesisEndpointClientTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.services.kinesis.KinesisClient;
+import software.amazon.awssdk.services.kinesis.model.PutRecordRequest;
+import software.amazon.awssdk.services.kinesis.model.PutRecordResponse;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+/**
+ * Each aws2-kinesis endpoint uses the client of its own configuration, and 
stopping one endpoint does not close the
+ * client of another.
+ */
+public class KinesisEndpointClientTest {
+
+    // nothing listens there: a call with an open client fails to connect, a 
call with a closed client fails at once
+    private static final String NO_SERVER = 
"&overrideEndpoint=true&uriEndpointOverride=http://localhost:1";;
+    private static final String STREAM_A = 
"aws2-kinesis:streamA?accessKey=a&secretKey=a&region=us-east-1" + NO_SERVER;
+    private static final String STREAM_B = 
"aws2-kinesis:streamB?accessKey=b&secretKey=b&region=eu-west-1" + NO_SERVER;
+
+    private CamelContext context;
+
+    @BeforeEach
+    public void createContext() {
+        context = new DefaultCamelContext();
+    }
+
+    @AfterEach
+    public void stopContext() {
+        context.stop();
+    }
+
+    @Test
+    public void eachEndpointCreatesTheClientOfItsOwnConfiguration() throws 
Exception {
+        addRoutes(STREAM_A, STREAM_B);
+        context.start();
+
+        KinesisClient a = context.getEndpoint(STREAM_A, 
Kinesis2Endpoint.class).getClient();
+        KinesisClient b = context.getEndpoint(STREAM_B, 
Kinesis2Endpoint.class).getClient();
+        assertNotSame(a, b);
+        assertEquals("us-east-1", 
a.serviceClientConfiguration().region().id());
+        assertEquals("eu-west-1", 
b.serviceClientConfiguration().region().id());
+    }
+
+    @Test
+    public void eachEndpointCreatesTheAsyncClientOfItsOwnConfiguration() 
throws Exception {
+        String a = STREAM_A + "&asyncClient=true";
+        String b = STREAM_B + "&asyncClient=true";
+        addRoutes(a, b);
+        context.start();
+
+        assertEquals("us-east-1",
+                context.getEndpoint(a, 
Kinesis2Endpoint.class).getAsyncClient().serviceClientConfiguration().region()
+                        .id());
+        assertEquals("eu-west-1",
+                context.getEndpoint(b, 
Kinesis2Endpoint.class).getAsyncClient().serviceClientConfiguration().region()
+                        .id());
+    }
+
+    @Test
+    public void producerUsesTheClientConfiguredOnItsEndpoint() throws 
Exception {
+        KinesisClient clientA = mockClient();
+        KinesisClient clientB = mockClient();
+        context.getRegistry().bind("clientA", clientA);
+        context.getRegistry().bind("clientB", clientB);
+        addRoutes("aws2-kinesis:streamA?amazonKinesisClient=#clientA", 
"aws2-kinesis:streamB?amazonKinesisClient=#clientB");
+        context.start();
+
+        send("direct:b");
+
+        Mockito.verify(clientB).putRecord(Mockito.any(PutRecordRequest.class));
+        Mockito.verify(clientA, 
Mockito.never()).putRecord(Mockito.any(PutRecordRequest.class));
+    }
+
+    @Test
+    public void removingARouteKeepsTheClientOfAnotherEndpointOpen() throws 
Exception {
+        addRoutes(STREAM_A, STREAM_A.replace("streamA", "streamB"));
+        context.start();
+
+        context.getRouteController().stopRoute("a");
+        context.removeRoute("a");
+
+        // still connects (and fails to connect), instead of failing with 
"Connection pool shut down"
+        Exception cause = send("direct:b").getException();
+        assertInstanceOf(SdkClientException.class, cause, () -> "unexpected 
failure: " + cause);
+    }
+
+    @Test
+    public void clientConfiguredOnTheComponentIsNotClosed() throws Exception {
+        KinesisClient client = mockClient();
+        context.getComponent("aws2-kinesis", 
Kinesis2Component.class).getConfiguration().setAmazonKinesisClient(client);
+        addRoutes("aws2-kinesis:streamA", "aws2-kinesis:streamB");
+        context.start();
+        send("direct:a");
+
+        context.stop();
+
+        Mockito.verify(client).putRecord(Mockito.any(PutRecordRequest.class));
+        Mockito.verify(client, Mockito.never()).close();
+    }
+
+    private void addRoutes(String a, String b) throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:a").routeId("a").to(a);
+                from("direct:b").routeId("b").to(b);
+            }
+        });
+    }
+
+    private Exchange send(String uri) {
+        return context.createProducerTemplate().send(uri, exchange -> {
+            exchange.getMessage().setHeader(Kinesis2Constants.PARTITION_KEY, 
"key");
+            exchange.getMessage().setBody("record");
+        });
+    }
+
+    private static KinesisClient mockClient() {
+        KinesisClient client = Mockito.mock(KinesisClient.class);
+        Mockito.when(client.putRecord(Mockito.any(PutRecordRequest.class)))
+                
.thenReturn(PutRecordResponse.builder().sequenceNumber("1").shardId("shardId-000000000000").build());
+        return client;
+    }
+}
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 351dcbdf45de..3318a7a7ea2e 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
@@ -239,6 +239,16 @@ which embeds its own relocated copy of Vert.x. The `vertx` 
option of `camel-aran
 See the 
https://github.com/fabric8io/kubernetes-client/blob/main/doc/MIGRATION-v8.md[Kubernetes
 client 8 migration guide]
 for the other changes.
 
+=== camel-aws2-kinesis - each endpoint uses its own client
+
+Each `aws2-kinesis` endpoint now uses the Kinesis client of its own 
configuration (`region`, credentials,
+`uriEndpointOverride`, or `amazonKinesisClient` / `amazonKinesisAsyncClient`). 
Before, all endpoints of the component
+used the client of the endpoint that started first, and stopping one endpoint 
(for example by removing its route)
+closed that client for every other endpoint.
+
+Camel now closes only the clients it created. A client set with 
`amazonKinesisClient` or `amazonKinesisAsyncClient`, on
+the component or on an endpoint, is no longer closed when Camel stops: close 
it yourself when you no longer need it.
+
 === camel-gson, camel-jsonb - the unmarshal type header needs 
allowUnmarshallType
 
 The Gson and JSON-B data formats now use the `CamelGsonUnmarshalType` and 
`CamelJsonbUnmarshallType` headers to choose

Reply via email to