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

Fix race condition in GetObject that could result in empty responses #1334

Merged
merged 3 commits into from
Mar 31, 2025

Conversation

passaro
Copy link
Contributor

@passaro passaro commented Mar 26, 2025

Address an issue in the Stream implementation for S3GetObjectResponse that could immediately return None (i.e. terminate the stream) when detecting that the meta request had completed, before returning previously received parts. Reported in #1331.

The fix changes the mechanism used to extract the response body parts and the request completion from the meta request callbacks. Instead of multiple independent channels, it introduces a single channel that supports multiple S3GetObjectEvents. The events in the new channel match the order in which the callbacks are invoked, which is guaranteed by the CRT. The events channel also includes the Headers event, avoiding the need of a separate channel to await for the headers to be returned.

When using Mountpoint, an occurrence of this issue would result in a read request failing with an Input/output error, with a warning entry in the logs containing this message:

mountpoint_s3_fs::fuse: read failed with errno 5: get request failed: get request terminated unexpectedly

Note however that we were not able to trigger the issue in our tests.

Does this change impact existing behavior?

No.

Does this change need a changelog entry? Does it require a version change?

Bug fix entry and increase patch version.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and I agree to the terms of the Developer Certificate of Origin (DCO).

@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro had a problem deploying to PR integration tests March 26, 2025 06:41 — with GitHub Actions Failure
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:06 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 26, 2025 08:07 — with GitHub Actions Inactive
// Guaranteed when select_biased! executes the CreateMPU branch.
assert!(!request.is_terminated());
// Wait for CreateMultipartUpload to complete, or return error.
mpu_created.await.unwrap()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would we wait indefinitely for mpu_created here? Is there any timeout under the hood or knowledge about CreateMultipartUpload that assure us that we won't wait forever?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See from line 93 above: if we get a result instead of the CreateMultipartUpload completing, it will be an error and we send it to this channel.

IsaevIlya
IsaevIlya previously approved these changes Mar 27, 2025
@passaro passaro marked this pull request as ready for review March 27, 2025 16:49
Some(S3GetObjectEvent::Error(e)) => {
return Err(e);
}
_ => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be useful to trace! the body here for debugging purpuses?

*this.next_offset = offset + part.len() as u64;
Poll::Ready(Some(Ok((offset, part))))
}
Poll::Ready(Some(S3GetObjectEvent::Headers(_))) => Poll::Pending,
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't ever happen right? Maybe we can emit some error log or assert

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact, it could happen before. I've now updated the on_headers callback to only send the headers once and added an assertion here.

|_, _| {},
|_| None,
move |result| {
if let Some(sender) = on_failure_sender.lock().unwrap().take() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe using an atomic bool instead of Arc<Mutex<Option<...>>> could be simpler here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what you have in mind.

Another approach I considered was to introduce an "event" channel like in get_object, but for MpuCreated and Result. However, it would require more changes to S3PutObjectRequest and we are considering removing the wait for CreateMultipartUpload anyway, so I decided to go for the less intrusive change.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking of having only one channel for all results and keeping track of errors in a AtomicBool, but I think that way the initial CreateMultipartUploadFailed error would be emitted once the caller first polls this method rather than initially from this method.

@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 09:07 — with GitHub Actions Inactive
unexge
unexge previously approved these changes Mar 28, 2025

#[ignore = "Stress-test to run many concurrent GetObjects. To be run manually."]
#[tokio::test(flavor = "multi_thread", worker_threads = 100)]
async fn stress_test_get_object() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

|_, _| {},
|_| None,
move |result| {
if let Some(sender) = on_failure_sender.lock().unwrap().take() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking of having only one channel for all results and keeping track of errors in a AtomicBool, but I think that way the initial CreateMultipartUploadFailed error would be emitted once the caller first polls this method rather than initially from this method.

@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 28, 2025 19:41 — with GitHub Actions Inactive
passaro added 3 commits March 31, 2025 09:21
Signed-off-by: Alessandro Passaro <alexpax@amazon.co.uk>
Signed-off-by: Alessandro Passaro <alexpax@amazon.co.uk>
Signed-off-by: Alessandro Passaro <alexpax@amazon.co.uk>
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro passaro temporarily deployed to PR integration tests March 31, 2025 09:31 — with GitHub Actions Inactive
@passaro
Copy link
Contributor Author

passaro commented Mar 31, 2025

I have rebased and taken out the make_meta_request refactor. Now this change only includes the race condition fix. I'll open a separate PR for the other changes.

Copy link
Contributor

@adpeace adpeace left a comment

Choose a reason for hiding this comment

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

lgtm, one minor comment about an inaccurate comment that can be addressed separately.

) -> Result<S3HttpRequest<Vec<u8>, E>, S3RequestError> {
let options = Self::new_meta_request_options(message, operation);
self.make_simple_http_request_from_options(options, request_span, |_| {}, on_error, |_, _| ())
self.make_simple_http_request_from_options(options, request_span, |_| {}, parse_meta_request_error, |_, _| ())
}

/// Make an HTTP request using this S3 client that returns the body on success or invokes the
Copy link
Contributor

Choose a reason for hiding this comment

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

Please can we update this comment? Looks like the interface of this function has changed slight (we seem to always send body to the channel, and parse errors through the caller-provided function).

Copy link
Contributor

Choose a reason for hiding this comment

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

(sounds like this is to be addressed in a later PR already in progress..)

@passaro passaro added this pull request to the merge queue Mar 31, 2025
Merged via the queue into awslabs:main with commit a3909e0 Mar 31, 2025
26 checks passed
@passaro passaro deleted the get-object-fix branch March 31, 2025 16:01
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