Skip to content

Start connections on a fresh ExecutionContext #29995

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

Closed
wants to merge 2 commits into from

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Feb 8, 2021

  • This change removes the ExecutionContext from new connections, this removes the possibility of capturing request scoped async locals and flowing them to the hub. This will ensure that the IHttpContextAccessor and Activity for hub invocations is always null and aren't carried over from the incoming request.
  • This will break the ability to pass state to the hub via an async local from middleware but I'm OK with that.

Related to #29846

This breaks the localization middleware for SignalR and for Blazor scenarios

PS: I need to write a test but nothing works in visual studio...

cc @stephentoub you were asking about dotnet/runtime#47998, this is the change.

- This change removes the ExecutionContext from new connections, this removes the possibility of capturing request scoped async locals and flowing them to the hub. This will ensure that the IHttpContextAccessor and Activity for hub invocations is always null and aren't carried over from the incoming request.
@ghost ghost added the area-signalr Includes: SignalR clients and servers label Feb 8, 2021
Comment on lines +555 to +561
_connectionDelegate = connectionDelegate;

// Queue the connection for execution
ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);

// Running this in an async method turns sync exceptions into async ones
await connectionDelegate(this);
// Wait for that task to finish signaling the end of the application execution
await _connectionDelegateTcs.Task;
Copy link
Member

Choose a reason for hiding this comment

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

Why do you choose this route rather than:

await AwaitableThreadPool.Yield();

Task t;
using (ExecutionContext.SuppressFlow())
    t = connectionDelegate(this);

await t;

?

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 generally don't like using suppress flow. I prefer the more explicit call that doesn't capture the ExecutionContext.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also the fact that SuppressFlow throws if the flow is already suppressed makes it annoying. You need to always code around the fact that somebody further up the stack might have already suppressed it.

Copy link
Member

Choose a reason for hiding this comment

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

You need to always code around the fact that somebody further up the stack might have already suppressed it.

There's no user code higher up the stack here. You shouldn't need to check or need additional code around it.

I generally don't like using suppress flow

Understood in general. In practice here, though, the code using SuppressFlow would seem to be less convoluted and (I'd need to measure to know for sure) less expensive.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's no user code higher up the stack here. You shouldn't need to check or need additional code around it.

There is. That's where the async locals come from and why this change is happening in the first place.

Understood in general. In practice here, though, the code using SuppressFlow would seem to be less convoluted and (I'd need to measure to know for sure) less expensive.

I think the code does look less convoluted but we use this pattern in other places in ASP.NET Core and it's a stylistic thing. It's the same reason I prefer using *Unsafe APIs instead of suppressing the flow in various cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

Are you saying:

ExecutionContext.SuppressFlow();

await Task.Yield();

ExecutionContext.SuppressFlow();

Works?

Copy link
Member Author

Choose a reason for hiding this comment

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

The other thing I don't like about that is that it's too subtle vs a call that says "This will not capture the execution context" and the code sucks a little bit because. You know what I really want? Task.UnsafeRun. That would work here as well.

Copy link
Member

@stephentoub stephentoub Feb 8, 2021

Choose a reason for hiding this comment

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

Are you saying ... works?

Forgetting for the moment the fact that Task.Yield() respects current SynchronizationContext as well (which your AwaitableThreadPool.Yield() does not), yes... suppression itself doesn't flow, and the thread pool ensures a work item can't leak a change to suppression to the next work item.

Copy link
Member

Choose a reason for hiding this comment

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

You know what I really want? Task.UnsafeRun. That would work here as well.

Why do you want to queue to the ThreadPool? I thought this this change was strictly about the ExecutionContext.

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 this change removed the old await AwaitableThreadPool.Yield(); Never mind me.

@davidfowl davidfowl added the breaking-change This issue / pr will introduce a breaking change, when resolved / merged. label Feb 8, 2021
Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

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

Can you file an issue to track the follow up work on this? I thought one of the tests was failing with this change.

@davidfowl
Copy link
Member Author

I haven't fixed it. I wanted to look at it before merging

@BrennanConroy
Copy link
Member

So any logging wont have the connection id associated with it? Do we want to manually add it back?

@davidfowl
Copy link
Member Author

So any logging wont have the connection id associated with it? Do we want to manually add it back?

Which logging exactly? The Kestrel connection id would be lost and the activity would be lost.

@BrennanConroy
Copy link
Member

SignalR's connection id we put in the logging scope

logScope.ConnectionId = connection?.ConnectionId;

@davidfowl
Copy link
Member Author

SignalR's connection id we put in the logging scope

argh good catch.

@davidfowl davidfowl closed this Feb 13, 2021
@dougbu dougbu deleted the davidfowl/strip-executioncontext branch August 21, 2021 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-signalr Includes: SignalR clients and servers breaking-change This issue / pr will introduce a breaking change, when resolved / merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants