[ 
https://issues.apache.org/jira/browse/CAMEL-25847?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

shashank updated CAMEL-25847:
-----------------------------
    Description: 
With {{jetstreamEnabled=true}} and {{pullSubscription=false}} the consumer 
subscribes with a JetStream push subscription that belongs to a dispatcher it 
creates ({{NatsConsumer.setupJetStreamConsumer}}, main 20cdb309f7d0, lines 
240-262). {{doStop}} (lines 93-133) then does two things that do not end that 
subscription:

* {{dispatcher.unsubscribe(configuration.getTopic())}}: a push subscription is 
on its deliver subject (an inbox), not on the topic, so nothing is unsubscribed;
* {{jetStreamSubscription.unsubscribe()}}: jnats refuses it for a subscription 
that belongs to a dispatcher ({{IllegalStateException}} "Subscriptions that 
belong to a dispatcher cannot respond to unsubscribe directly."), which only 
ends in a WARN "Error during unsubscribing".

The dispatcher is never closed. When the consumer owns its connection the 
connection is closed right after, which ends everything. With a shared 
connection (the {{connection}} endpoint option, an already instantiated 
{{Connection}}), the connection stays open and the subscription stays active:

* while the route is stopped, messages keep being delivered to the stopped 
consumer; the exchanges fail and are nak'ed, and the server delivers them again 
at once (a busy loop for as long as the route is stopped; with {{maxDeliver}} 
set the messages are then dropped, with {{ackPolicy=None}} they are 
acknowledged and lost);
* when the route is started again, a second subscription is made, and every 
message is routed twice for as long as the application runs (with an ephemeral 
consumer both consumers get every message).

In plain NATS mode with a shared connection the topic is unsubscribed, but the 
dispatcher (one thread) of every stopped consumer is left behind.

*Fix*: {{doStop}} ends the push subscription through its dispatcher 
({{dispatcher.unsubscribe(jetStreamSubscription)}}, which jnats accepts for a 
subscription of that dispatcher and which does not touch the dispatcher 
thread), and closes the dispatcher the consumer created 
({{connection.closeDispatcher}}, while the connection is open), which ends its 
thread, once no message is in flight on it: jnats closes a dispatcher by 
cancelling its thread with an interrupt ({{NatsDispatcher.stop(true)}}, 
{{Future.cancel(true)}}), and that thread runs the route, which Camel stops 
before it waits for the exchanges in flight. When a message is in flight at the 
stop, the dispatcher is closed when it is done. The existing unsubscribe calls 
are kept. No option changes.

*Tests*: new {{NatsConsumerJetStreamPushStopTest}} with a fake JetStream server 
behind the jnats interfaces the consumer calls, which, like jnats, refuses 
{{unsubscribe()}} on a subscription of a dispatcher and ends the subscriptions 
of a closed dispatcher. 
{{stoppedRouteGetsNoMessagesAndRestartedRouteGetsEachMessageOnce}} (shared 
connection, push, stop and start the route, publish one message) fails on main 
with {{mock://result Received message count. Expected: <1> but was: <2>}}, and 
passes with the fix (no subscription left after the stop, one after the start). 
{{stoppingTheRouteDoesNotInterruptTheExchangeInFlight}} stops the route while a 
step waits; with the dispatcher closed in {{doStop}} the step is interrupted 
({{expected: <false> but was: <true>}}), with the fix it completes and the 
dispatcher is closed afterwards (the fake interrupts the delivering thread on 
{{closeDispatcher}}, as jnats does). A probe on main with the same fake, 
publishing while the route is stopped, saw the stopped route fail and nak the 
message over a million times in 2 seconds. camel-nats unit tests with the fix: 
7 run, 0 failures; merged with the CAMEL-25845 fix: 19 run, 0 failures.

*How it was found*: while writing the CAMEL-25845 fix, reading the jnats 
classes (2.26.3, the version of Camel main) ({{NatsSubscription.unsubscribe}}), 
then a JUnit test with a fake JetStream server. Not run against a real NATS 
server.

*Affected*: main, camel-4.23.x, camel-4.22.x and camel-4.18.x (the same 
{{doStop}}; read, not run).

_Filed with Claude Code on behalf of allthingssecurity._


  was:
With {{jetstreamEnabled=true}} and {{pullSubscription=false}} the consumer 
subscribes with a JetStream push subscription that belongs to a dispatcher it 
creates ({{NatsConsumer.setupJetStreamConsumer}}, main 20cdb309f7d0, lines 
240-262). {{doStop}} (lines 93-133) then does two things that do not end that 
subscription:

* {{dispatcher.unsubscribe(configuration.getTopic())}}: a push subscription is 
on its deliver subject (an inbox), not on the topic, so nothing is unsubscribed;
* {{jetStreamSubscription.unsubscribe()}}: jnats refuses it for a subscription 
that belongs to a dispatcher ({{IllegalStateException}} "Subscriptions that 
belong to a dispatcher cannot respond to unsubscribe directly."), which only 
ends in a WARN "Error during unsubscribing".

The dispatcher is never closed. When the consumer owns its connection the 
connection is closed right after, which ends everything. With a shared 
connection (the {{connection}} endpoint option, an already instantiated 
{{Connection}}), the connection stays open and the subscription stays active:

* while the route is stopped, messages keep being delivered to the stopped 
consumer; the exchanges fail and are nak'ed, and the server delivers them again 
at once (a busy loop for as long as the route is stopped; with {{maxDeliver}} 
set the messages are then dropped, with {{ackPolicy=None}} they are 
acknowledged and lost);
* when the route is started again, a second subscription is made, and every 
message is routed twice for as long as the application runs (with an ephemeral 
consumer both consumers get every message).

In plain NATS mode with a shared connection the topic is unsubscribed, but the 
dispatcher (one thread) of every stopped consumer is left behind.

*Fix*: {{doStop}} closes the dispatcher the consumer created 
({{connection.closeDispatcher}}) when the connection is not closed. That 
unsubscribes all its subscriptions, the JetStream push subscription included, 
and ends its thread. The existing unsubscribe calls are kept. No option changes.

*Tests*: new {{NatsConsumerJetStreamPushStopTest}} with a fake JetStream server 
behind the jnats interfaces the consumer calls, which, like jnats, refuses 
{{unsubscribe()}} on a subscription of a dispatcher and ends the subscriptions 
of a closed dispatcher. 
{{stoppedRouteGetsNoMessagesAndRestartedRouteGetsEachMessageOnce}} (shared 
connection, push, stop and start the route, publish one message) fails on main 
with {{mock://result Received message count. Expected: <1> but was: <2>}}, and 
passes with the fix (no subscription left after the stop, one after the start). 
A probe on main with the same fake, publishing while the route is stopped, saw 
the stopped route fail and nak the message over a million times in 2 seconds. 
camel-nats unit tests with the fix: 6 run, 0 failures; merged with the 
CAMEL-25815 and CAMEL-25845 fixes: 11 run, 0 failures.

*How it was found*: while writing the CAMEL-25845 fix, reading the jnats 2.26.4 
classes ({{NatsSubscription.unsubscribe}}), then a JUnit test with a fake 
JetStream server. Not run against a real NATS server.

*Affected*: main, camel-4.23.x, camel-4.22.x and camel-4.18.x (the same 
{{doStop}}; read, not run).

_Filed with Claude Code on behalf of allthingssecurity._



> camel-nats - a JetStream push consumer on a shared connection is not 
> unsubscribed when the route stops: the stopped route keeps getting messages 
> and after a restart every message is routed twice
> --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25847
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25847
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-nats
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>
> With {{jetstreamEnabled=true}} and {{pullSubscription=false}} the consumer 
> subscribes with a JetStream push subscription that belongs to a dispatcher it 
> creates ({{NatsConsumer.setupJetStreamConsumer}}, main 20cdb309f7d0, lines 
> 240-262). {{doStop}} (lines 93-133) then does two things that do not end that 
> subscription:
> * {{dispatcher.unsubscribe(configuration.getTopic())}}: a push subscription 
> is on its deliver subject (an inbox), not on the topic, so nothing is 
> unsubscribed;
> * {{jetStreamSubscription.unsubscribe()}}: jnats refuses it for a 
> subscription that belongs to a dispatcher ({{IllegalStateException}} 
> "Subscriptions that belong to a dispatcher cannot respond to unsubscribe 
> directly."), which only ends in a WARN "Error during unsubscribing".
> The dispatcher is never closed. When the consumer owns its connection the 
> connection is closed right after, which ends everything. With a shared 
> connection (the {{connection}} endpoint option, an already instantiated 
> {{Connection}}), the connection stays open and the subscription stays active:
> * while the route is stopped, messages keep being delivered to the stopped 
> consumer; the exchanges fail and are nak'ed, and the server delivers them 
> again at once (a busy loop for as long as the route is stopped; with 
> {{maxDeliver}} set the messages are then dropped, with {{ackPolicy=None}} 
> they are acknowledged and lost);
> * when the route is started again, a second subscription is made, and every 
> message is routed twice for as long as the application runs (with an 
> ephemeral consumer both consumers get every message).
> In plain NATS mode with a shared connection the topic is unsubscribed, but 
> the dispatcher (one thread) of every stopped consumer is left behind.
> *Fix*: {{doStop}} ends the push subscription through its dispatcher 
> ({{dispatcher.unsubscribe(jetStreamSubscription)}}, which jnats accepts for a 
> subscription of that dispatcher and which does not touch the dispatcher 
> thread), and closes the dispatcher the consumer created 
> ({{connection.closeDispatcher}}, while the connection is open), which ends 
> its thread, once no message is in flight on it: jnats closes a dispatcher by 
> cancelling its thread with an interrupt ({{NatsDispatcher.stop(true)}}, 
> {{Future.cancel(true)}}), and that thread runs the route, which Camel stops 
> before it waits for the exchanges in flight. When a message is in flight at 
> the stop, the dispatcher is closed when it is done. The existing unsubscribe 
> calls are kept. No option changes.
> *Tests*: new {{NatsConsumerJetStreamPushStopTest}} with a fake JetStream 
> server behind the jnats interfaces the consumer calls, which, like jnats, 
> refuses {{unsubscribe()}} on a subscription of a dispatcher and ends the 
> subscriptions of a closed dispatcher. 
> {{stoppedRouteGetsNoMessagesAndRestartedRouteGetsEachMessageOnce}} (shared 
> connection, push, stop and start the route, publish one message) fails on 
> main with {{mock://result Received message count. Expected: <1> but was: 
> <2>}}, and passes with the fix (no subscription left after the stop, one 
> after the start). {{stoppingTheRouteDoesNotInterruptTheExchangeInFlight}} 
> stops the route while a step waits; with the dispatcher closed in {{doStop}} 
> the step is interrupted ({{expected: <false> but was: <true>}}), with the fix 
> it completes and the dispatcher is closed afterwards (the fake interrupts the 
> delivering thread on {{closeDispatcher}}, as jnats does). A probe on main 
> with the same fake, publishing while the route is stopped, saw the stopped 
> route fail and nak the message over a million times in 2 seconds. camel-nats 
> unit tests with the fix: 7 run, 0 failures; merged with the CAMEL-25845 fix: 
> 19 run, 0 failures.
> *How it was found*: while writing the CAMEL-25845 fix, reading the jnats 
> classes (2.26.3, the version of Camel main) 
> ({{NatsSubscription.unsubscribe}}), then a JUnit test with a fake JetStream 
> server. Not run against a real NATS server.
> *Affected*: main, camel-4.23.x, camel-4.22.x and camel-4.18.x (the same 
> {{doStop}}; read, not run).
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to