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

@aws-sdk/client-s3 Passing a stream Body to PutObjectCommand will hang if a retry happens #5479

Closed
3 tasks done
jeanbmar opened this issue Nov 11, 2023 · 8 comments
Closed
3 tasks done
Assignees
Labels
bug This issue is a bug. closed-for-staleness p2 This is a standard priority issue response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days.

Comments

@jeanbmar
Copy link

jeanbmar commented Nov 11, 2023

Checkboxes for prior research

Describe the bug

I work on a project that needs to upload a few hundred files to S3 a few times per day. This happens in Lambda.

I'm using a single PutObjectCommand for each file and pass bodies as Readable streams. The ContentLength property of PutObjectCommand is properly set.

I noticed Lambda timeouts on PutObjectCommand send calls, that I couldn't explain, happening randomly.

I monkey patched @smithy/node-http-handler, and figured it happened when I was receiving 503 Slow Down errors from AWS. Fine? No, because the 15s timeout from my Lambda didn't match the retry delay which is between 100-1000ms. It should have been more than enough.

After more monkey patching, I found the cause:

When a Readable is passed to PutObjectCommand, the stream will be consumed by the http request through:

https://github.com/awslabs/smithy-typescript/blob/a4b58b32ac2ae778917e276ba381527f551c2d3d/packages/node-http-handler/src/write-request-body.ts#L60C7-L60C7

function writeBody(
  httpRequest: ClientRequest | ClientHttp2Stream,
  body?: string | ArrayBuffer | ArrayBufferView | Readable | Uint8Array
) {
  if (body instanceof Readable) {
    // pipe automatically handles end
    body.pipe(httpRequest); // <---------------- here
  } else if (body) {
    httpRequest.end(Buffer.from(body as Parameters<typeof Buffer.from>[0]));
  } else {
    httpRequest.end();
  }
}

Now what happens when there's some throttling involved and the request has to be retried? The stream is already consumed and ended. The end event from the Readable will never be emitted again. The http request will hang instead of creating a response:

https://github.com/awslabs/smithy-typescript/blob/a4b58b32ac2ae778917e276ba381527f551c2d3d/packages/node-http-handler/src/node-http-handler.ts#L148-L156

const req = requestFunc(nodeHttpsOptions, (res) => {
    // <--------------- this block will never happen on retry because the body stream can't end again, preventing the request itself from ending
    const httpResponse = new HttpResponse({
        statusCode: res.statusCode || -1,
        reason: res.statusMessage,
        headers: getTransformedHeaders(res.headers),
        body: res,
    });
    resolve({ response: httpResponse });
});

I'm surprised such a major issue was never noticed. It makes streams impossible to use with PutObjectCommand (and possibly others) as soon as there's some volume involved since S3 sends 503 when scaling.

SDK version number

@aws-sdk/client-s3@3.435.0

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

v18.17.0

Reproduction Steps

Loop on this until S3 replies with a 503 Slow down error, the send promise won't resolve when retrying.

const buffer = Buffer.alloc(10000, 'a');
const stream = Readable.from(buffer);
const s3Client = new S3Client();
const input = {
  Bucket: bucketName,
  Key: key,
  Body: stream,
  ContentLength: buffer.length,
};
await s3Client.send(new PutObjectCommand(input));

Observed Behavior

The send promise is hanging.

Expected Behavior

The send promise should either properly resolve after the retry delay or reject with a message stating than body streams cannot be retried.

Possible Solution

Use a PassThrough stream between the body and the http request to copy flowing data in a buffer, if ContentLength is reasonably small. Use the buffer when a retry happens (or throw if ContentLength was too large).

Additional Information/Context

No response

@jeanbmar jeanbmar added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Nov 11, 2023
@jeanbmar jeanbmar changed the title @aws-sdk/client-s3 Passing a stream Body to PutObjectCommand will hang forever if a retry happens @aws-sdk/client-s3 Passing a stream Body to PutObjectCommand will hang if a retry happens Nov 13, 2023
@RanVaknin RanVaknin self-assigned this Nov 14, 2023
@RanVaknin
Copy link
Contributor

Hi @jeanbmar,
Thanks for reaching out. I see how this could be an issue and I admit I dont remember seeing this before.

Im not sure about your use case here, but in your repro example you are creating the stream from a buffer. In that case you can supply the buffer directly to the Body parameter, this would make it so that the SDK could seek back to the beginning of the buffer. Would that work for your case?

Thanks,
Ran~

@RanVaknin RanVaknin added response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days. p2 This is a standard priority issue and removed needs-triage This issue or PR still needs to be triaged. labels Nov 14, 2023
@jeanbmar
Copy link
Author

Hi,

This was for the purpose of repro, I don't create the Readable from a buffer in real code.
I read the entire Readable into a buffer though in order to bypass the issue (https://nodejs.org/api/webstreams.html#streamconsumersbufferstream).

But I'm afraid this issue is a big flaw for any command that uses a Readable body. There's a chance the process will hang and it's very hard for developers to pinpoint the origin of the problem.

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days. label Nov 16, 2023
@RanVaknin RanVaknin added needs-review This issue/pr needs review from an internal developer. queued This issues is on the AWS team's backlog and removed needs-review This issue/pr needs review from an internal developer. labels Nov 16, 2023
@RanVaknin
Copy link
Contributor

Hi @jeanbmar ,

Yeah thats what I thought.

Unfortunately I was not able to artificially reproduce this.

I've tried your approach of iterating over that list over and over but it did not cause the service to throw a slow down 503 error.
I tried concurrently sending thousands of requests and it didnt slow down:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";

const s3Client = new S3Client();

async function uploadStream(id){

    const buffer = Buffer.alloc(10000, 'a');
    const stream = Readable.from(buffer);

    const input = {
        Bucket: "foo-bucket",
        Key: `my key${id}`,
        Body: stream,
        ContentLength: buffer.length,
    };
    
    try {
        const response = await s3Client.send(new PutObjectCommand(input));
        console.log(response.$metadata.httpStatusCode)
    } catch (error) {
        console.log(error)
    }
}

const promises = []

for (let i = 0; i < 20000; i++) {
    promises.push(new Promise((resolve, reject) =>{
        return uploadStream(i)
    }))
}
Promise.all(promises).then(() => {
    console.log(`Batch ${i + 1} of ${batches} completed.`);
});

I also tried unplugging my wifi to simulate a connection error, but that didnt work either. The request was not retried.

This will need to get a deeper look. Adding to our backlog.
Thanks again,
Ran

@kuhe
Copy link
Contributor

kuhe commented Dec 1, 2023

@RanVaknin sample repro code in smithy-lang/smithy-typescript#1092

@jeanbmar a fix is queued for release sometime next week. We are not going to attempt to buffer the request at this time due to complexity and unpredictable behavior. Any failures submitting a stream will be thrown instead of retried.

@kuhe kuhe added pending-release This issue will be fixed by an approved PR that hasn't been released yet. and removed queued This issues is on the AWS team's backlog labels Dec 1, 2023
@Mathie01
Copy link

Same problem, and the fix does not fix the bug
My S3Client is blocked in all send() call

@kuhe kuhe removed the pending-release This issue will be fixed by an approved PR that hasn't been released yet. label Jan 9, 2024
@kuhe
Copy link
Contributor

kuhe commented Jan 9, 2024

@Mathie01 provide a reproduction code sample.

@kuhe kuhe added the response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days. label Jan 9, 2024
Copy link

This issue has not received a response in 1 week. If you still think there is a problem, please leave a comment to avoid the issue from automatically closing.

@github-actions github-actions bot added closing-soon This issue will automatically close in 4 days unless further comments are made. closed-for-staleness and removed closing-soon This issue will automatically close in 4 days unless further comments are made. labels Jan 17, 2024
Copy link

github-actions bot commented Feb 4, 2024

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug This issue is a bug. closed-for-staleness p2 This is a standard priority issue response-requested Waiting on additional info and feedback. Will move to \"closing-soon\" in 7 days.
Projects
None yet
Development

No branches or pull requests

4 participants