shashank created CAMEL-25519:
--------------------------------
Summary: camel-google-storage - consumer routes an object again
while its first exchange is still in flight (asynchronous routes)
Key: CAMEL-25519
URL: https://issues.apache.org/jira/browse/CAMEL-25519
Project: Camel
Issue Type: Bug
Components: camel-google-storage
Reporter: shashank
{{GoogleCloudStorageConsumer.processBatch}} hands each exchange to the route
with {{getAsyncProcessor().process(exchange, EmptyAsyncCallback.get())}} (line
229 at main 5af71ab258b8) and returns without waiting; the object is deleted
({{deleteAfterRead=true}}, the default) or moved only by the exchange's on
completion ({{processCommit}}, lines 212-225 and 240-271). 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 same object again, downloads it again and routes it a second
time. If the first exchange deletes the object after the next poll listed it,
the download of that poll ({{createExchange}}, line 337) fails with a 404
{{StorageException}}: the whole poll ends with an exception (logged as a WARN)
and none of the objects it listed is routed by it.
This is the defect fixed for aws2-s3 in CAMEL-17110 ("What the s3 consumer
lacks is an in-progress repository") and for minio in CAMEL-25512; the
google-storage consumer has no in-progress check. The same happens with
{{objectName}} set (one object per poll). With {{deleteAfterRead=false}} every
poll routes every object anyway, also while it is in flight.
h3. Reproduction
New {{GoogleCloudStorageConsumerInProgressTest}} with the module's in-memory
{{LocalStorageHelper}} storage: one object, route
{{google-storage://myCamelBucket?delay=10}} to {{seda:work}}; the seda route
holds the exchange until the consumer has completed a second poll (counted with
a {{pollStrategy}}). Two cases: listing the bucket, and {{objectName=a.txt}}.
On main both fail:
{noformat}
[a.txt was consumed again by a later poll while its first exchange was still in
flight]
expected: 1
but was: 2
{noformat}
New {{GoogleCloudStorageConsumerDeletedObjectTest}}: three objects, the storage
deletes {{b.txt}} right after the listing (as another consumer would); on main
the poll fails ({{[the poll failed because b.txt was deleted between the
listing and the download] Expecting AtomicInteger(1) to have value: 0}}) and
{{a.txt}} and {{c.txt}} are only routed by a later poll.
h3. Proposed fix
The consumer keeps the names of the objects whose exchanges are in progress (a
set in the consumer, as the minio fix): a poll skips them, in both listing and
{{objectName}} mode; a name is added before the download and released when its
exchange completes or fails (in the on completion, after the delete/move), when
the poll does not process it (download failure, consumer stopping). A listed
object that no longer exists when its content is downloaded (404) is skipped
and the rest of the poll goes on, the same as with {{objectName}} for a missing
object; as the exchanges are created before the batch is routed,
{{CamelBatchSize}} only counts the objects that are routed. No new option:
aws2-s3 exposes a pluggable {{inProgressRepository}}; like its default memory
repository, the set is not cleared when the route is stopped (names of
exchanges still in flight are released when they complete), and an exchange
that never completes keeps its object reserved. 4.23 upgrade-guide note
(in-flight objects are skipped, also with {{deleteAfterRead=false}}; a deleted
object no longer fails the poll). New
{{GoogleCloudStorageConsumerInProgressFailureTest}} checks that a failed object
is consumed again, and
{{GoogleCloudStorageConsumerInProgressTest.notProcessedObjectIsConsumedAgain}}
that an object whose exchange a stopping consumer did not process is not left
in progress (both pass on main too, which has no in-progress set; they guard
the releases on failure and on stop, and the second one fails when the release
on stop is removed). camel-google-storage unit tests: 58, 0 failures.
Not covered: if the first exchange completes (deletes and releases the object)
after the next poll listed it but before that poll downloads it, the download
sees a 404; with the fix that object is skipped instead of failing the poll.
Found with a TLA+ model of the poll (list, download per listed object, dispatch
per exchange, stop of the batch) and asynchronous completion (delete): "with
deleteAfterRead every object is routed once" is violated in 10 steps (poll,
download, dispatch, poll again, download, dispatch again) and "a poll does not
fail because of an object another exchange of this consumer deleted" in 10
steps; both hold with a synchronous route. With the fix both hold, and no name
stays in progress when nothing is in flight (also after a stopped batch). 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-storage since 2024 (7
issues: download path containment, producer metadata, health check, prefix;
none about consumption), text "GoogleCloudStorageConsumer" (6, none about
in-flight objects), "google-storage" + "progress" (CAMEL-25512 is the minio
sibling); GitHub pull requests "google-storage consumer": only the minio PR
#27645 (merged); no open PR touches camel-google-storage.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)