[
https://issues.apache.org/jira/browse/CAMEL-26539?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-26539 started by shashank.
----------------------------------------
> camel-aws2-s3 - streaming upload loses objects and parts when exchanges are
> sent concurrently
> ---------------------------------------------------------------------------------------------
>
> Key: CAMEL-26539
> URL: https://issues.apache.org/jira/browse/CAMEL-26539
> Project: Camel
> Issue Type: Bug
> Components: camel-aws2
> Reporter: shashank
> Assignee: shashank
> Priority: Major
>
> Found with a SPIN model of {{AWS2S3StreamUploadProducer}} (exchanges sent at
> the same time through the one producer of a route, the lifecycle thread, and
> a fake bucket in which a completed upload replaces the object of the same
> key), then reproduced on the real producer with a mocked {{S3Client}}.
> With {{streamingUploadMode=true}} (without {{timestampGroupingEnabled}}), the
> producer collects the exchanges into one open multipart upload
> ({{uploadAggregate}}). {{processWithoutTimestampGrouping}} checks for the
> open upload under the producer's lock (since CAMEL-24195), but when there is
> none it starts an upload of its own *outside* the lock: it creates the upload
> state (which takes the object name from the producer's part number,
> {{progressive}} naming by default), calls {{createMultipartUpload}}, and only
> at the end of the exchange takes the lock again to make its state the open
> upload, or, if another exchange did that meanwhile, to copy its buffered
> bytes into that one ("handle potential race condition"). Exchanges run at the
> same time whenever the route has more than one thread: concurrent consumers
> (seda, jms, kafka), parallel splits, {{threads()}}, HTTP consumers.
> Two exchanges that find no open upload at the same time (at the start, and
> again after every completed object, so after every {{batchMessageNumber}}
> messages) both start an upload, and:
> * *an object replaces another one*: both states take the same part number, so
> both uploads get the same key. If the other exchange's upload is completed
> first (for example once the next messages reach {{batchMessageNumber}}), the
> slower exchange then makes its state the open upload, and when that one is
> completed it replaces the object. With the defaults
> ({{batchMessageNumber=10}}, progressive naming) and two threads, the 10
> messages of the first object are lost, although their exchanges completed
> without an error (the one that completed the object even got its ETag). When
> every exchange completes its own upload ({{batchMessageNumber=1}}, or bodies
> of {{batchSize}} or more), two concurrent exchanges can write the same key.
> * *the first part of a large body is lost*: with {{multiPartUpload=true}}, an
> exchange whose body is larger than {{partSize}} uploads its first part to its
> own upload; if another exchange opened the shared upload meanwhile, the rest
> of the body goes to the shared upload and the own upload, with the first
> part, is neither completed nor aborted (an incomplete multipart upload that
> is billed until a lifecycle rule removes it).
> * the upload of the exchange whose bytes are copied is neither completed nor
> aborted either.
> Short trace of the model (3 exchanges, {{batchMessageNumber=2}}; line numbers
> on main at dff263d86f08):
> {noformat}
> 1 E2 : no open upload -> new state, part 0 (key camel.txt),
> createMultipartUpload (AWS2S3StreamUploadProducer:256-304, no lock)
> 2 E1 : no open upload -> new state, part 0 (key camel.txt),
> createMultipartUpload (no lock)
> 3 E2 : open upload := E2's state
> (:338-351)
> 4 E0 : writes into the open upload, batchMessageNumber reached -> complete
> camel.txt, part = 1, open upload := null (:222-255)
> 5 E1 : open upload is null -> open upload := E1's state (key camel.txt)
> (:338-351)
> 6 stop (or the next messages): complete E1's upload -> camel.txt replaced, E0
> and E2 lost
> {noformat}
> The real-class test holds the first {{createMultipartUpload}} call in the
> mocked client: exchange 0 waits there while 10 more exchanges are sent, then
> 9 more. On main the bucket ends with one object of 10 messages
> ({{camel.txt}}: message 0 and 11-19); messages 1-10 are gone. A second test
> sends a body of one part and a half while a small exchange arrives: the first
> part is lost and its upload stays open.
> *Proposal:* write one exchange at a time: take the lock for the whole
> exchange in {{processWithoutTimestampGrouping}} (the shared upload and the
> part number of the names belong to the producer, and an exchange appended to
> the shared upload already does its S3 calls under the lock). An exchange that
> finds no open upload then starts it and makes it the open upload before the
> next exchange looks, and the copy branch is no longer needed. The timestamp
> grouping path is not changed.
> Affected: main, 4.23.x, 4.22.x (the same code), 4.18.x, 4.14.x (the same race
> in older code).
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)