-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(fetch): properly release fetch during long-lived stream handling #13967
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
Conversation
…tream is cancelled - Modified `resolveResponse` to properly handle the cancellation of child streams by overriding the `cancel` method on the parent response's body. - Updated unit tests to cover scenarios for both successful stream resolution and error cases involving stream cancellation.
I saw some errors in the CI. I'll deal with them later |
It seems that there are no further errors related to this PR. Can you please help confirm this, @chargome . |
Response was already in the code before, but now it's triggering a TS2304 error. Do you have any idea why this is happening? It looks like only the TS2304 error and the file size check remain to be solved now. |
@Lei-k not sure, but the same error is also crashing |
All tests have passed on my side. Please check, thanks! |
Hi, @chargome There’s one remaining bundle size check that failed due to the size limit. Could you assist me in adjusting the size, or do you have any suggestions for resolving this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @Lei-k sorry for the silence on this one, could you please have a look at my review and potentially also merge develop into your pr? We had some changes to our package structure lately, e.g. @sentry/types got deprecated.
if (res && res.body) { | ||
const body = res.body; | ||
const responseReader = body.getReader(); | ||
async function resloveReader(reader: WebReadableStreamDefaultReader, onFinishedResolving: () => void): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async function resloveReader(reader: WebReadableStreamDefaultReader, onFinishedResolving: () => void): Promise<void> { | |
async function resolveReader(reader: WebReadableStreamDefaultReader, onFinishedResolving: () => void): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for reply. I’m merging the develop branch now and will check for any issues caused by the recent package structure changes.
* | ||
* Export For Test Only | ||
*/ | ||
export function resolveResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we might be introducing a potential memory leak within this function. Previously we were cancelling the resolving after a max timeout plus a timeout for individual chunk resolving. Could you explain why you removed these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
Based on WHATWG specification, getReader
returns a ReadableStreamDefaultReader
. When using an asynchronous method to repeatedly call read
, the ReadableStreamDefaultReader
will respond as long as data is available, and it will appropriately handle cases where no data is available or an error occurs. A memory leak would only occur if responses are not properly handled—for example, using a recursive approach to read data, which is also noted in the WHATWG specification.
On the other hand, the previous implementation, which added a timeout for each chunk, could lead to issues. If the network is unstable or experiences jitter, chunks might not be retrieved within the specified timeout, causing the program to incorrectly cancel the reader prematurely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore, adding a timeout to address a memory leak issue doesn’t make sense. If a timeout occurs, it means there wasn’t enough data available for us to read within the given time frame. If there isn’t sufficient data to read, there’s no accumulation of unhandled data, and therefore, a memory leak cannot happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I am Here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @chargome
I can understand your concerns.
Here are the patches I applied to Sentry 8.30.0, which are similar to this PR.
After running in production for more than 2 months, we have not encountered any memory leak issues.
The memory usage remains stable, and all monitored streams are properly canceled and released as expected.
I’ve also attached the memory test results and patches for reference.
@sentry+utils+8.30.0.patch
@sentry-internal+replay+8.30.0.patch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Lei-k could you address this concern?
Closing this one for now, we might need to re-visit the entire fetch instrumentation for streams |
body.cancel().then(null, () => { | ||
// noop on error | ||
}); | ||
}, 5000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think removing the 5-second chunkTimeout is fine, but if there is any reason to keep it, please let me know. Thanks!
packages/types/src/whatwg/fetch.ts
Outdated
arrayBuffer(): Promise<ArrayBuffer>; // Reads response body as ArrayBuffer | ||
blob(): Promise<object>; // Reads response body as Blob | ||
formData(): Promise<object>; // Reads response body as FormData | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we need to define the DOM types, maybe according to the WHATWG spec?
if (res && res.body) { | ||
const body = res.body; | ||
const responseReader = body.getReader(); | ||
async function resloveReader(reader: WebReadableStreamDefaultReader, onFinishedResolving: () => void): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank for reply. I’m merging the develop branch now and will check for any issues caused by the recent package structure changes.
* | ||
* Export For Test Only | ||
*/ | ||
export function resolveResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
Based on WHATWG specification, getReader
returns a ReadableStreamDefaultReader
. When using an asynchronous method to repeatedly call read
, the ReadableStreamDefaultReader
will respond as long as data is available, and it will appropriately handle cases where no data is available or an error occurs. A memory leak would only occur if responses are not properly handled—for example, using a recursive approach to read data, which is also noted in the WHATWG specification.
On the other hand, the previous implementation, which added a timeout for each chunk, could lead to issues. If the network is unstable or experiences jitter, chunks might not be retrieved within the specified timeout, causing the program to incorrectly cancel the reader prematurely.
* | ||
* Export For Test Only | ||
*/ | ||
export function resolveResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore, adding a timeout to address a memory leak issue doesn’t make sense. If a timeout occurs, it means there wasn’t enough data available for us to read within the given time frame. If there isn’t sufficient data to read, there’s no accumulation of unhandled data, and therefore, a memory leak cannot happen.
* | ||
* Export For Test Only | ||
*/ | ||
export function resolveResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I am Here.
* | ||
* Export For Test Only | ||
*/ | ||
export function resolveResponse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @chargome
I can understand your concerns.
Here are the patches I applied to Sentry 8.30.0, which are similar to this PR.
After running in production for more than 2 months, we have not encountered any memory leak issues.
The memory usage remains stable, and all monitored streams are properly canceled and released as expected.
I’ve also attached the memory test results and patches for reference.
@sentry+utils+8.30.0.patch
@sentry-internal+replay+8.30.0.patch
Hi @chargome, |
Hi,
I saw that the
trackFetchStreamPerformance
option was introduced in PR #13951 to bypass this issue, and Issue #13950 was closed as a result. However, this hasn't fully resolved the problem, so I attempted to address it in this PR.Here is my modified test result after applying the changes: