shashank created CAMEL-26539:
--------------------------------

             Summary: 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


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)

Reply via email to