There are a number of questions on SO about how to avoid deadlocks in async code (for example, HttpClient methods) being called from sync code, like this. I'm aware of the various ways to avoid these deadlocks.
In contrast, I'd like to learn about strategies to aggravate or trigger these deadlocks in faulty code during testing.
Here's an example bit of bad code that recently caused problems for us:
public static string DeadlockingGet(Uri uri)
{
using (var http = new HttpClient())
{
var response = http.GetAsync(uri).Result;
response.EnsureSuccessStatusCode();
return response.Content.ReadAsStringAsync().Result;
}
}
It was being called from an ASP.NET app, and thus had a non-null value of SynchronizationContext.Current, which provided the fuel for a potential deadlock fire.
Aside from blatantly misusing HttpClient, this code deadlocked in one of our company's servers... but only sporadically.
My attempt to repro deadlock
I work in QA, so I tried to repro the deadlock via a unit test that hits a local instance of Fiddler's listener port:
public class DeadlockTest
{
[Test]
[TestCase("http://localhost:8888")]
public void GetTests(string uri)
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
var context = SynchronizationContext.Current;
var thread = Thread.CurrentThread.ManagedThreadId;
var result = DeadlockingGet(new Uri(uri));
var thread2 = Thread.CurrentThread.ManagedThreadId;
}
}
A couple things to note:
By default, a unit test has a null
SynchronizationContext.Current, and so.Resultcaptures the context ofTaskScheduler, which is the thread pool context. Therefore I useSetSynchronizationContextto set it to a specific context, to more closely emulate what happens in an ASP.NET or UI context.I've configured Fiddler to wait a while (~1 minute) before responding back. I've heard from coworkers that this may help repro the deadlock (but I have no hard evidence this is the case).
I've ran it with debugger to make sure that
contextis non-nullandthread == thread2.
Unfortunately, I've had no luck triggering deadlocks with this unit test. It always finishes, no matter how long the delay in Fiddler is, unless the delay exceeds the 100-second default Timeout of HttpClient (in which case it just blows up with an exception).
Am I missing an ingredient to ignite a deadlock fire? I'd like to repro the deadlocks, just to be positive that our eventual fix actually works.
You have not been able to reproduce the issue because
SynchronizationContextitself does not mimic the context installed by ASP.NET. The baseSynchronizationContextdoes no locking or synchronization, but the ASP.NET context does: BecauseHttpContext.Currentis not thread-safe nor is it stored in theLogicalCallContextto be passed between threads, theAspNetSynchronizationContextdoes a bit of work to a. restoreHttpContext.Currentwhen resuming a task and b. lock to ensure that only one task is running for a given context.A similar problem exists with MVC: http://btburnett.com/2016/04/testing-an-sdk-for-asyncawait-synchronizationcontext-deadlocks.html
The approach given there is to test your code with a context which ensures that
SendorPostis never called on the context. If it is, this is an indication of the deadlocking behavior. To resolve, either make the method treeasyncall the way up or useConfigureAwait(false)somewhere, which essentially detaches the task completion from the sync context. For more information, this article details when you should useConfigureAwait(false)