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

davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 640bf033c0b8 CAMEL-25297: Backport producer consumer cache race fix to 
camel-4.18.x (#27970)
640bf033c0b8 is described below

commit 640bf033c0b847a41e515c94946e59ed33099ea8
Author: maheshkumar0710 <[email protected]>
AuthorDate: Sun Oct 18 02:51:06 2026 -0500

    CAMEL-25297: Backport producer consumer cache race fix to camel-4.18.x 
(#27970)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/direct/DirectProducer.java     |  33 ++++--
 .../camel/component/kamelet/KameletProducer.java   |  28 ++++-
 .../KameletProducerSuspendedConsumerTest.java      | 123 +++++++++++++++++++++
 .../DirectProducerSuspendedConsumerTest.java       | 119 ++++++++++++++++++++
 4 files changed, 287 insertions(+), 16 deletions(-)

diff --git 
a/components/camel-direct/src/main/java/org/apache/camel/component/direct/DirectProducer.java
 
b/components/camel-direct/src/main/java/org/apache/camel/component/direct/DirectProducer.java
index 721e86b0eca8..0300cb8ff1e8 100644
--- 
a/components/camel-direct/src/main/java/org/apache/camel/component/direct/DirectProducer.java
+++ 
b/components/camel-direct/src/main/java/org/apache/camel/component/direct/DirectProducer.java
@@ -30,8 +30,9 @@ public class DirectProducer extends DefaultAsyncProducer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DirectProducer.class);
 
-    private volatile DirectConsumer consumer;
-    private int stateCounter;
+    // the consumer and the state counter of the component when the consumer 
was looked up, kept together as several
+    // threads can send with this producer at the same time
+    private volatile CachedConsumer cachedConsumer;
 
     private final DirectEndpoint endpoint;
     private final DirectComponent component;
@@ -50,10 +51,7 @@ public class DirectProducer extends DefaultAsyncProducer {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        if (consumer == null || stateCounter != component.getStateCounter()) {
-            stateCounter = component.getStateCounter();
-            consumer = component.getConsumer(key, block, timeout);
-        }
+        DirectConsumer consumer = getConsumer();
         if (consumer == null) {
             if (endpoint.isFailIfNoConsumers()) {
                 throw new DirectConsumerNotAvailableException("No consumers 
available on endpoint: " + endpoint, exchange);
@@ -74,10 +72,7 @@ public class DirectProducer extends DefaultAsyncProducer {
                 callback.done(true);
                 return true;
             }
-            if (consumer == null || stateCounter != 
component.getStateCounter()) {
-                stateCounter = component.getStateCounter();
-                consumer = component.getConsumer(key, block, timeout);
-            }
+            DirectConsumer consumer = getConsumer();
             if (consumer == null) {
                 if (endpoint.isFailIfNoConsumers()) {
                     exchange.setException(new 
DirectConsumerNotAvailableException(
@@ -115,4 +110,22 @@ public class DirectProducer extends DefaultAsyncProducer {
         }
     }
 
+    /**
+     * Gets the consumer, which is looked up again when it has been added or 
removed (such as when its route is
+     * suspended or stopped) since it was looked up last.
+     */
+    private DirectConsumer getConsumer() throws InterruptedException {
+        CachedConsumer cached = cachedConsumer;
+        // read the counter before the lookup, so a change during the lookup 
makes the next exchange look up again
+        int stateCounter = component.getStateCounter();
+        if (cached == null || cached.consumer() == null || 
cached.stateCounter() != stateCounter) {
+            DirectConsumer consumer = component.getConsumer(key, block, 
timeout);
+            cachedConsumer = new CachedConsumer(consumer, stateCounter);
+            return consumer;
+        }
+        return cached.consumer();
+    }
+
+    private record CachedConsumer(DirectConsumer consumer, int stateCounter) {
+    }
 }
diff --git 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletProducer.java
 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletProducer.java
index 1d219d524d34..3715297bc09f 100644
--- 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletProducer.java
+++ 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletProducer.java
@@ -31,8 +31,9 @@ final class KameletProducer extends DefaultAsyncProducer 
implements RouteIdAware
 
     private static final Logger LOG = 
LoggerFactory.getLogger(KameletProducer.class);
 
-    private volatile KameletConsumer consumer;
-    private int stateCounter;
+    // the consumer and the state counter of the component when the consumer 
was looked up, kept together as several
+    // threads can send with this producer at the same time
+    private volatile CachedConsumer cachedConsumer;
 
     private final KameletEndpoint endpoint;
     private final KameletComponent component;
@@ -56,10 +57,7 @@ final class KameletProducer extends DefaultAsyncProducer 
implements RouteIdAware
     @Override
     public boolean process(Exchange exchange, AsyncCallback callback) {
         try {
-            if (consumer == null || stateCounter != 
component.getStateCounter()) {
-                stateCounter = component.getStateCounter();
-                consumer = component.getConsumer(key, block, timeout);
-            }
+            final KameletConsumer consumer = getConsumer();
             if (consumer == null) {
                 if (endpoint.isFailIfNoConsumers()) {
                     exchange.setException(new 
KameletConsumerNotAvailableException(
@@ -148,4 +146,22 @@ final class KameletProducer extends DefaultAsyncProducer 
implements RouteIdAware
         }
     }
 
+    /**
+     * Gets the consumer, which is looked up again when it has been added or 
removed (such as when its route is
+     * suspended or stopped) since it was looked up last.
+     */
+    private KameletConsumer getConsumer() throws InterruptedException {
+        CachedConsumer cached = cachedConsumer;
+        // read the counter before the lookup, so a change during the lookup 
makes the next exchange look up again
+        int stateCounter = component.getStateCounter();
+        if (cached == null || cached.consumer() == null || 
cached.stateCounter() != stateCounter) {
+            KameletConsumer consumer = component.getConsumer(key, block, 
timeout);
+            cachedConsumer = new CachedConsumer(consumer, stateCounter);
+            return consumer;
+        }
+        return cached.consumer();
+    }
+
+    private record CachedConsumer(KameletConsumer consumer, int stateCounter) {
+    }
 }
diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletProducerSuspendedConsumerTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletProducerSuspendedConsumerTest.java
new file mode 100644
index 000000000000..866b3f161050
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletProducerSuspendedConsumerTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.kamelet;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * While one exchange waits for the consumer of a suspended or stopped kamelet 
route, other exchanges sent with the same
+ * producer must wait too, and not be sent to the suspended or stopped 
consumer.
+ */
+@Timeout(30)
+public class KameletProducerSuspendedConsumerTest extends CamelTestSupport {
+
+    // counted down by each exchange that looks up the consumer of the kamelet 
route
+    private final AtomicReference<CountDownLatch> lookups = new 
AtomicReference<>(new CountDownLatch(0));
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.addComponent("kamelet", new KameletComponent() {
+            @Override
+            protected KameletConsumer getConsumer(String key, boolean block, 
long timeout) throws InterruptedException {
+                lookups.get().countDown();
+                return super.getConsumer(key, block, timeout);
+            }
+        });
+        return context;
+    }
+
+    @Test
+    public void testExchangesWaitForSuspendedConsumer() throws Exception {
+        testExchangesWaitForConsumer(() -> 
context.getRouteController().suspendRoute("echo"),
+                () -> context.getRouteController().resumeRoute("echo"));
+    }
+
+    @Test
+    public void testExchangesWaitForStoppedConsumer() throws Exception {
+        testExchangesWaitForConsumer(() -> 
context.getRouteController().stopRoute("echo"),
+                () -> context.getRouteController().startRoute("echo"));
+    }
+
+    private void testExchangesWaitForConsumer(RouteAction removeConsumer, 
RouteAction addConsumer) throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:kamelet");
+        mock.expectedBodiesReceived("warm");
+        // the producer of the kamelet now caches the consumer of the kamelet 
route
+        template.sendBody("direct:start", "warm");
+        mock.assertIsSatisfied();
+
+        removeConsumer.run();
+
+        CountDownLatch firstWaiting = new CountDownLatch(1);
+        lookups.set(firstWaiting);
+        CompletableFuture<Object> first = 
template.asyncSendBody("direct:start", "first");
+        // the first exchange found that the consumer changed and waits for 
the consumer
+        assertThat(firstWaiting.await(10, TimeUnit.SECONDS)).isTrue();
+
+        CountDownLatch secondWaiting = new CountDownLatch(1);
+        lookups.set(secondWaiting);
+        CompletableFuture<Object> second = 
template.asyncSendBody("direct:start", "second");
+
+        // the second exchange must wait for the consumer too, and not reach 
the kamelet route while its consumer is gone
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
secondWaiting.getCount() == 0 || second.isDone());
+        assertThat(mock.getReceivedCounter()).as("No exchange should reach the 
kamelet route while its consumer is gone")
+                .isEqualTo(1);
+        assertThat(second).as("The second exchange should wait for the 
consumer of the kamelet route").isNotDone();
+
+        mock.reset();
+        mock.expectedBodiesReceivedInAnyOrder("first", "second");
+        addConsumer.run();
+
+        first.get(10, TimeUnit.SECONDS);
+        second.get(10, TimeUnit.SECONDS);
+        mock.assertIsSatisfied();
+    }
+
+    @FunctionalInterface
+    private interface RouteAction {
+        void run() throws Exception;
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("echo")
+                        .from("kamelet:source")
+                        .to("mock:kamelet");
+
+                from("direct:start")
+                        .to("kamelet:echo/echo?timeout=20000");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/direct/DirectProducerSuspendedConsumerTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/direct/DirectProducerSuspendedConsumerTest.java
new file mode 100644
index 000000000000..b49b4c02c83e
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/direct/DirectProducerSuspendedConsumerTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.direct;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * While one exchange waits for the consumer of a suspended or stopped direct 
route, other exchanges sent with the same
+ * producer must wait too, and not be sent to the suspended or stopped 
consumer.
+ */
+@Timeout(30)
+public class DirectProducerSuspendedConsumerTest extends ContextTestSupport {
+
+    // counted down by each exchange that looks up the consumer of the 
suspended route
+    private final AtomicReference<CountDownLatch> lookups = new 
AtomicReference<>(new CountDownLatch(0));
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.addComponent("direct", new DirectComponent() {
+            @Override
+            protected DirectConsumer getConsumer(String key, boolean block, 
long timeout) throws InterruptedException {
+                lookups.get().countDown();
+                return super.getConsumer(key, block, timeout);
+            }
+        });
+        return context;
+    }
+
+    @Test
+    public void testExchangesWaitForSuspendedConsumer() throws Exception {
+        testExchangesWaitForConsumer(() -> 
context.getRouteController().suspendRoute("b"),
+                () -> context.getRouteController().resumeRoute("b"));
+    }
+
+    @Test
+    public void testExchangesWaitForStoppedConsumer() throws Exception {
+        testExchangesWaitForConsumer(() -> 
context.getRouteController().stopRoute("b"),
+                () -> context.getRouteController().startRoute("b"));
+    }
+
+    private void testExchangesWaitForConsumer(RouteAction removeConsumer, 
RouteAction addConsumer) throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:b");
+        mock.expectedBodiesReceived("warm");
+        // the producer of direct:b now caches the consumer of route b
+        template.sendBody("direct:start", "warm");
+        mock.assertIsSatisfied();
+
+        removeConsumer.run();
+
+        CountDownLatch firstWaiting = new CountDownLatch(1);
+        lookups.set(firstWaiting);
+        CompletableFuture<Object> first = 
template.asyncSendBody("direct:start", "first");
+        // the first exchange found that the consumer changed and waits for 
the consumer
+        assertTrue(firstWaiting.await(10, TimeUnit.SECONDS));
+
+        CountDownLatch secondWaiting = new CountDownLatch(1);
+        lookups.set(secondWaiting);
+        CompletableFuture<Object> second = 
template.asyncSendBody("direct:start", "second");
+
+        // the second exchange must wait for the consumer too, and not reach 
route b while its consumer is gone
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
secondWaiting.getCount() == 0 || second.isDone());
+        assertEquals(1, mock.getReceivedCounter(), "No exchange should reach 
route b while its consumer is gone");
+        assertFalse(second.isDone(), "The second exchange should wait for the 
consumer of route b");
+
+        mock.reset();
+        mock.expectedBodiesReceivedInAnyOrder("first", "second");
+        addConsumer.run();
+
+        first.get(10, TimeUnit.SECONDS);
+        second.get(10, TimeUnit.SECONDS);
+        mock.assertIsSatisfied();
+    }
+
+    @FunctionalInterface
+    private interface RouteAction {
+        void run() throws Exception;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("direct:start").to("direct:b?timeout=20000");
+
+                from("direct:b").routeId("b").to("mock:b");
+            }
+        };
+    }
+}

Reply via email to