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 3b0c32f0de7c CAMEL-25844: camel-dapr - subscribe again to 
configuration changes when the stream ends (#28317)
3b0c32f0de7c is described below

commit 3b0c32f0de7cf27797d42b1a5af1ac2a60903f7b
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 23 13:52:06 2026 +0530

    CAMEL-25844: camel-dapr - subscribe again to configuration changes when the 
stream ends (#28317)
    
    The Dapr configuration consumer subscribed once to the configuration stream 
and stopped receiving changes when the stream failed or completed. It now 
subscribes again, after one second and then with a growing delay of up to 30 
seconds, while the consumer runs.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/docs/dapr-component.adoc  |  2 +
 .../camel-dapr/src/main/docs/dapr-component.adoc   |  2 +
 .../dapr/consumer/DaprConfigurationConsumer.java   | 24 +++++++++--
 .../consumer/DaprConfigurationConsumerTest.java    | 49 ++++++++++++++++++++++
 4 files changed, 74 insertions(+), 3 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dapr-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dapr-component.adoc
index c43f3015a9cb..3f11fcc534fb 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dapr-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dapr-component.adoc
@@ -105,6 +105,8 @@ The Dapr Configuration Consumer in Apache Camel allows your 
route to receive rea
 
 To configure the consumer, the configStore name and configKeys must be 
provided.
 
+When the subscription ends, for example because the Dapr sidecar restarts, the 
consumer subscribes again, first after one second and then with a growing delay 
of up to 30 seconds while the sidecar cannot be reached. A change made to the 
configuration store while the consumer is not subscribed is not delivered.
+
 == Examples
 
 === Producer Operations Examples
diff --git a/components/camel-dapr/src/main/docs/dapr-component.adoc 
b/components/camel-dapr/src/main/docs/dapr-component.adoc
index c43f3015a9cb..3f11fcc534fb 100644
--- a/components/camel-dapr/src/main/docs/dapr-component.adoc
+++ b/components/camel-dapr/src/main/docs/dapr-component.adoc
@@ -105,6 +105,8 @@ The Dapr Configuration Consumer in Apache Camel allows your 
route to receive rea
 
 To configure the consumer, the configStore name and configKeys must be 
provided.
 
+When the subscription ends, for example because the Dapr sidecar restarts, the 
consumer subscribes again, first after one second and then with a growing delay 
of up to 30 seconds while the sidecar cannot be reached. A change made to the 
configuration store while the consumer is not subscribed is not delivered.
+
 == Examples
 
 === Producer Operations Examples
diff --git 
a/components/camel-dapr/src/main/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumer.java
 
b/components/camel-dapr/src/main/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumer.java
index 02da12ece5bb..23038a41ed3f 100644
--- 
a/components/camel-dapr/src/main/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumer.java
+++ 
b/components/camel-dapr/src/main/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumer.java
@@ -36,11 +36,14 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import reactor.core.Disposable;
 import reactor.core.publisher.Flux;
+import reactor.util.retry.Retry;
 
 public class DaprConfigurationConsumer extends DefaultConsumer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DaprConfigurationConsumer.class);
     private static final Duration UNSUBSCRIBE_TIMEOUT = Duration.ofSeconds(10);
+    private static final Duration RESUBSCRIBE_DELAY = Duration.ofSeconds(1);
+    private static final Duration MAX_RESUBSCRIBE_DELAY = 
Duration.ofSeconds(30);
     private final String configStore;
     private final List<String> configKeys;
     private DaprClient client;
@@ -74,10 +77,25 @@ public class DaprConfigurationConsumer extends 
DefaultConsumer {
         client = getEndpoint().getClient();
 
         SubscribeConfigurationRequest configRequest = new 
SubscribeConfigurationRequest(configStore, configKeys);
-        Flux<SubscribeConfigurationResponse> responses = 
client.subscribeConfiguration(configRequest);
+        // the stream ends when the call to the sidecar fails or is closed 
(for example when the sidecar restarts), and
+        // the Dapr client does not open it again: subscribe again (after a 
delay) for as long as the consumer runs, else
+        // the consumer stays started and receives no further configuration 
changes
+        Flux<SubscribeConfigurationResponse> responses = Flux.defer(() -> 
client.subscribeConfiguration(configRequest))
+                .doOnComplete(() -> LOG.warn(
+                        "The subscription to configuration changes of store {} 
ended. Subscribing again in {} ms",
+                        configStore, RESUBSCRIBE_DELAY.toMillis()))
+                .repeatWhen(completed -> 
completed.delayElements(RESUBSCRIBE_DELAY))
+                .retryWhen(Retry.backoff(Long.MAX_VALUE, 
RESUBSCRIBE_DELAY).maxBackoff(MAX_RESUBSCRIBE_DELAY)
+                        .transientErrors(true)
+                        .doBeforeRetry(signal -> {
+                            LOG.warn("The subscription to configuration 
changes of store {} failed: {}. Subscribing again",
+                                    configStore, 
signal.failure().getMessage());
+                            LOG.debug("The subscription to configuration 
changes of store {} failed", configStore,
+                                    signal.failure());
+                        }));
 
         subscription = responses.subscribe((response) -> {
-            // every response contains the subscription id, the first ever 
response has no items
+            // every response contains the subscription id, the first response 
of a subscription has no items
             subscriptionId = response.getSubscriptionId();
             if (response.getItems() == null || response.getItems().isEmpty()) {
                 LOG.debug("App subscribed to config changes with subscription 
id: {}", subscriptionId);
@@ -88,7 +106,7 @@ public class DaprConfigurationConsumer extends 
DefaultConsumer {
                 AsyncCallback cb = defaultConsumerCallback(exchange, true);
                 getAsyncProcessor().process(exchange, cb);
             }
-        });
+        }, error -> getExceptionHandler().handleException("Error subscribing 
to configuration changes", error));
     }
 
     @Override
diff --git 
a/components/camel-dapr/src/test/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumerTest.java
 
b/components/camel-dapr/src/test/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumerTest.java
index 5070c41765d7..224db04fe53f 100644
--- 
a/components/camel-dapr/src/test/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumerTest.java
+++ 
b/components/camel-dapr/src/test/java/org/apache/camel/component/dapr/consumer/DaprConfigurationConsumerTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.dapr.consumer;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import io.dapr.client.DaprClient;
 import io.dapr.client.domain.ConfigurationItem;
@@ -49,6 +50,8 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -118,4 +121,50 @@ public class DaprConfigurationConsumerTest extends 
CamelTestSupport {
         // the client belongs to the endpoint: it is not closed
         verify(mockClient, never()).close();
     }
+
+    @Test
+    void testConfigurationConsumerSubscribesAgainWhenTheStreamFails() throws 
Exception {
+        // the first stream ends with an error, as when the Dapr sidecar 
restarts; the second one delivers an update
+        AtomicInteger calls = new AtomicInteger();
+        when(mockClient.subscribeConfiguration(any())).thenAnswer(inv -> 
calls.incrementAndGet() == 1
+                ? Flux.just(new SubscribeConfigurationResponse("mySubId", 
Map.of()))
+                        .concatWith(Flux.error(new 
IllegalStateException("UNAVAILABLE: sidecar restarted")))
+                : Flux.just(new SubscribeConfigurationResponse("mySubId2", 
Map.of()), update("myVal2", "mySubId2"))
+                        .concatWith(Flux.never()));
+        when(mockClient.unsubscribeConfiguration(anyString(), anyString()))
+                .thenReturn(Mono.just(new 
UnsubscribeConfigurationResponse(true, "")));
+
+        consumer.doStart();
+
+        verify(mockClient, 
timeout(10000).times(2)).subscribeConfiguration(any(SubscribeConfigurationRequest.class));
+        verify(processor, timeout(10000)).process(exchangeCaptor.capture(), 
callbackCaptor.capture());
+        assertEquals(Map.of("myKey1", "myVal2"), 
exchangeCaptor.getValue().getIn().getBody());
+
+        consumer.doStop();
+        verify(mockClient).unsubscribeConfiguration("mySubId2", "myStore");
+    }
+
+    @Test
+    void testConfigurationConsumerSubscribesAgainWhenTheStreamCompletes() 
throws Exception {
+        // the first stream completes, as when the sidecar ends the call on 
shutdown
+        AtomicInteger calls = new AtomicInteger();
+        when(mockClient.subscribeConfiguration(any())).thenAnswer(inv -> 
calls.incrementAndGet() == 1
+                ? Flux.just(new SubscribeConfigurationResponse("mySubId", 
Map.of()))
+                : Flux.just(update("myVal3", 
"mySubId2")).concatWith(Flux.never()));
+        when(mockClient.unsubscribeConfiguration(anyString(), anyString()))
+                .thenReturn(Mono.just(new 
UnsubscribeConfigurationResponse(true, "")));
+
+        consumer.doStart();
+
+        verify(processor, timeout(10000)).process(exchangeCaptor.capture(), 
callbackCaptor.capture());
+        assertEquals(Map.of("myKey1", "myVal3"), 
exchangeCaptor.getValue().getIn().getBody());
+
+        consumer.doStop();
+        verify(mockClient, 
times(2)).subscribeConfiguration(any(SubscribeConfigurationRequest.class));
+    }
+
+    private static SubscribeConfigurationResponse update(String value, String 
subscriptionId) {
+        return new SubscribeConfigurationResponse(
+                subscriptionId, Map.of("myKey1", new 
ConfigurationItem("myKey1", value, "v")));
+    }
 }

Reply via email to