shashank created CAMEL-25558:
--------------------------------
Summary: camel-aws2-s3 - a listed object that cannot be got
(deleted meanwhile, or a failed get) fails the poll and stays in the
in-progress repository, so it is never consumed until a restart
Key: CAMEL-25558
URL: https://issues.apache.org/jira/browse/CAMEL-25558
Project: Camel
Issue Type: Bug
Components: camel-aws2-s3
Reporter: shashank
Assignee: shashank
When the consumer lists the bucket, {{AWS2S3Consumer.createExchanges(List)}}
adds each listed key to the endpoint's {{inProgressRepository}} (line 264 at
main 3a6cdba883a5) and then gets the object (line 287). When that get throws,
the catch block (lines 301-315) closes the streams and removes the keys of the
exchanges that were already created, and rethrows, so the poll fails. The key
of the object that failed has no exchange yet, so it is not removed. The
default repository is a memory repository that is only cleared when the
endpoint stops, so every later poll skips that key as "already in progress"
(lines 264-268).
Effects:
* A listed object that is deleted before the get (another consumer on the same
bucket, a lifecycle rule, or the exchange of an earlier poll that completes
after the listing, with an asynchronous route) makes the get fail with
{{NoSuchKeyException}}: that poll fails. The other listed objects are consumed
by a following poll (with a truncated listing the marker has already moved on,
lines 173-188, so they wait until the listing starts over). A new object stored
later under the same key is never consumed.
* An object whose get fails for another reason (403, throttling, a network
error), or whose download fails while it is read into the body
({{includeBody=true}}, line 500), fails that poll in the same way, stays in the
bucket, and is never consumed again, until the endpoint is restarted. In the
second case the exchange that was created for it (line 493) is also never
released to the exchange factory.
This is not new and not caused by CAMEL-25521: camel-4.22.x has the same loop
and catch block.
For comparison: camel-minio (CAMEL-25512) and camel-google-storage
(CAMEL-25519) skip a listed object that no longer exists (DEBUG) and consume
the rest; camel-ibm-cos skips a listed object whose get fails for any reason
(WARN with the key and the message, no stack trace), removes its key and
continues with the rest.
h3. Reproduction
New {{AWS2S3ConsumerDeletedObjectTest}} (6 tests) with the in-process
{{FakeS3Server}} of the module (it can now remove an object right after a
listing returned it, refuse the gets of an object with 403, and cut the body of
a get short), and {{AWS2S3ConsumerDeletedObjectPooledExchangeTest}}, which runs
the same 6 with {{PooledExchangeFactory}} and adds one. The scheduler is not
started ({{startScheduler=false}}); the tests call {{poll()}}. On main all 13
fail:
* {{listedObjectThatNoLongerExistsIsSkipped}}: {{a.txt}}, {{b.txt}}, {{c.txt}};
{{b.txt}} is removed after the listing. Expected: the poll returns 2 and routes
a and c with {{CamelBatchSize}} 2. On main: {{NoSuchKeyException: The specified
key does not exist}} from {{poll()}}.
* {{objectCreatedAgainAfterItWasSkippedIsConsumed}}: after that poll a new
{{b.txt}} is stored and must be consumed by the next poll. On main:
{{NoSuchKeyException}} from the first poll.
* {{objectWhoseGetIsRefusedDoesNotStopTheOtherObjects}}: every get of {{b.txt}}
is refused. Expected: the poll returns 2 (a and c), the next poll consumes a
new {{d.txt}} and tries {{b.txt}} again, and {{b.txt}} is consumed once its get
is allowed. On main: {{S3Exception: Access Denied}} from the first {{poll()}}.
* {{skippedObjectOfATruncatedListingIsTriedAgainWhenTheListingStartsOver}}
({{maxMessagesPerPoll=2}}, four objects, {{b.txt}} refused): the polls return
1, 2 (second page, {{b.txt}} not tried), 0 (listing starts over, {{b.txt}}
tried again), then 1 once the get is allowed. On main: {{S3Exception}} from the
first {{poll()}}.
* {{objectWhoseDownloadFailsIsSkippedAndNotLeftInProgress}}: the content stream
throws an {{IOException}} while it is read into the body. Expected: the poll
returns 0, the stream is closed, the key is not in progress. On main:
{{RuntimeCamelException}} ({{IOException: connection reset}}) from {{poll()}}.
* {{objectStreamIsClosedWhenIncludeS3ObjectFails}}: an overridden
{{includeS3Object}} throws. Expected as above. On main:
{{IllegalStateException}} from {{poll()}}.
* {{exchangeOfAnObjectWhoseDownloadFailsIsGivenBackToThePool}} (pooled): the
body of {{b.txt}} is cut short. Expected: a, c and d are routed with their own
bodies, 4 exchanges taken from the pool and 4 released. On main:
{{RuntimeCamelException}} ({{ConnectionClosedException: Premature end of
Content-Length delimited message body}}) from {{poll()}}.
h3. Proposed fix
In {{createExchanges(List)}}: a failure to get one listed object, or to create
its exchange, no longer fails the poll. The object is skipped (stream closed,
exchange given back to the exchange factory, key removed from the in-progress
repository), the other listed objects are consumed in the same poll, the
skipped object is not counted, and a later poll tries it again.
{{NoSuchKeyException}} is logged at DEBUG; any other failure at WARN with the
key and the message, without a stack trace, because an object that can never be
read (archived, access denied) is logged at every poll that lists it. This is
what camel-ibm-cos does; the alternative (keep failing the poll and only remove
the key) would make such an object fail every poll and block the objects listed
after it.
Consequence: such a failure is no longer a failed poll, so it does not reach
the exception handler ({{bridgeErrorHandler}}) and does not count for
{{backoffErrorThreshold}}. The listing marker is set from the listing response
before the gets, as before: with a truncated listing a skipped object is tried
again when the listing starts over, otherwise at the next poll.
Not changed, known limits: with {{fileName}}, a failed get still fails the
poll. An {{Error}} (for example {{OutOfMemoryError}} while a large object is
read into the body) is not caught, so its key still stays in progress.
With the pooled exchange factory the exchange of a skipped object is given back
with {{done()}}: {{releaseExchange(exchange, true)}} on an exchange that is not
done adds it to the pool twice, and two later objects of the poll then share it
(seen with the pooled test). The same call for never-routed exchanges exists on
main in this consumer ({{processBatch}} when stopping, the outer catch) and in
other consumers; not changed here.
No new option. 4.23 upgrade-guide note under the existing camel-aws2-s3
in-progress entry. camel-aws2-s3 unit tests with the fix: 98, 0 failures (the
68 integration tests need LocalStack and were not run).
Found by comparing the consumer with the other object storage consumers after
CAMEL-25521, then confirmed with the real consumer as above.
Affected: main; camel-4.22.x, camel-4.18.x and camel-4.14.x have the same loop
and catch block (read, not run).
Duplicate check (2026-10-11): JIRA text "aws2-s3 NoSuchKey" (1: CAMEL-25521,
the related fix, which does not change this path), "AWS2S3Consumer
NoSuchKeyException" (1: CAMEL-17110, which added the in-progress repository),
component camel-aws2-s3 with text "inProgressRepository" (1: CAMEL-25521),
component camel-aws2-s3 created since 2026-01-01 (10 issues, none about a
deleted object or a failed get in the consumer); GitHub pull requests
"AWS2S3Consumer NoSuchKey" and "aws2-s3 NoSuchKey": none; no open pull request
touches {{AWS2S3Consumer}}.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)