shashank created CAMEL-25758:
--------------------------------
Summary: camel-azure-storage-blob - consumer with moveAfterRead
deletes the source blob although the copy to the destination container failed,
was aborted or its status could not be read: the blob is lost
Key: CAMEL-25758
URL: https://issues.apache.org/jira/browse/CAMEL-25758
Project: Camel
Issue Type: Bug
Components: camel-azure
Reporter: shashank
Assignee: shashank
With {{moveAfterRead=true}} the blob consumer moves a consumed blob in
{{BlobConsumer.processCommit}} (main e13303ef1ef7, lines 283-286) as copy +
delete:
{code:java}
copyBlobToDestination(containerName, blobName, destinationContainer,
destinationBlobName);
deleteBlob(containerName, blobName);
{code}
and {{copyBlobToDestination}} (lines 362-363) runs:
{code:java}
destBlobClient.beginCopy(sourceBlobClient.getBlobUrl(), Duration.ofSeconds(2))
.waitForCompletion(timeout);
{code}
The result of {{waitForCompletion}} is not looked at. A Copy Blob is an
asynchronous operation of the service: its status is {{pending}} until it ends
as {{success}}, {{aborted}} or {{failed}} (Get Blob Properties,
{{x-ms-copy-status}}). The {{SyncPoller}} of {{BlobClientBase.beginCopy}}
(azure-storage-blob 12.32, read from the jar) does not throw when the copy does
not succeed: it returns the last poll response once its status is complete,
that is {{FAILED}} for a failed copy, {{USER_CANCELLED}} for an aborted copy,
and the custom complete status {{POLLING_FAILED}} when reading the status of a
pending copy failed (an exception of {{getProperties()}} is caught in
{{onPoll}}). Apart from a copy request that the service refuses
({{BlobStorageException}} when the copy starts), only a timeout throws
({{RuntimeException}} with a {{TimeoutException}}). In all three cases the
consumer deletes the source blob next. The destination of a failed or aborted
copy has no content (Copy Blob remarks: an aborted copy leaves a blob of 0
bytes, a copy that times out on the service leaves an empty blob), and with
{{POLLING_FAILED}} the copy can still be pending when the source is deleted
("any changes to the source while the copy operation is in progress will cause
that operation to fail", Copy Blob remarks). The blob is gone from both
containers and Camel logs nothing.
How likely: the move is always within one storage account (one
{{BlobServiceClient}}), and Microsoft Learn says such a copy "can complete
synchronously". A copy that completes synchronously is read back as {{success}}
and is not affected. The loss needs a copy that the service runs asynchronously
and that then fails or is aborted, or a failed status read while it is pending.
How often same-account copies are asynchronous is not documented.
The copy was a synchronous {{copyFromUrl}} up to camel-4.18.x (it throws on
failure); CAMEL-24158 switched to {{beginCopy}} for the authorization of the
copy source.
*Fix* (one class): {{processCommit}} deletes the source blob only when the copy
ended with {{SUCCESSFULLY_COMPLETED}}. Otherwise it reports the copy status to
the consumer's exception handler and keeps the blob, which is consumed again on
the next poll, the same as after a copy request that fails with a
{{BlobStorageException}} today. A copy that keeps failing is therefore retried
at every poll with one exception handler call each time, as a copy that is
refused already is. A copy reported as {{POLLING_FAILED}} that had in fact
succeeded is copied and routed again on the next poll (at least once instead of
a possible loss); if the status can never be read (credentials that may write
but not read the destination container), every move is reported and retried
this way, where main deleted the source without knowing the outcome. A copy
that times out is unchanged: the exception leaves the on completion, Camel logs
it and the source blob is kept.
*Tests*: new {{BlobConsumerMoveAfterReadCopyStatusTest}} (4 tests, Mockito
clients as in {{BlobConsumerInProgressTest}}; the poller returns the given
status): the source blob is kept and the exception handler is called when the
copy ends {{FAILED}}, {{USER_CANCELLED}} or {{POLLING_FAILED}}; the source blob
is deleted after a successful copy. The three "kept" tests fail on main
({{NeverWantedButInvoked: blobClient.delete()}}), the fourth passes on main and
guards the success path of the fix. The camel-azure-storage-blob unit tests
pass with the fix (68 run, 0 failures).
*How it was found*: deterministic simulation testing with a fake Azure storage
world (the consumer, its scheduled polls, its on completion and Camel's routing
real; the sync {{BlobServiceClient}} / {{BlobContainerClient}} / {{BlobClient}}
faked from the Blob service REST reference, with asynchronous copies whose
final status the seed picks). The seed picked {{moveAfterRead}} or
{{deleteAfterRead}}, {{maxResultsPerPage}}, the poll delay, the {{timeout}}, an
asynchronous route step, fault rates, and how often a copy stays pending and
ends as failed, aborted or with a failed status read. The invariant "nothing
lost: every blob is still in the source container or was moved with its
content" failed in 99 of 1000 seeds on main, all with {{moveAfterRead=true}}
and a copy that did not succeed (first failing seed 11: a pending copy ended as
failed after about 4 s and the source blob was deleted); with the fix, 0 of the
same 1000 seeds fail. The world's self-check: with CAMEL-25551 reverted, 733 of
the 1000 seeds fail its invariant "a blob is not routed again while its
exchange is in flight".
*Affected*: main, camel-4.23.x and camel-4.22.x (the same
{{beginCopy(...).waitForCompletion(timeout)}} followed by the delete; read, not
run). camel-4.18.x uses {{copyFromUrl}} and is not affected.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)