shashank created CAMEL-25790:
--------------------------------
Summary: camel-google-pubsub - the consumer subscribes or pulls
again during a graceful shutdown (deferShutdown), so the shutdown waits for its
timeout, and a route restart leaves the previous subscriber loops running
Key: CAMEL-25790
URL: https://issues.apache.org/jira/browse/CAMEL-25790
Project: Camel
Issue Type: Bug
Components: camel-google-pubsub
Reporter: shashank
Assignee: shashank
{{GooglePubsubConsumer}} (main 7aae27315946) runs one loop per concurrent
consumer ({{SubscriberWrapper}}): the asynchronous loop starts a {{Subscriber}}
and waits until it terminates, the synchronous loop pulls, routes the messages
and pulls again. Both loops continue while {{isRunAllowed() &&
!isSuspendingOrSuspended()}} (lines 253 and 323). That check misses two cases.
*1. Graceful shutdown.* Since CAMEL-23291 the consumer is {{ShutdownAware}}:
{{deferShutdown}} stops the subscribers ({{stopAsync}}) and cancels the pending
synchronous pulls, and the shutdown strategy then waits for the pending and
inflight exchanges before it stops the consumer. The consumer is still started
during that wait, so the loop sees {{isRunAllowed() == true}} and at once
starts a new subscriber (asynchronous pull) or issues a new pull (synchronous
pull, as soon as the cancelled {{get()}} returns). New messages keep being
routed while the strategy waits for the consumer to drain. With a steady flow
of messages the pending count does not reach zero: the graceful shutdown (route
stop, context stop) waits for its full timeout (45 s by default; longer than
the 30 s a Kubernetes pod gets by default) and then forces the shutdown, with
exchanges still in flight; their messages are delivered again (duplicates).
CAMEL-23291 added {{deferShutdown}} to avoid exactly that.
*2. Route restart.* {{doStop}} only calls {{stopAsync}} and returns; the
subscriber terminates later (streams closed, acks flushed). If the consumer is
started again before that ({{restartRoute}}, a route controller or a reload
that stops and starts the route), the old loop wakes up in a started consumer
and subscribes again, next to the loops of the new start. Each restart adds
{{concurrentConsumers}} subscriber loops (each with its streams and
flow-control budget), and the executors of the previous start are only shut
down in {{doShutdown}}. The loops stop only when the route stops for good.
No message is lost; the effects are a shutdown that does not drain (hang until
the timeout, forced shutdown, redelivered messages) and subscriber loops and
threads that grow with every restart.
*Fix*: each loop gets a generation; the consumer increments it when it starts,
in {{deferShutdown}} and in {{doStop}}, and a loop of an older generation ends
instead of subscribing or pulling again (the check after the subscriber start
and after the pull is issued uses the same condition). A start after a stop
also shuts down (not gracefully, {{ExecutorServiceManager.shutdown}}) the two
executors of the previous start; their loops end on their own. {{doShutdown}}
is unchanged.
*Tests*: new {{GooglePubsubConsumerStopPullingTest}} (3 tests, mocked
{{Subscriber}} / {{SubscriberStub}} as in
{{GooglePubsubConsumerStopRaceTest}}). On main all three fail:
{{asyncPullDoesNotSubscribeAgainAfterDeferShutdown}} ({{subscribers created
after the consumer stopped pulling ==> expected: <1> but was: <2>}}),
{{syncPullDoesNotPullAgainAfterDeferShutdown}} ({{pulls issued after the
consumer stopped pulling ==> expected: <1> but was: <2>}}),
{{asyncPullLoopOfPreviousStartEndsWhenConsumerIsRestarted}} ({{subscribers
created by a start and a restart with one concurrent consumer ==> expected: <2>
but was: <3>}}). With the fix they pass; camel-google-pubsub unit tests: 20
run, 0 failures, 10 skipped (need credentials or the emulator).
*How it was found*: mutation testing (PIT) showed the consumer's stop /
shutdown paths and the asynchronous pull ({{CamelMessageReceiver}}, the
subscriber loop) untested, then reading the code against the {{ShutdownAware}}
contract (CAMEL-23291: "deferShutdown() should stop the message pump while
keeping connections alive") and the shutdown strategy
({{DefaultShutdownStrategy}}: a deferred consumer stays started until the
pending exchanges are done), then the JUnit tests.
*Affected*: main, camel-4.23.x, camel-4.22.x and camel-4.18.x (since
CAMEL-23291, 4.18.3 / 4.21.0: same loop condition with {{deferShutdown}} and
the executor shut down only in {{doShutdown}}). camel-4.14.x has no
{{deferShutdown}} and shuts the executor down gracefully in {{doStop}}, which
waits for the loops to end (read, not run).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)