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

Claus Ibsen updated CAMEL-25758:
--------------------------------
    Fix Version/s: 4.24.0

> 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
>            Priority: Major
>             Fix For: 4.24.0
>
>
> 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)

Reply via email to