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

shashank updated CAMEL-25520:
-----------------------------
    Description: 
{{GoogleMailStreamConsumer.processBatch}} hands each exchange to the route with 
{{getAsyncProcessor().process(exchange, EmptyAsyncCallback.get())}} (line 136 
at main 5af71ab258b8) and returns without waiting; the message is marked as 
read ({{markAsRead=true}}, the default since CAMEL-16329) only by the 
exchange's on completion ({{processCommit}}, lines 120-133 and 145-166). When 
the route continues asynchronously, for example {{to("seda:...")}} (seda hands 
the on completion over to its consumer), an asynchronous producer such as 
kafka, or {{threads()}}, the poll thread returns, and the next poll (500 ms 
later by default) lists the message again (it is still unread, and the default 
query is {{is:unread}}), gets it again and routes it a second time, and so on 
for as long as the first exchange runs.

When one of these duplicates fails, its rollback ({{processRollback}}, lines 
170-188) adds the UNREAD label back, also when another exchange of the same 
message has already completed and marked it read, so a message that was 
processed successfully is consumed once more by a later poll.

h3. Reproduction

New {{GoogleMailStreamConsumerInProgressTest}} with a Gmail client whose 
transport answers from an in-memory mailbox ({{FakeGmailClientFactory}}, 
google-http-client {{MockHttpTransport}}: labels, list of unread messages, get, 
modify): one unread message, route 
{{google-mail-stream://index?clientId=id&clientSecret=secret&delay=10}} to 
{{seda:hold}}; the seda route holds the exchange until the consumer has 
completed a second poll (counted with a {{pollStrategy}}). On main:
{noformat}
m1 was consumed again by a later poll while its first exchange was still in 
flight ==> expected: <1> but was: <2>
{noformat}

h3. Proposed fix

The consumer keeps the ids of the messages whose exchanges are in progress (a 
set in the consumer, as the in-progress repository of aws2-s3): a poll skips 
them; an id is added before the message is fetched and released when its 
exchange completes or fails (in the on completion, after marking read or 
unread), and when the poll does not process it (get failure, consumer 
stopping). No new option; the set is not cleared when the route is stopped (ids 
of exchanges still in flight are released when they complete), and an exchange 
that never completes keeps its message reserved. With {{markAsRead=false}} a 
message is now consumed again only after its previous exchange completed 
(before, every poll routed it again, also while it was in flight); 4.23 
upgrade-guide note. Two more tests check that a message whose exchange failed 
is consumed again and then marked as read ({{failedMessageIsConsumedAgain}}), 
and that a message whose exchange a stopping consumer did not process is not 
left in progress ({{notProcessedMessageIsConsumedAgain}}); both pass on main 
too, which has no in-progress set, and guard the releases on failure and on 
stop (the second one fails when the release on stop is removed). A further test 
checks that a message whose get failed is consumed by a later poll. 
camel-google-mail unit tests: 48, 0 failures.

Found with a TLA+ model of the poll (list unread, get, dispatch, stop of the 
batch) and the asynchronous completion (mark read) or failure (mark unread): "a 
message is routed again only after an exchange for it failed" is violated in 10 
steps (poll, get, dispatch, poll again, get, dispatch again); with failures, "a 
message processed successfully is not routed again" is violated too (a 
duplicate fails after the first completed). With a synchronous route all 
properties hold; with the fix they hold also with failures, and no id stays in 
progress when nothing is in flight. Then confirmed with the real consumer as 
above.

Affected: main, camel-4.22.x, camel-4.18.x, camel-4.14.x (same code).

Duplicate check (2026-10-10): JIRA component camel-google-mail-stream (5 
issues: raw body, markAsRead default, verifier; none about duplicates), text 
"GoogleMailStreamConsumer" (4, none related), component camel-google-mail since 
2023 (2, unrelated); GitHub pull requests "google-mail stream": 4 merged, none 
about in-flight messages; no open PR touches camel-google-mail.

_Filed with Claude Code on behalf of allthingssecurity._


  was:
{{GoogleMailStreamConsumer.processBatch}} hands each exchange to the route with 
{{getAsyncProcessor().process(exchange, EmptyAsyncCallback.get())}} (line 136 
at main 5af71ab258b8) and returns without waiting; the message is marked as 
read ({{markAsRead=true}}, the default since CAMEL-16329) only by the 
exchange's on completion ({{processCommit}}, lines 120-133 and 145-166). When 
the route continues asynchronously, for example {{to("seda:...")}} (seda hands 
the on completion over to its consumer), an asynchronous producer such as 
kafka, or {{threads()}}, the poll thread returns, and the next poll (500 ms 
later by default) lists the message again (it is still unread, and the default 
query is {{is:unread}}), gets it again and routes it a second time, and so on 
for as long as the first exchange runs.

When one of these duplicates fails, its rollback ({{processRollback}}, lines 
170-188) adds the UNREAD label back, also when another exchange of the same 
message has already completed and marked it read, so a message that was 
processed successfully is consumed once more by a later poll.

h3. Reproduction

New {{GoogleMailStreamConsumerInProgressTest}} with a Gmail client whose 
transport answers from an in-memory mailbox ({{FakeGmailClientFactory}}, 
google-http-client {{MockHttpTransport}}: labels, list of unread messages, get, 
modify): one unread message, route 
{{google-mail-stream://index?clientId=id&clientSecret=secret&delay=10}} to 
{{seda:hold}}; the seda route holds the exchange until the consumer has 
completed a second poll (counted with a {{pollStrategy}}). On main:
{noformat}
m1 was consumed again by a later poll while its first exchange was still in 
flight ==> expected: <1> but was: <2>
{noformat}

h3. Proposed fix

The consumer keeps the ids of the messages whose exchanges are in progress (a 
set in the consumer, as the in-progress repository of aws2-s3): a poll skips 
them; an id is added before the message is fetched and released when its 
exchange completes or fails (in the on completion, after marking read or 
unread), and when the poll does not process it (get failure, consumer 
stopping). No new option; the set is not cleared when the route is stopped (ids 
of exchanges still in flight are released when they complete), and an exchange 
that never completes keeps its message reserved. With {{markAsRead=false}} a 
message is now consumed again only after its previous exchange completed 
(before, every poll routed it again, also while it was in flight); 4.23 
upgrade-guide note. Two more tests check that a message whose exchange failed 
is consumed again and then marked as read ({{failedMessageIsConsumedAgain}}), 
and that a message whose exchange a stopping consumer did not process is not 
left in progress ({{notProcessedMessageIsConsumedAgain}}); both pass on main 
too, which has no in-progress set, and guard the releases on failure and on 
stop (the second one fails when the release on stop is removed). 
camel-google-mail unit tests: 47, 0 failures.

Found with a TLA+ model of the poll (list unread, get, dispatch, stop of the 
batch) and the asynchronous completion (mark read) or failure (mark unread): "a 
message is routed again only after an exchange for it failed" is violated in 10 
steps (poll, get, dispatch, poll again, get, dispatch again); with failures, "a 
message processed successfully is not routed again" is violated too (a 
duplicate fails after the first completed). With a synchronous route all 
properties hold; with the fix they hold also with failures, and no id stays in 
progress when nothing is in flight. Then confirmed with the real consumer as 
above.

Affected: main, camel-4.22.x, camel-4.18.x, camel-4.14.x (same code).

Duplicate check (2026-10-10): JIRA component camel-google-mail-stream (5 
issues: raw body, markAsRead default, verifier; none about duplicates), text 
"GoogleMailStreamConsumer" (4, none related), component camel-google-mail since 
2023 (2, unrelated); GitHub pull requests "google-mail stream": 4 merged, none 
about in-flight messages; no open PR touches camel-google-mail.

_Filed with Claude Code on behalf of allthingssecurity._



> camel-google-mail - google-mail-stream consumer routes a message again while 
> its first exchange is still in flight (asynchronous routes)
> ----------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25520
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25520
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-google-mail-stream
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>
> {{GoogleMailStreamConsumer.processBatch}} hands each exchange to the route 
> with {{getAsyncProcessor().process(exchange, EmptyAsyncCallback.get())}} 
> (line 136 at main 5af71ab258b8) and returns without waiting; the message is 
> marked as read ({{markAsRead=true}}, the default since CAMEL-16329) only by 
> the exchange's on completion ({{processCommit}}, lines 120-133 and 145-166). 
> When the route continues asynchronously, for example {{to("seda:...")}} (seda 
> hands the on completion over to its consumer), an asynchronous producer such 
> as kafka, or {{threads()}}, the poll thread returns, and the next poll (500 
> ms later by default) lists the message again (it is still unread, and the 
> default query is {{is:unread}}), gets it again and routes it a second time, 
> and so on for as long as the first exchange runs.
> When one of these duplicates fails, its rollback ({{processRollback}}, lines 
> 170-188) adds the UNREAD label back, also when another exchange of the same 
> message has already completed and marked it read, so a message that was 
> processed successfully is consumed once more by a later poll.
> h3. Reproduction
> New {{GoogleMailStreamConsumerInProgressTest}} with a Gmail client whose 
> transport answers from an in-memory mailbox ({{FakeGmailClientFactory}}, 
> google-http-client {{MockHttpTransport}}: labels, list of unread messages, 
> get, modify): one unread message, route 
> {{google-mail-stream://index?clientId=id&clientSecret=secret&delay=10}} to 
> {{seda:hold}}; the seda route holds the exchange until the consumer has 
> completed a second poll (counted with a {{pollStrategy}}). On main:
> {noformat}
> m1 was consumed again by a later poll while its first exchange was still in 
> flight ==> expected: <1> but was: <2>
> {noformat}
> h3. Proposed fix
> The consumer keeps the ids of the messages whose exchanges are in progress (a 
> set in the consumer, as the in-progress repository of aws2-s3): a poll skips 
> them; an id is added before the message is fetched and released when its 
> exchange completes or fails (in the on completion, after marking read or 
> unread), and when the poll does not process it (get failure, consumer 
> stopping). No new option; the set is not cleared when the route is stopped 
> (ids of exchanges still in flight are released when they complete), and an 
> exchange that never completes keeps its message reserved. With 
> {{markAsRead=false}} a message is now consumed again only after its previous 
> exchange completed (before, every poll routed it again, also while it was in 
> flight); 4.23 upgrade-guide note. Two more tests check that a message whose 
> exchange failed is consumed again and then marked as read 
> ({{failedMessageIsConsumedAgain}}), and that a message whose exchange a 
> stopping consumer did not process is not left in progress 
> ({{notProcessedMessageIsConsumedAgain}}); both pass on main too, which has no 
> in-progress set, and guard the releases on failure and on stop (the second 
> one fails when the release on stop is removed). A further test checks that a 
> message whose get failed is consumed by a later poll. camel-google-mail unit 
> tests: 48, 0 failures.
> Found with a TLA+ model of the poll (list unread, get, dispatch, stop of the 
> batch) and the asynchronous completion (mark read) or failure (mark unread): 
> "a message is routed again only after an exchange for it failed" is violated 
> in 10 steps (poll, get, dispatch, poll again, get, dispatch again); with 
> failures, "a message processed successfully is not routed again" is violated 
> too (a duplicate fails after the first completed). With a synchronous route 
> all properties hold; with the fix they hold also with failures, and no id 
> stays in progress when nothing is in flight. Then confirmed with the real 
> consumer as above.
> Affected: main, camel-4.22.x, camel-4.18.x, camel-4.14.x (same code).
> Duplicate check (2026-10-10): JIRA component camel-google-mail-stream (5 
> issues: raw body, markAsRead default, verifier; none about duplicates), text 
> "GoogleMailStreamConsumer" (4, none related), component camel-google-mail 
> since 2023 (2, unrelated); GitHub pull requests "google-mail stream": 4 
> merged, none about in-flight messages; no open PR touches camel-google-mail.
> _Filed with Claude Code on behalf of allthingssecurity._



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

Reply via email to