-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Prevent flowing ExecutionContext #1825
base: main
Are you sure you want to change the base?
Conversation
Prevent flowing the Execution Context to background threads. Without this, it means values from AsyncLocal and CallContext will flow/leak into background threads which should be avoided because it may inadvertently be capturing some ambient contexts like DbContext, etc... which can have unwanted side affects. In most cases anything stored in AsyncLocal is also not thread safe and if it's a mutable object it means concurrent threads can be modifying it which also may cause unwanted side affects. I don't think there's a use-case for flowing the execution context into a background thread since the execution in these threads should be entirely isolated. I put together a gist showing all the different ways the AsyncLocal/CallContext values flow to child threads here https://gist.github.com/Shazwazza/c127e8c567ab505d146e54ac802a9945 I think this is the only place where the created threads are actually started but I could be wrong. Calling SuppressFlow is only required where the thread is started.
Just downloaded the source since the build is breaking and can see there's a other places that start threads too. Will update. |
Seems there's one test failing: SqlServerTimeoutJobFacts.cs:line 240. Unfortunately I'm unable to get this project to build locally in VS (latest) and have tried dotnet build as per docs. Have run out of time today but will see what I can do this week. |
Hello @Shazwazza Are you aware of any broken real world use cases because of this? In our AspNetCore 3.1 app, we use Hangfire and NLog for logging. The minimum log level is set to We don't use
|
Hi, I submitted this based on recent research I as doing in Umbraco CMS which used to use CallContext and now uses AsyncLocal to store an ambient context. Some things in netcore use AsyncLocal too like HttpContextAccessor, ActionContextAccessor and others. My reasoning for submitting this is because an AsyncLocal (or CallContext) value should only live within it's own execution context whereas a background thread is it's own isolated context. The docs for Hangfire actually state that the work should be done in it's own execution context (i.e. https://docs.hangfire.io/en/latest/getting-started/index.html) but that is actually not the case because without suppressing the flow, the execution context will flow into these background threads. This becomes problematic if you happen to have anything that is mutable stored in AsyncLocal or CallContext. The general rule is that you "shouldn't" do that because strange things might happen - like if the execution context if flowed to a background long lived thread. An ugly example of this is if there is an IDisposable object in AsyncLocal which is created on the parent thread, then a background task is created and launched and then it is disposed on the parent thread. Meanwhile, the child thread continues to execute and inadvertently accesses a disposed object. Depending on what libraries do with AsyncLocal/CallContext and if they use an Ambient context design pattern, then flowing this state to a background thread can be problematic. In the case of Umbraco, if an Umbraco Scope is created and a hangfire task is executed then that Scope (potentially along with Sql connection/transactions) will leak and disposed objects may be accessed. EFCore appears to use AsyncLocal for a few things as boolean values (non mutable) but those values will still flow into the child tasks created by HangFire without SuppressFlow calls. I'm unsure about the |
Thanks for the more detailed explanation. As for Every Hangfire's running job has a
It allows you to get the current job |
Ah I see. In the case of passing the PerformingContext via AsyncLocal, then yes using SuppressFlow() will suppress that value from being available in child threads. I understand that it's probably a convenience way to access the Perhaps Hangfire needs some way to specify if the ExecutionContext should be suppressed or not when spawning new threads since there's not really any way to control this outside of Hangfire since that needs to be done in the main thread where the child thread is started. I don't think suppressing ExecutionContext can be done with Hangfire filters since from what I can tell in code these don't execute specifically before/after the thread is started. Potentially filters do do supply a way to work around the issue of preventing AsyncLocal/CallContext values from flowing to the child threads but I'm unsure which type of filter that could be since it would need to be a filter that is executing on the child thread but before the job code is executed to provide a way to null out specific AsyncLocal/CallContext that should not be flowed. |
Thank you for the information – I didn't know that execution context is flowing even when starting a new thread calling the At the same time I understand that theoretically it will be useful to provide such an isolation. But to avoid any difficulties and since this can theoretically produce breaking changes in very rare scenarios, I'd suggest to implement this in the Also I think it will be useful to implement some state reset method that will be called after each background job is completed. |
Hi @odinserj , thanks for the info!
aha! that's great, that is a key part to all of this then. It's my first time navigating through this code and didn't have time to step through it or anything so couldn't determine how/when the threads were spawned. So yes then in theory AsynLocal or CalContext could only leak to those threads in unique scenarios where someone is booting hangfire a little different I guess. Happy to rebase on dev, i can do that next week i think. |
Prevent flowing the Execution Context to background threads. Without this, it means values from AsyncLocal and CallContext will flow/leak into background threads which should be avoided because it may inadvertently be capturing some ambient contexts like DbContext, etc... which can have unwanted side affects. In most cases anything stored in AsyncLocal is also not thread safe and if it's a mutable object it means concurrent threads can be modifying it which also may cause unwanted side affects. I don't think there's a use-case for flowing the execution context into a background thread since the execution in these threads should be entirely isolated.
I put together a gist showing all the different ways the AsyncLocal/CallContext values flow to child threads here https://gist.github.com/Shazwazza/c127e8c567ab505d146e54ac802a9945
I think this is the only place where the created threads are actually started but I could be wrong. Calling SuppressFlow is only required where the thread is started.