shashank created CAMEL-25288:
--------------------------------
Summary: camel-dapr - the consumers close the configured or
autowired Dapr clients when they stop, and the configuration consumer fails to
start in a route with a NullPointerException
Key: CAMEL-25288
URL: https://issues.apache.org/jira/browse/CAMEL-25288
Project: Camel
Issue Type: Bug
Components: camel-dapr
Reporter: shashank
The {{client}}, {{previewClient}} and {{workflowClient}} options are autowired
({{@Metadata(autowired = true)}}): a single {{DaprClient}} /
{{DaprPreviewClient}} bean in the registry (for example the beans of the Dapr
Spring Boot starter) is used by every dapr endpoint. The consumers close
clients that they do not own:
h3. 1. DaprPubSubConsumer closes a configured or autowired preview client
{code:java}
protected void doStop() throws Exception {
if (subscription != null) {
subscription.close();
}
if (client != null) {
client.close(); // also the configured / autowired previewClient
}
...
{code}
After the route is stopped (or restarted), every other dapr endpoint using that
client fails (the gRPC channel is shut down), and the route itself cannot
subscribe again: a route restart creates a new consumer, which gets the same
closed client from the configuration.
h3. 2. DaprConfigurationConsumer closes the client of its endpoint, and never
ends its subscription
{code:java}
protected void doStop() throws Exception {
if (client != null) {
client.unsubscribeConfiguration(subscriptionId, configStore); // a
Mono that is never subscribed: does nothing
client.close(); // the
endpoint's client
}
...
{code}
The client is {{endpoint.getClient()}}: the configured / autowired
{{DaprClient}} shared by all dapr endpoints, or the client that the endpoint
created for all its producers and consumers. The {{Flux}} subscription is never
disposed and the unsubscribe {{Mono}} is never subscribed, so the subscription
is not ended at Dapr when the client is not closed. The subscription id was
only recorded from a response without items.
h3. 3. DaprConfigurationConsumer cannot start in a route
The consumer takes {{endpoint.getClient()}} in its constructor, but the
endpoint creates (or takes the configured) client in {{doStart}}, and a route
creates its consumer before it starts the endpoint. In a route the consumer has
no client and the route fails to start:
{noformat}
FailedToStartRoute Failed to start route: configuration because: Cannot invoke
"io.dapr.client.DaprClient.subscribeConfiguration(io.dapr.client.domain.SubscribeConfigurationRequest)"
because "this.client" is null
{noformat}
The existing unit test builds the consumer with a mocked endpoint whose client
is already set, so it did not show this.
h3. Reproduction (unit tests, no Dapr sidecar)
{{DaprConsumerSharedClientTest}}: a {{CamelTestSupport}} test with mocked
{{DaprClient}}, {{DaprPreviewClient}} and {{DaprWorkflowClient}} beans in the
registry (autowired), a {{dapr:pubSub}} route and a {{dapr:configuration}}
route.
* Stop and start the pub/sub route: on main {{previewClient.close()}} is called
({{NeverWantedButInvoked: daprPreviewClient.close()}}).
* Start the configuration route: on main it fails with the
{{NullPointerException}} above. With only the client taken in {{doStart}},
stopping it does not end the subscription ({{expected: <true> but was:
<false>}}), does not unsubscribe and closes the shared client.
* The existing {{DaprPubSubConsumerTest}} / {{DaprConfigurationConsumerTest}}
asserted {{close()}} on the configured / endpoint client; they now assert that
it is not closed (and that the configuration consumer unsubscribes with the
subscription id).
h3. Proposed fix
Ownership as in the other components (for example the AWS components close only
the clients that they created):
* {{DaprPubSubConsumer}} closes only a preview client that it created (none
configured); then a restart creates a new one.
* {{DaprConfigurationConsumer}} takes the endpoint's client in {{doStart}},
records the subscription id from every response, and on stop disposes its
subscription and unsubscribes (blocking up to 10 seconds, a failure is logged),
without closing the client. Both are needed with Dapr Java SDK 1.18.1: its
{{Flux}} does not cancel the gRPC stream when it is disposed, so the explicit
{{unsubscribeConfiguration}} is what ends the subscription at the sidecar (the
sidecar logs that this call is deprecated in favour of disconnecting).
* {{DaprEndpoint}} closes the {{DaprClient}}, {{DaprPreviewClient}} and
{{DaprWorkflowClient}} that it created in {{doStart}} when it stops (as
{{Sqs2Endpoint}} does), never the configured or autowired ones; it creates new
ones when it is started again.
No upgrade note: a configured or autowired client was closed by Camel and now
stays open for its owner (Spring or the application), as for the other
components.
{{DaprEndpointClientOwnershipTest}} (2): the endpoint closes the clients it
created (Mockito {{mockConstruction}} of {{DaprClientBuilder}} /
{{DaprWorkflowClient}}; fails without the endpoint change: {{Wanted but not
invoked: daprClient.close()}}) and never the configured ones, which it uses
again after a restart.
With the fix the camel-dapr unit tests pass (56).
Affected: 4.14.x, 4.18.x and main (same code).
Duplicate check (2026-10-03): JIRA component camel-dapr / text "dapr" (9
issues): CAMEL-25274 (ours, acknowledgement), CAMEL-23645 (open, move to the
new Topic API), feature issues. GitHub pull requests
"DaprConfigurationConsumer", "camel-dapr close client": none.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)