Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@uppy/aws-s3-multipart: Fix race condition in #uploadParts #3955

Merged
merged 2 commits into from Aug 8, 2022

Conversation

mogzol
Copy link
Contributor

@mogzol mogzol commented Aug 4, 2022

The #uploadParts function calls itself after any part is uploaded. It also
determines which new chunks to upload based on their state.busy value. This
introduced a race condition, as state.busy was being set to false in the XHR
event handlers. So if one part were to complete while another part had finished
the XHR request, but not yet completed, then an upload for that second part
would be started again, despite the fact that the previous upload was still in
progress. Multiple uploads for the same part at the same time cause numerous
issues, and should never happen.

This is especially noticeable when an XHR request fails. #uploadPart is
wrapped in #retryable, so the part will be retried, however, for the entire
retryDelay, the chunk's state.busy value would be false, meaning that if
any other part completed, this part would be uploaded again, despite the fact
that the upload is already ongoing.

To fix this, this commit moves setting state.busy to the before and after
functions of the #retryable call, so a part will remain busy for the entire
time it is being uploaded/retried.

The `#uploadParts` function calls itself after any part is uploaded. It also
determines which new chunks to upload based on their `state.busy` value. This
introduced a race condition, as `state.busy` was being set to false in the XHR
event handlers. So if one part were to complete while another part had finished
the XHR request, but not yet completed, then an upload for that second part
would be started again, despite the fact that the previous upload was still in
progress. Multiple uploads for the same part at the same time cause numerous
issues, and should never happen.

This is especially noticeable when an XHR request fails. `#uploadPart` is
wrapped in `#retryable`, so the part will be retried, however, for the entire
`retryDelay`, the chunk's `state.busy` value would be false, meaning that if
any other part completed, this part would be uploaded again, despite the fact
that the upload is already ongoing.

To fix this, this commit moves setting `state.busy` to the `before` and `after`
functions of the `#retryable` call, so a part will remain `busy` for the entire
time it is being uploaded/retried.
@mogzol
Copy link
Contributor Author

mogzol commented Aug 4, 2022

Assuming this gets approved, what is the process for backporting a fix to the 2.x release? I need this fix in my application, which is still on uppy 2.x. Should I open a separate PR to the 2.x branch?

@arturi
Copy link
Contributor

arturi commented Aug 5, 2022

@mogzol Thanks for the PR! If approved, we’ll be able to cherry-pick and do a patch release for 2.x as well.

Copy link
Member

@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This has been a bug I couldn't put my finger on where to find it for a while.

@Murderlon Murderlon removed the request for review from Acconut August 5, 2022 08:53
@Murderlon Murderlon mentioned this pull request Aug 5, 2022
@Murderlon
Copy link
Member

Murderlon commented Aug 5, 2022

It would be nice to test this, but we don't really have the setup for it in e2e so I added a note for it in the PR. Unless you think it's best to test this in unit tests?

EDIT: I think we should be able to test this with nock and spying chunkState. We already have a test that does a lot of this:

describe('without companionUrl (custom main functions)', () => {
let core
let awsS3Multipart
beforeEach(() => {
core = new Core()
core.use(AwsS3Multipart, {
createMultipartUpload: jest.fn(() => {
return {
uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
key: 'test/upload/multitest.dat',
}
}),
completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
abortMultipartUpload: jest.fn(),
prepareUploadParts: jest.fn(async () => {
const presignedUrls = {}
const possiblePartNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
possiblePartNumbers.forEach((partNumber) => {
presignedUrls[
partNumber
] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
})
return { presignedUrls, headers: { 1: { 'Content-MD5': 'foo' } } }
}),
})
awsS3Multipart = core.getPlugin('AwsS3Multipart')
})
it('Calls the prepareUploadParts function totalChunks / limit times', async () => {
const scope = nock(
'https://bucket.s3.us-east-2.amazonaws.com',
).defaultReplyHeaders({
'access-control-allow-headers': '*',
'access-control-allow-method': 'PUT',
'access-control-allow-origin': '*',
'access-control-expose-headers': 'ETag, Content-MD5',
})
// 6MB file will give us 2 chunks, so there will be 2 PUT and 2 OPTIONS
// calls to the presigned URL from 1 prepareUploadParts calls
const fileSize = 5 * MB + 1 * MB
scope
.options((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
.reply(function replyFn () {
expect(this.req.headers['access-control-request-headers']).toEqual('Content-MD5')
return [200, '']
})
scope
.options((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
.reply(function replyFn () {
expect(this.req.headers['access-control-request-headers']).toBeUndefined()
return [200, '']
})
scope
.put((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
.reply(200, '', { ETag: 'test1' })
scope
.put((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
.reply(200, '', { ETag: 'test2' })
core.addFile({
source: 'jest',
name: 'multitest.dat',
type: 'application/octet-stream',
data: new File([new Uint8Array(fileSize)], {
type: 'application/octet-stream',
}),
})
await core.upload()
expect(
awsS3Multipart.opts.prepareUploadParts.mock.calls.length,
).toEqual(1)
scope.done()
})

Would you be willing to try to make a test for it?

@mogzol
Copy link
Contributor Author

mogzol commented Aug 5, 2022

I added a test to verify that busy doesn't get changed to false before done is changed to true. I don't know if that's really the best way to test this, but I couldn't figure out a better way. It does pass with my changes and fail without them.

EDIT: Oops I left a .only in the test, fixing that...

EDIT 2: Fixed

@aduh95 aduh95 added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label Aug 5, 2022
@github-actions github-actions bot removed the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label Aug 5, 2022
Copy link
Member

@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the effort!

@Murderlon Murderlon added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label Aug 8, 2022
@github-actions github-actions bot removed the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label Aug 8, 2022
@aduh95 aduh95 added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label Aug 8, 2022
@github-actions github-actions bot removed pending end-to-end tests safe to test Add this label on trustworthy PRs to spawn the e2e test suite labels Aug 8, 2022
@Murderlon Murderlon merged commit 24b584f into transloadit:main Aug 8, 2022
aduh95 pushed a commit that referenced this pull request Aug 8, 2022
The `#uploadParts` function calls itself after any part is uploaded. It also
determines which new chunks to upload based on their `state.busy` value. This
introduced a race condition, as `state.busy` was being set to false in the XHR
event handlers. So if one part were to complete while another part had finished
the XHR request, but not yet completed, then an upload for that second part
would be started again, despite the fact that the previous upload was still in
progress. Multiple uploads for the same part at the same time cause numerous
issues, and should never happen.

This is especially noticeable when an XHR request fails. `#uploadPart` is
wrapped in `#retryable`, so the part will be retried, however, for the entire
`retryDelay`, the chunk's `state.busy` value would be false, meaning that if
any other part completed, this part would be uploaded again, despite the fact
that the upload is already ongoing.

To fix this, this commit moves setting `state.busy` to the `before` and `after`
functions of the `#retryable` call, so a part will remain `busy` for the entire
time it is being uploaded/retried.
aduh95 pushed a commit that referenced this pull request Aug 8, 2022
The `#uploadParts` function calls itself after any part is uploaded. It also
determines which new chunks to upload based on their `state.busy` value. This
introduced a race condition, as `state.busy` was being set to false in the XHR
event handlers. So if one part were to complete while another part had finished
the XHR request, but not yet completed, then an upload for that second part
would be started again, despite the fact that the previous upload was still in
progress. Multiple uploads for the same part at the same time cause numerous
issues, and should never happen.

This is especially noticeable when an XHR request fails. `#uploadPart` is
wrapped in `#retryable`, so the part will be retried, however, for the entire
`retryDelay`, the chunk's `state.busy` value would be false, meaning that if
any other part completed, this part would be uploaded again, despite the fact
that the upload is already ongoing.

To fix this, this commit moves setting `state.busy` to the `before` and `after`
functions of the `#retryable` call, so a part will remain `busy` for the entire
time it is being uploaded/retried.
github-actions bot added a commit that referenced this pull request Aug 8, 2022
| Package                | Version | Package                | Version |
| ---------------------- | ------- | ---------------------- | ------- |
| @uppy/aws-s3-multipart |   2.4.2 | @uppy/robodog          |   2.9.3 |
| @uppy/compressor       |   0.3.2 | uppy                   |  2.13.3 |
| @uppy/utils            |   4.1.1 |                        |         |

This release marks the transition of Uppy 2.x to maintenance mode. The team is
now focused on working Uppy 3.0.0 releases, and plan to not land any new feature
on the 2.x release line. We are committed on backporting bug fixes that affects
the deprecated Robodog package and its dependencies for at least one year, we
encourage Robodog users to migrate away from Robodog to Uppy plugins.

- @uppy/aws-s3-multipart: Fix race condition in `#uploadParts` (Morgan Zolob / #3955)
- meta: fork branch in preparation for LTS (Antoine du Hamel)
- @uppy/aws-s3-multipart: ignore exception inside `abortMultipartUpload` (Antoine du Hamel / #3950)
- @uppy/utils: modernize `getDroppedFiles` (Antoine du Hamel / #3534)
- @uppy/compressor: Fix Compressor being broken when no name is in the compressed blob (Artur Paikin / #3947)
- e2e: mark tests as flaky (Antoine du Hamel / #3940)
github-actions bot added a commit that referenced this pull request Aug 16, 2022
| Package                   |      Version | Package                   |      Version |
| ------------------------- | ------------ | ------------------------- | ------------ |
| @uppy/audio               | 1.0.0-beta.2 | @uppy/progress-bar        | 3.0.0-beta.2 |
| @uppy/aws-s3              | 3.0.0-beta.3 | @uppy/provider-views      | 3.0.0-beta.3 |
| @uppy/aws-s3-multipart    | 3.0.0-beta.4 | @uppy/react               | 3.0.0-beta.4 |
| @uppy/box                 | 2.0.0-beta.2 | @uppy/redux-dev-tools     | 3.0.0-beta.2 |
| @uppy/companion           | 4.0.0-beta.4 | @uppy/remote-sources      | 1.0.0-beta.4 |
| @uppy/companion-client    | 3.0.0-beta.2 | @uppy/screen-capture      | 3.0.0-beta.3 |
| @uppy/compressor          | 1.0.0-beta.3 | @uppy/status-bar          | 3.0.0-beta.3 |
| @uppy/core                | 3.0.0-beta.4 | @uppy/store-default       | 3.0.0-beta.3 |
| @uppy/dashboard           | 3.0.0-beta.4 | @uppy/store-redux         | 3.0.0-beta.3 |
| @uppy/drag-drop           | 3.0.0-beta.2 | @uppy/svelte              | 2.0.0-beta.2 |
| @uppy/drop-target         | 2.0.0-beta.3 | @uppy/thumbnail-generator | 3.0.0-beta.2 |
| @uppy/dropbox             | 3.0.0-beta.2 | @uppy/transloadit         | 3.0.0-beta.5 |
| @uppy/facebook            | 3.0.0-beta.2 | @uppy/tus                 | 3.0.0-beta.3 |
| @uppy/file-input          | 3.0.0-beta.2 | @uppy/unsplash            | 3.0.0-beta.2 |
| @uppy/form                | 3.0.0-beta.2 | @uppy/url                 | 3.0.0-beta.3 |
| @uppy/golden-retriever    | 3.0.0-beta.2 | @uppy/utils               | 5.0.0-beta.1 |
| @uppy/google-drive        | 3.0.0-beta.2 | @uppy/vue                 | 1.0.0-beta.2 |
| @uppy/image-editor        | 2.0.0-beta.3 | @uppy/webcam              | 3.0.0-beta.3 |
| @uppy/informer            | 3.0.0-beta.3 | @uppy/xhr-upload          | 3.0.0-beta.3 |
| @uppy/instagram           | 3.0.0-beta.2 | @uppy/zoom                | 2.0.0-beta.2 |
| @uppy/locales             | 3.0.0-beta.4 | uppy                      | 3.0.0-beta.5 |
| @uppy/onedrive            | 3.0.0-beta.2 |                           |              |

- meta: prepare release workflow for beta versions (Antoine du Hamel)
- @uppy/provider-views: Reset filter input correctly in provider views (Merlijn Vos / #3978)
- @uppy/aws-s3-multipart: Fix when using Companion (Merlijn Vos / #3969)
- @uppy/companion: Companion: bring back default upload protocol (Mikael Finstad / #3967)
- meta: Update CONTRIBUTING.md (Mikael Finstad / #3966)
- meta: fix contributing link (Mikael Finstad / #3968)
- @uppy/companion: enforce usage of uploadUrls (Mikael Finstad / #3965)
- @uppy/utils: Fix webp mimetype (Merlijn Vos / #3961)
- @uppy/locales: Add compressor string translation to Japanese locale (kenken / #3963)
- meta: Fix statement about cropping images in README.md (Mikael Finstad / #3964)
- @uppy/aws-s3-multipart: Fix race condition in `#uploadParts` (Morgan Zolob / #3955)
- @uppy/provider-views: core validateRestrictions: return error directly vs the result/reason obj (Artur Paikin / #3951)
- @uppy/aws-s3: Export AwsS3UploadParameters & AwsS3Options interfaces (Antonina Vertsinskaya / #3956)
- website: convert all website examples to ESM (Antoine du Hamel / #3957)
- @uppy/companion: fix crash if redis disconnects (Mikael Finstad / #3954)
- @uppy/companion: upgrade `ws` version (Antoine du Hamel / #3949)
- @uppy/companion: sort Dropbox response & refactor to async/await (Mikael Finstad / #3897)
- @uppy/utils: modernize `getDroppedFiles` (Antoine du Hamel / #3534)
- @uppy/companion: fix default getKey for non-standalone too (Mikael Finstad / #3945)
- @uppy/aws-s3-multipart: ignore exception inside `abortMultipartUpload` (Antoine du Hamel / #3950)
- @uppy/companion: remove `isobject` from dependencies (Antoine du Hamel / #3948)
- @uppy/compressor: Fix Compressor being broken when no name is in the compressed blob (Artur Paikin / #3947)
- @uppy/core,@uppy/react: Fix all breaking todo comments for 3.0 (Merlijn Vos / #3907)
- @uppy/companion: show deprecation message when using legacy s3 options (Antoine du Hamel / #3944)
- example: fix aws-companion example (Antoine du Hamel / #3850)
HeavenFox pushed a commit to docsend/uppy that referenced this pull request Jun 27, 2023
| Package                   |      Version | Package                   |      Version |
| ------------------------- | ------------ | ------------------------- | ------------ |
| @uppy/audio               | 1.0.0-beta.2 | @uppy/progress-bar        | 3.0.0-beta.2 |
| @uppy/aws-s3              | 3.0.0-beta.3 | @uppy/provider-views      | 3.0.0-beta.3 |
| @uppy/aws-s3-multipart    | 3.0.0-beta.4 | @uppy/react               | 3.0.0-beta.4 |
| @uppy/box                 | 2.0.0-beta.2 | @uppy/redux-dev-tools     | 3.0.0-beta.2 |
| @uppy/companion           | 4.0.0-beta.4 | @uppy/remote-sources      | 1.0.0-beta.4 |
| @uppy/companion-client    | 3.0.0-beta.2 | @uppy/screen-capture      | 3.0.0-beta.3 |
| @uppy/compressor          | 1.0.0-beta.3 | @uppy/status-bar          | 3.0.0-beta.3 |
| @uppy/core                | 3.0.0-beta.4 | @uppy/store-default       | 3.0.0-beta.3 |
| @uppy/dashboard           | 3.0.0-beta.4 | @uppy/store-redux         | 3.0.0-beta.3 |
| @uppy/drag-drop           | 3.0.0-beta.2 | @uppy/svelte              | 2.0.0-beta.2 |
| @uppy/drop-target         | 2.0.0-beta.3 | @uppy/thumbnail-generator | 3.0.0-beta.2 |
| @uppy/dropbox             | 3.0.0-beta.2 | @uppy/transloadit         | 3.0.0-beta.5 |
| @uppy/facebook            | 3.0.0-beta.2 | @uppy/tus                 | 3.0.0-beta.3 |
| @uppy/file-input          | 3.0.0-beta.2 | @uppy/unsplash            | 3.0.0-beta.2 |
| @uppy/form                | 3.0.0-beta.2 | @uppy/url                 | 3.0.0-beta.3 |
| @uppy/golden-retriever    | 3.0.0-beta.2 | @uppy/utils               | 5.0.0-beta.1 |
| @uppy/google-drive        | 3.0.0-beta.2 | @uppy/vue                 | 1.0.0-beta.2 |
| @uppy/image-editor        | 2.0.0-beta.3 | @uppy/webcam              | 3.0.0-beta.3 |
| @uppy/informer            | 3.0.0-beta.3 | @uppy/xhr-upload          | 3.0.0-beta.3 |
| @uppy/instagram           | 3.0.0-beta.2 | @uppy/zoom                | 2.0.0-beta.2 |
| @uppy/locales             | 3.0.0-beta.4 | uppy                      | 3.0.0-beta.5 |
| @uppy/onedrive            | 3.0.0-beta.2 |                           |              |

- meta: prepare release workflow for beta versions (Antoine du Hamel)
- @uppy/provider-views: Reset filter input correctly in provider views (Merlijn Vos / transloadit#3978)
- @uppy/aws-s3-multipart: Fix when using Companion (Merlijn Vos / transloadit#3969)
- @uppy/companion: Companion: bring back default upload protocol (Mikael Finstad / transloadit#3967)
- meta: Update CONTRIBUTING.md (Mikael Finstad / transloadit#3966)
- meta: fix contributing link (Mikael Finstad / transloadit#3968)
- @uppy/companion: enforce usage of uploadUrls (Mikael Finstad / transloadit#3965)
- @uppy/utils: Fix webp mimetype (Merlijn Vos / transloadit#3961)
- @uppy/locales: Add compressor string translation to Japanese locale (kenken / transloadit#3963)
- meta: Fix statement about cropping images in README.md (Mikael Finstad / transloadit#3964)
- @uppy/aws-s3-multipart: Fix race condition in `#uploadParts` (Morgan Zolob / transloadit#3955)
- @uppy/provider-views: core validateRestrictions: return error directly vs the result/reason obj (Artur Paikin / transloadit#3951)
- @uppy/aws-s3: Export AwsS3UploadParameters & AwsS3Options interfaces (Antonina Vertsinskaya / transloadit#3956)
- website: convert all website examples to ESM (Antoine du Hamel / transloadit#3957)
- @uppy/companion: fix crash if redis disconnects (Mikael Finstad / transloadit#3954)
- @uppy/companion: upgrade `ws` version (Antoine du Hamel / transloadit#3949)
- @uppy/companion: sort Dropbox response & refactor to async/await (Mikael Finstad / transloadit#3897)
- @uppy/utils: modernize `getDroppedFiles` (Antoine du Hamel / transloadit#3534)
- @uppy/companion: fix default getKey for non-standalone too (Mikael Finstad / transloadit#3945)
- @uppy/aws-s3-multipart: ignore exception inside `abortMultipartUpload` (Antoine du Hamel / transloadit#3950)
- @uppy/companion: remove `isobject` from dependencies (Antoine du Hamel / transloadit#3948)
- @uppy/compressor: Fix Compressor being broken when no name is in the compressed blob (Artur Paikin / transloadit#3947)
- @uppy/core,@uppy/react: Fix all breaking todo comments for 3.0 (Merlijn Vos / transloadit#3907)
- @uppy/companion: show deprecation message when using legacy s3 options (Antoine du Hamel / transloadit#3944)
- example: fix aws-companion example (Antoine du Hamel / transloadit#3850)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants