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(nextjs): Don't share I/O resources in between requests #6980

Merged
merged 4 commits into from Jan 31, 2023

Conversation

lforst
Copy link
Member

@lforst lforst commented Jan 30, 2023

Fixes #6975

Vercel edge routes don't allow shared access to I/O resources (req, res objects etc.) between requests. Our flushing logic inside the default PromiseBuffer was inherently doing that by processing outgoing fetch requests, even after the Edge route terminated.

This PR aims to resolve this issue by making the flushing of Sentry events atomic to a particular request.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2023

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 19.85 KB (+0.02% 🔺)
@sentry/browser - ES5 CDN Bundle (minified) 61.55 KB (0%)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 18.52 KB (+0.02% 🔺)
@sentry/browser - ES6 CDN Bundle (minified) 54.85 KB (0%)
@sentry/browser - Webpack (gzipped + minified) 20.24 KB (0%)
@sentry/browser - Webpack (minified) 66.25 KB (0%)
@sentry/react - Webpack (gzipped + minified) 20.27 KB (0%)
@sentry/nextjs Client - Webpack (gzipped + minified) 47.56 KB (0%)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 26.78 KB (+0.01% 🔺)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 25.06 KB (+0.01% 🔺)
@sentry/replay ES6 CDN Bundle (gzipped + minified) 44.15 KB (+0.22% 🔺)
@sentry/replay - Webpack (gzipped + minified) 38.9 KB (+0.25% 🔺)
@sentry/browser + @sentry/tracing + @sentry/replay - ES6 CDN Bundle (gzipped + minified) 61.48 KB (+0.17% 🔺)

@lforst lforst marked this pull request as ready for review January 30, 2023 16:59
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

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

We could also just update makePromiseBuffer to just clear on drain right? And just add an option to pass into the constructor?

* We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests
* and responses, and the normal PromiseBuffer inherently buffers stuff inbetween incoming requests.
*/
class IsolatedPromiseBuffer {
Copy link
Member

Choose a reason for hiding this comment

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

l: should this implement the PromiseBuffer interface?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is not exported - should we do that?

Copy link
Member

Choose a reason for hiding this comment

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

Ah maybe we just leave it then, that's more of a change then I thought.

// If we ever remove it from the interface we should also remove it here.
public $: Array<PromiseLike<TransportMakeRequestResponse>> = [];

private _taskProducers: (() => PromiseLike<TransportMakeRequestResponse>)[] = [];
Copy link
Member

Choose a reason for hiding this comment

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

l: Why do we use _taskProducers instead of $? $ is only used by tests iirc.

Copy link
Member Author

Choose a reason for hiding this comment

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

They're of a different type. We need to store producers in this case because we do not want to actually create the promises right away. Producers are () => Promise whereas $ is just an array of Promises.

Copy link
Member

Choose a reason for hiding this comment

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

I see - so we are essentially making all outgoing fetch requests lazy, because we want to only send them when drain is called.

I guess this works for now, but there is always the risk that data does not get sent to Sentry because of this right? This is an important caveat that should be put in as a code comment, that we knew about this tradeoff but we still proceeded with this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Generally, we flush on every edge function invocation so we should be sending everything. This implementation has the drawback though that there is a hard cap on the amount of events sent for any given edge function invocation: https://github.com/getsentry/sentry-javascript/pull/6980/files#diff-750cbb0cd6b1f70c9ca7a215d0e9ef1e0268b822bf194ccf49feddb7a6341d64R19-R20

}

return createTransport(options, makeRequest);
return createTransport(options, makeRequest, new IsolatedPromiseBuffer(options.bufferSize));
Copy link
Member

Choose a reason for hiding this comment

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

m: If we export IsolatedPromiseBuffer, we can add unit tests for drain, I think we should have that to prevent regressions.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add some!

@lforst
Copy link
Member Author

lforst commented Jan 31, 2023

We could also just update makePromiseBuffer to just clear on drain right? And just add an option to pass into the constructor?

If we can avoid bloating the API for this fix I would probably do so. We need to rethink this part of the SDK anyhow when we build a runtime-agnostic one. Do you have a stronger opinion here?

Edit: Thought about this more, doing that will not fix the problem. Two request handlers may write to that promise buffer and one flushes both fetch requests - again sharing the response.

Copy link
Member

@AbhiPrasad AbhiPrasad 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 the tests!

// If we ever remove it from the interface we should also remove it here.
public $: Array<PromiseLike<TransportMakeRequestResponse>> = [];

private _taskProducers: (() => PromiseLike<TransportMakeRequestResponse>)[] = [];
Copy link
Member

Choose a reason for hiding this comment

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

I see - so we are essentially making all outgoing fetch requests lazy, because we want to only send them when drain is called.

I guess this works for now, but there is always the risk that data does not get sent to Sentry because of this right? This is an important caveat that should be put in as a code comment, that we knew about this tradeoff but we still proceeded with this.

@lforst lforst merged commit 66ebf2f into master Jan 31, 2023
@lforst lforst deleted the lforst-fix-vercel-edge-function-io-sharing branch January 31, 2023 10:15
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.

Next.js reuses I/O object on Vercel Edge routes
2 participants