shashank created CAMEL-25712:
--------------------------------

             Summary: camel-google-pubsub - with synchronousPull, a failed nack 
of a message that reached maxDeliveryAttempts ends the pull loop: the consumer 
never receives again while the route stays started
                 Key: CAMEL-25712
                 URL: https://issues.apache.org/jira/browse/CAMEL-25712
             Project: Camel
          Issue Type: Bug
          Components: camel-google-pubsub
            Reporter: shashank
            Assignee: shashank


With {{synchronousPull=true}} the consumer pulls in a loop on its own thread 
({{GooglePubsubConsumer.SubscriberWrapper.synchronousPull}}). Since CAMEL-23142 
a message whose delivery attempt has reached {{maxDeliveryAttempts}} is nacked 
without routing, so that Pub/Sub moves it to the dead-letter topic. The limit 
is the endpoint option, or, when the option is not set, the 
{{maxDeliveryAttempts}} of the dead-letter policy of the subscription, read at 
start.

The nack is a {{ModifyAckDeadline}} call with deadline 0 
({{AcknowledgeSync.nack}}), which wraps any failure in a 
{{RuntimeCamelException}}. At main 18f53efde6c7 the call 
({{GooglePubsubConsumer}} line 365) is inside the {{try}} of the loop, but that 
{{try}} only catches {{CancellationException}}, {{IOException}} and 
{{ExecutionException}}. A failed nack leaves {{synchronousPull}}, {{run()}} 
logs it at ERROR ("Failure getting messages from PubSub") and the consumer 
thread ends. Then:
* the route stays started and the consumer never pulls again; every message of 
the subscription waits until the route is restarted;
* the messages pulled in the same batch after the poison message are not routed 
(they are delivered again after their ack deadline, to nobody).

Trigger: {{synchronousPull=true}}, a subscription with a dead-letter policy (or 
{{maxDeliveryAttempts}} set), a message that keeps failing in the route until 
it reaches the limit (the case the option exists for), and a failure of that 
one {{ModifyAckDeadline}} call: Pub/Sub returning {{UNAVAILABLE}} for longer 
than the client's retry budget (with the default stub settings of 
google-cloud-pubsub 1.157.0, {{ModifyAckDeadline}} is retried only on 
{{UNAVAILABLE}}, within 60 s in total), any other error status, such as 
{{DEADLINE_EXCEEDED}}, which is not retried, or an expired ack id on a 
subscription with exactly-once delivery (the batch is not extended in 
synchronous pull mode, so an ack id can expire while the earlier messages of 
the batch are routed). With the default {{concurrentConsumers=1}} one failure 
stops all consumption.

h3. Reproduction

New {{GooglePubsubConsumerNackFailureTest}} (Mockito {{SubscriberStub}}, no 
emulator): the first pull returns a message at the limit 
({{deliveryAttempt=3}}, {{maxDeliveryAttempts=3}}) and its 
{{ModifyAckDeadline}} fails with {{UnavailableException}}; the second pull 
returns a normal message. On main: {{mock://result Received message count. 
Expected: <1> but was: <0>}}, and the log shows {{ERROR GooglePubsubConsumer - 
Failure getting messages from PubSub}} with {{RuntimeCamelException: 
com.google.api.gax.rpc.UnavailableException}} at {{AcknowledgeSync.nack}}; 
nothing is pulled after it.

h3. Proposed fix

Catch the failure of that nack, report it through the consumer's 
{{ExceptionHandler}} (WARN by default, or the route's error handler with 
{{bridgeErrorHandler}}), and go on with the next message. The message is 
delivered again when its ack deadline expires and is nacked then; Pub/Sub's own 
dead-letter forwarding is not affected. No option or default changes, so no 
upgrade guide entry.

camel-google-pubsub tests with the fix: 17 run, 0 failures, 0 errors, 10 
skipped (the skipped ones need the Pub/Sub emulator container). Not run: the 
integration tests with the emulator, a real outage.

How it was found: a sweep of consumers that run their own loop instead of a 
scheduled poll, looking for calls whose exception leaves the loop; the test 
confirmed it.

Not changed: an {{ExecutionException}} of the pull whose cause is not retryable 
still ends the loop on purpose (the retryable codes can be widened with the 
component option {{synchronousPullRetryableCodes}}), as the asynchronous 
subscriber does for non-retryable errors.

Affected: main (18f53efde6c7; tested). camel-4.23.x and camel-4.22.x (line 365) 
and camel-4.18.x (line 358) have the same call outside any matching catch 
(read, not run). camel-4.14.x has no maxDeliveryAttempts enforcement.

Duplicate check (2026-10-16): JIRA text "google-pubsub synchronous pull" 
(CAMEL-17185 server error, 2021; CAMEL-17871 graceful shutdown), "pubsub 
maxDeliveryAttempts" (CAMEL-23142), "pubsub nack" (CAMEL-22947 ack/nack 
callbacks), "AcknowledgeSync" (CAMEL-15252, CAMEL-24350, producer), 
"synchronousPull nack" (none); GitHub pull requests "pubsub synchronous", 
"pubsub maxDeliveryAttempts", "AcknowledgeSync": nothing about this path.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to