shashank created CAMEL-26462:
--------------------------------

             Summary: camel-nats - a consumer that stops while its task 
subscribes can leave its dispatcher (and its subscription) open on a shared 
connection
                 Key: CAMEL-26462
                 URL: https://issues.apache.org/jira/browse/CAMEL-26462
             Project: Camel
          Issue Type: Bug
          Components: camel-nats
            Reporter: shashank
            Assignee: shashank


Follow-up of CAMEL-26366 and CAMEL-25847, found while working on the flaky 
tests of CAMEL-26366.

The JetStream push consumer subscribes on its own executor 
({{NatsConsumingTask}}), after {{doStart}} returned. Since CAMEL-25847 
{{doStop}} ends the push subscription through the dispatcher of the consumer 
and closes that dispatcher (once no message is in flight), which matters when 
the connection is shared ({{connection=#conn}}) and stays open. But {{doStop}} 
only sees the dispatcher once the task has set it ({{NatsConsumer.dispatcher}}, 
which is not volatile either). When the consumer stops after the task created 
the dispatcher and before it set it, {{doStop}} finds nothing to close, shuts 
the executor down (which interrupts the task) and returns. The task then goes 
on:

* the push subscribe asks the server first (jnats 
{{NatsJetStreamImpl.makeRequestResponseRequired}}), so on the interrupted 
thread it fails with an {{IOException}}; the task logs it at DEBUG (the 
consumer is stopping) and the dispatcher it created stays open, with its 
thread, on the shared connection, until the connection closes. Each such stop 
leaks one.
* if the subscribe is made anyway (the interrupt of {{doStop}} comes after it: 
{{doStop}} reads the dispatcher before it shuts the executor down), the task 
sees {{isRunAllowed() == false}} and stops the keeper of CAMEL-25845 
({noformat}stopped while subscribing: doStop did not see the keeper{noformat}), 
but not the dispatcher: the subscription stays active and the stopped route 
keeps getting messages from the shared connection; the task also marks the 
consumer active (which matters for a first run only).

The consumer without JetStream ({{setupStandardNatsConsumer}}) has the same 
window: its subscribe sends no request, so it is made after such a stop, and 
its dispatcher gets the messages of the subject from the shared connection.

*Proposal*
* Right after the task set the dispatcher of its run, and before it subscribes, 
check the state of the consumer; when it is stopping, close that dispatcher (no 
subscription yet) and return. With the field {{NatsConsumer.dispatcher}} 
volatile, the task sees the stop, or {{doStop}} sees the dispatcher and closes 
it on the stopping thread (a subscribe on the closed dispatcher then fails), or 
both; in that last case {{doStop}} must not report the failed unsubscribe of 
the closed dispatcher ({{Dispatcher.isActive()}} is false). The same in the 
consumer without JetStream.
* Do not close the dispatcher from the task after its subscribe: {{doStop}} 
interrupts the task, and jnats drops the UNSUB that a close queues on an 
interrupted thread ({{WriterMessageQueue.push}}), so the server would keep the 
subscription.
* Tests in {{NatsConsumerJetStreamPushStopTest}} (fake server, no container, 
faithful to jnats on an interrupted thread): the consumer stops right after its 
task created the dispatcher, for the push consumer and a consumer without 
JetStream, and a stop that waits in its flush until the task closed its 
dispatcher; the stopped run does not subscribe, its dispatcher is closed, 
nothing is reported to the exception handler.

Not part of this: a JetStream pull subscription made after such a stop gets no 
messages (the fetch loop does not run), but is not unsubscribed; and a quick 
stop then start, where the task of the stopped run sees the consumer running 
again (that needs a token per run).

_Filed with Claude Code on behalf of allthingssecurity._



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

Reply via email to