Federico Mariani created CAMEL-26647:
----------------------------------------

             Summary: camel-google-pubsub - manual ack of an aggregated 
exchange (synchronousPull) acks none of the grouped messages
                 Key: CAMEL-26647
                 URL: https://issues.apache.org/jira/browse/CAMEL-26647
             Project: Camel
          Issue Type: Bug
          Components: camel-google-pubsub
    Affects Versions: 4.18.5, 4.23.0, 4.22.2
            Reporter: Federico Mariani


h3. Problem
With {{synchronousPull=true}} and {{ackMode=NONE}} the consumer puts an 
{{AcknowledgeSync}} in the header 
{{GooglePubsubConstants.GOOGLE_PUBSUB_ACKNOWLEDGE}} 
({{components/camel-google/camel-google-pubsub/src/main/java/org/apache/camel/component/google/pubsub/GooglePubsubConsumer.java}}
 lines 415-421 on main, commit 7978db3d42dd). When the route acks (or nacks) an 
aggregated exchange whose body is a {{List<Exchange>}}, 
{{AcknowledgeSync.getAckIdList}} is meant to collect the ack id of every 
grouped exchange. Inside the loop it reads the header of the *outer* exchange 
instead 
({{components/camel-google/camel-google-pubsub/src/main/java/org/apache/camel/component/google/pubsub/consumer/AcknowledgeSync.java}}
 line 75):
{code:java}
for (Object body : exchange.getIn().getBody(List.class)) {
    if (body instanceof Exchange) {
        String ackId = exchange.getIn().getHeader(GooglePubsubConstants.ACK_ID, 
String.class); // should be the element's
{code}
{{GroupedExchangeAggregationStrategy}} builds the outer exchange with {{new 
DefaultExchange(newExchange)}}, which has no {{CamelGooglePubsubMsgAckId}} 
header. So the request carries no ack id, none of the grouped messages is 
acked, and Pub/Sub delivers them again once their ack deadline expires. This 
repeats on every delivery. If a strategy keeps the first exchange as the 
holder, its id is sent N+1 times and the other ids never (read from the source, 
not run). {{nack}} has the same defect.

h3. How to reproduce
Measured with the Pub/Sub emulator from {{camel-test-infra-google-pubsub}}: a 
subscription with a 5 s ack deadline, 3 messages published, and this route:
{code:java}
from("google-pubsub:test-project:sub?synchronousPull=true&ackMode=NONE&maxMessagesPerPoll=10&maxDeliveryAttempts=0")
    .aggregate(constant(true), new 
GroupedExchangeAggregationStrategy()).completionSize(3)
    .process(e -> {
        List<Exchange> grouped = e.getIn().getBody(List.class);
        
grouped.get(0).getIn().getHeader(GooglePubsubConstants.GOOGLE_PUBSUB_ACKNOWLEDGE,
 GooglePubsubAcknowledge.class).ack(e);
    });
{code}
A delegating {{SubscriberStub}} (through an overridden 
{{GooglePubsubComponent.getSubscriberStub}}) records each 
{{AcknowledgeRequest}}.
||Case||AcknowledgeRequest(s) sent||Redelivered after the deadline||
|{{ack(aggregated)}}|one request with an empty id list (the emulator accepted 
it without error)|yes: the same 3 messages delivered again after the 5 s 
deadline|
|control: {{ack(g)}} for each grouped exchange|3 requests with 1 id each|no, 
none in the 15 s observed|
On the aggregated exchange the {{CamelGooglePubsubMsgAckId}} header was 
{{null}}, while the 3 grouped exchanges had their own ids. Not checked against 
the real service, which may reject an empty {{ack_ids}} with INVALID_ARGUMENT. 
In that case {{ack()}} throws and the result is the same redelivery.

h3. Proposed change
* In the loop, read {{ACK_ID}} from {{((Exchange) body).getIn()}}. Keep the 
outer exchange's own id when present, and remove duplicates.
* Do not send an empty {{AcknowledgeRequest}} / {{ModifyAckDeadlineRequest}}: 
log a warning instead (or fail the call).
* Tests: a unit test on {{AcknowledgeSync}} with a stubbed {{SubscriberStub}} 
supplier that checks the ids for a grouped body, for both ack and nack. Also an 
emulator IT with an aggregated manual ack that checks nothing is redelivered.
* Docs: the "Manual Acknowledgement" section does not mention aggregated 
exchanges. It should state that acking a grouped exchange acks every grouped 
message with the synchronous consumer only. {{AcknowledgeAsync}} (the default 
asynchronous consumer) ignores the exchange argument and acks only the message 
its header came from.

h3. Impact / upgrade
* No default changes, so no upgrade-guide entry is needed.
* Only manual ack ({{ackMode=NONE}}) of an exchange with a {{List<Exchange>}} 
body, with {{synchronousPull=true}}.
* With the default {{ackMode=AUTO}}, each pulled message is acked by its own 
exchange's completion, which is not affected.
* The same code is on {{camel-4.18.x}} and {{camel-4.22.x}} (same line 75) and 
in the 4.18.5, 4.22.2 and 4.23.0 releases, so the fix is a backport candidate.

h3. Related
* CAMEL-10178 (07f57c33962c): the original code read the ack id of each grouped 
exchange ({{ex.getIn()}} over the {{GROUPED_EXCHANGE}} list).
* CAMEL-14514 (5ef63390a07a): rewrote it to iterate the List body and 
introduced this bug.
* CAMEL-15252 (cc75c3cb9d9c): put the acknowledge object in a header for 
{{ackMode=NONE}}.
* CAMEL-18447 (1a5463a8d31f): the stub supplier used by {{AcknowledgeSync}}.
* CAMEL-21929: a different aggregation issue, on the producer side.

_Claude Code on behalf of Federico Mariani_



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

Reply via email to