Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Fix SocketAsyncEventArgs' handling of ExecutionContext#30712

Merged
stephentoub merged 1 commit intodotnet:masterfrom
stephentoub:socketsecflow
Jul 3, 2018
Merged

Fix SocketAsyncEventArgs' handling of ExecutionContext#30712
stephentoub merged 1 commit intodotnet:masterfrom
stephentoub:socketsecflow

Conversation

@stephentoub
Copy link
Member

SocketAsyncEventArgs has a few issues with ExecutionContext, presumably stemming from the fact that capturing ExecutionContext in .NET Framework is not a cheap operation. As a result, when this code was written, it was optimized for avoiding calls to ExecutionContext.Capture. The SAEA tries to hold onto a captured ExecutionContext for as long as possible, only re-capturing when either the SAEA is used with a different socket instance or when an event handler is changed. That has several problems, though. First, it largely violates the purpose of ExecutionContext, which is to flow information from the point where the async operation begins to the continuation/callback, but if the context is only being captured when the Socket or handler is changed, then the context isn't actually tied to the location where the async operation begins, and that means that data like that in an AsyncLocal doesn't properly flow across the async point. Second, it means that the SocketAsyncEventArgs (the whole purpose of which is to cache it) can end up keeping state in an ExecutionContext alive well beyond when it should be kept alive, because the SocketAsyncEventArgs is holding onto the ExecutionContext instance until either the Socket or handler is changed.

This commit fixes this behavior. Since ExecutionContext.Capture in .NET Core is relatively cheap (no allocation, primarily just a ThreadStatic access), we now just always capture the context when starting an operation, and then clear it out when completing the operation.

Fixes #30670
Depends on dotnet/coreclr#18670 (the new SocketsHttpHandler test won't pass until that's merged and consumed into corefx)

cc: @geoffkizer, @davidsh, @kouvel

SocketAsyncEventArgs has a few issues with ExecutionContext, presumably stemming from the fact that capturing ExecutionContext in .NET Framework is not a cheap operation.  As a result, when this code was written, it was optimized for avoiding calls to ExecutionContext.Capture.  The SAEA tries to hold onto a captured ExecutionContext for as long as possible, only re-capturing when either the SAEA is used with a different socket instance or when an event handler is changed.  That has several problems, though.  First, it largely violates the purpose of ExecutionContext, which is to flow information from the point where the async operation begins to the continuation/callback, but if the context is only being captured when the Socket or handler is changed, then the context isn't actually tied to the location where the async operation begins, and that means that data like that in an AsyncLocal doesn't properly flow across the async point.  Second, it means that the SocketAsyncEventArgs (the whole purpose of which is to cache it) can end up keeping state in an ExecutionContext alive well beyond when it should be kept alive, because the SocketAsyncEventArgs is holding onto the ExecutionContext instance until either the Socket or handler is changed.

This commit fixes this behavior.  Since ExecutionContext.Capture in .NET Core is relatively cheap (no allocation, primarily just a ThreadStatic access), we now just always capture the context when starting an operation, and then clear it out when completing the operation.
@stephentoub stephentoub added this to the 3.0 milestone Jun 28, 2018
@stephentoub stephentoub self-assigned this Jun 28, 2018
@smbecker
Copy link

Any chance that this will be back-ported to release/2.1? Or release/2.2 at the very least?

@stephentoub
Copy link
Member Author

Any chance that this will be back-ported to release/2.1? Or release/2.2 at the very least?

I don't think this would make the bar for 2.1, though @danmosemsft can correct me if I'm wrong.

@karelz, can you comment on 2.2?

@karelz
Copy link
Member

karelz commented Jun 30, 2018

What is the impact of the bug? Which scenarios are impacted? How bad is it? How common are the scenarios?
The bug bar for 2.2 is not clear yet. @danmosemsft likely knows more at this point.

@stephentoub
Copy link
Member Author

stephentoub commented Jun 30, 2018

What is the impact of the bug?

If there are objects in AsyncLocals when you make an http request that requires opening a new http connection, those objects may be kept alive until the next time that same pooled state in the connection manager is reused to open another connection.

Fully addressing it also requires dotnet/coreclr#18670.

@smbecker
Copy link

smbecker commented Jul 1, 2018

In our case, it caused a pretty severe memory leak that crashed our servers since we are opening connections to a variety of sources all the time. However, for now we are working around it using https://github.com/dotnet/corefx/issues/30670#issuecomment-400776344.

@smbecker
Copy link

smbecker commented Jul 1, 2018

Another work around is to disable the SocketsHttpHandler using environment variables but that is not very desirable.

FinishOperationSyncFailure(socketError, bytesTransferred, flags);

if (_context == null)
if (context == null)

Choose a reason for hiding this comment

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

Since this pattern is repeated a couple times, couldn't we encapsulate it into something like "InvokeCompletion"?

And couldn't we move the clearing of the execution context there?

Copy link
Member Author

Choose a reason for hiding this comment

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

And couldn't we move the clearing of the execution context there?

We need to clear it even on synchronous completion, in which case we're not invoking a callback.

Choose a reason for hiding this comment

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

Ok.

@stephentoub
Copy link
Member Author

it caused a pretty severe memory leak that crashed our servers since we are opening connections to a variety of sources all the time

This is surprising to me. We only cache Environment.ProcessorCount event arg instances, so at worst I'd expect that many ExecutionContexts to be kept alive beyond when they could otherwise be collected. Can you elaborate on how this resulted in such a severe leak?

@stephentoub stephentoub merged commit 851a53b into dotnet:master Jul 3, 2018
@stephentoub stephentoub deleted the socketsecflow branch July 3, 2018 17:21
@benaadams
Copy link
Member

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…#30712)

SocketAsyncEventArgs has a few issues with ExecutionContext, presumably stemming from the fact that capturing ExecutionContext in .NET Framework is not a cheap operation.  As a result, when this code was written, it was optimized for avoiding calls to ExecutionContext.Capture.  The SAEA tries to hold onto a captured ExecutionContext for as long as possible, only re-capturing when either the SAEA is used with a different socket instance or when an event handler is changed.  That has several problems, though.  First, it largely violates the purpose of ExecutionContext, which is to flow information from the point where the async operation begins to the continuation/callback, but if the context is only being captured when the Socket or handler is changed, then the context isn't actually tied to the location where the async operation begins, and that means that data like that in an AsyncLocal doesn't properly flow across the async point.  Second, it means that the SocketAsyncEventArgs (the whole purpose of which is to cache it) can end up keeping state in an ExecutionContext alive well beyond when it should be kept alive, because the SocketAsyncEventArgs is holding onto the ExecutionContext instance until either the Socket or handler is changed.

This commit fixes this behavior.  Since ExecutionContext.Capture in .NET Core is relatively cheap (no allocation, primarily just a ThreadStatic access), we now just always capture the context when starting an operation, and then clear it out when completing the operation.

Commit migrated from dotnet/corefx@851a53b
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConnectHelper.ConnectEventArgs shouldn't capture AsyncLocals

5 participants