Search code examples
c#asp.net-coreasync-awaittask-parallel-librarydeadlock

Strange deadlock situation in ASP.NET Core Controller


In my ASP.NET Core application I have a a seemingly very simple action. It awaits some value from an asynchronous method and then returns it as an OK-result:

public async Task<IActionResult> GetNextCommand()
{
    var command = await LongPollManager.Instance.GetNextCommand(HttpContext.RequestAborted);
    return Ok(command);
}

When I call this route with some HTTP client I can verify in the debugger that this asynchronous method returns the desired value and passes it to the Ok method:

enter image description here

If I let the debugger continue I would expect to get the result in my HTTP client. But the client never receives a response.

When I then break the debugger I can see that the thread is blocked on some internal lock. You can see this in the current screenshot:

enter image description here

This behavior can be seen only since I made some changes in my LongPollManager class (which is actually quite complex and uses TaskCompletionSources and ConcurrentDictionaries, and SemaphoreSlims internally).

The thing that puzzles me is that it's actually not my own GetNextCommand method which is blocking, but the blocking seems to happen inside ASP.NET Core. Once execution is in line 29 and I got my command object, all the complicated asynchronous stuff of my LongPollManager class is over and I don't see how anything I change in LongPollManager can prevent ASP.NET Core from properly finishing the request.

What could it be that ASP.NET Core is waiting here for? How can my code (which runs to line 29 without a deadlock) cause such a deadlock situation?


Solution

  • As mentioned in the comments

    But with my latest changes it also contains a public Task property. Do you think this could have any side-effects when ASP.NET Core tries to serialize it?

    Yes it will.

    The framework will try to invoke the property to get the value for serialization. And as the property returns a Task, will most likely try to serialize the task's .Result property synchronously which would lead to your dead lock.

    Mixing async and blocking calls like .Result and .Wait() can cause deadlocks and should be avoided.

    Reference Async/Await - Best Practices in Asynchronous Programming

    Actions should return simple POCOs that have no side effects when serialized.

    The public Task property should be hidden from the serailizer, ignored via attributes or should be converted to methods that do not get invoked when the model is serialized.