-1

In the following code, a deadlock happens on the Task.WhenAll line:

[Fact]
public async Task GetLdapEntries_ReturnsLdapEntries()
{
    var ldapEntries = _fixture.CreateMany<LdapEntryDto>(2).ToList();
    var creationTasks = new List<Task>();
    foreach (var led in ldapEntries)
    {
        var task = _attributesServiceClient.CreateLdapEntry(led);
        creationTasks.Add(task);
    }
    await Task.WhenAll(creationTasks); // <- deadlock here

    var result = await _ldapAccess.GetLdapEntries();

    result.Should().BeEquivalentTo(ldapEntries);
}

public async Task<LdapEntryDto> CreateLdapEntry(LdapEntryDto ldapEntryDto)
{
    using (var creationResponse = await _httpClient.PostAsJsonAsync<LdapEntryDto>("", ldapEntryDto))
    {
        if (creationResponse.StatusCode == HttpStatusCode.Created)
        {
            return await creationResponse.Content.ReadAsAsync<LdapEntryDto>();
        }

        return null;
    }
}

The xUnit test attempts to create test data asynchronously by calling an asynchronous method that itself awaits a response from a web service. _httpClient is a real HttpClient created off an in-memory TestServer via TestServer.CreateClient().

When setting a breakpoint on the using line in the CreateLdapEntry method, it is hit twice. A breakpoint on the status code check is never hit. When breaking on Task.WhenAll() and inspecting creationTasks, both tasks are in state WaitingForActivation:

creationTasks
Count = 2
    [0]: Id = 32, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"
    [1]: Id = 33, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}"

When not using Task.WhenAll() but instead awaiting each task individually, no deadlock occurs:

foreach (var led in ldapEntries)
{
    await _attributesServiceClient.CreateLdapEntry(led);
}

I am aware that a similar question has been asked and answered, however the code examples there make use of .Result, not of await Task.WhenAll().

I'd like to understand why that deadlock is occuring when using Task.WhenAll().

EDIT: Added Call Stack of locked threads

Not Flagged     3992    11  Worker Thread   Worker Thread   Microsoft.AspNetCore.Routing.dll!Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke
                        [Managed to Native Transition]
                        Microsoft.AspNetCore.Routing.dll!Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext)
                        ShibbolethAttributes.Service.dll!RoleManager.Service.Middleware.ApiKeyHandlerMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context) Line 38
                        Microsoft.AspNetCore.Diagnostics.dll!Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context)
                        System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Startd__7>(ref Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.d__7 stateMachine)
                        Microsoft.AspNetCore.Diagnostics.dll!Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context)
                        Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.Internal.HostingApplication.ProcessRequestAsync(Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context context)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.TestServer.ApplicationWrapper.ProcessRequestAsync(Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context context)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.HttpContextBuilder.SendAsync.AnonymousMethod__0()
                        System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Startc__DisplayClass10_0.b__0>d stateMachine)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.HttpContextBuilder.SendAsync.AnonymousMethod__0()
                        System.Private.CoreLib.dll!System.Threading.Tasks.Task.InnerInvoke()
                        System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
                        System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)
                        System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()

Not Flagged     1496    10  Worker Thread   Worker Thread   Microsoft.AspNetCore.Routing.dll!Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke
                        Microsoft.AspNetCore.Routing.dll!Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext)
                        ShibbolethAttributes.Service.dll!RoleManager.Service.Middleware.ApiKeyHandlerMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context) Line 38
                        Microsoft.AspNetCore.Diagnostics.dll!Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context)
                        System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Startd__7>(ref Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.d__7 stateMachine)
                        Microsoft.AspNetCore.Diagnostics.dll!Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext context)
                        Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.Internal.HostingApplication.ProcessRequestAsync(Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context context)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.TestServer.ApplicationWrapper.ProcessRequestAsync(Microsoft.AspNetCore.Hosting.Internal.HostingApplication.Context context)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.HttpContextBuilder.SendAsync.AnonymousMethod__0()
                        System.Private.CoreLib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Startc__DisplayClass10_0.b__0>d stateMachine)
                        Microsoft.AspNetCore.TestHost.dll!Microsoft.AspNetCore.TestHost.HttpContextBuilder.SendAsync.AnonymousMethod__0()
                        System.Private.CoreLib.dll!System.Threading.Tasks.Task.InnerInvoke()
                        System.Private.CoreLib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state)
                        System.Private.CoreLib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot)
                        System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()
Thaoden
  • 3,460
  • 3
  • 33
  • 45
  • Is `buildException` ever hit? If so, please post the code – canton7 Feb 20 '19 at 16:36
  • It would be helpful to include all relevant code (like `buildException`). [Also I would recommend appending Async](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/). – Erik Philips Feb 20 '19 at 16:37
  • `I'd like to understand why that deadlock is occuring when using Task.WhenAll().` Looks like you have a context contention between the tasks that are executing at the same time. In your working code each task processes on it's own so no race conditions can occur there between tasks. – Igor Feb 20 '19 at 16:37
  • `buildException` is never hit, when instead of `throw`ing I just `return null`, nothing changes. Changed the code in question accordingly. – Thaoden Feb 20 '19 at 16:39
  • What data type is `_httpClient`? Is it an actual `HttpClient`, or a mock? – canton7 Feb 20 '19 at 16:42
  • @canton7 It's an actual `HttpClient`, obtained via `TestServer.CreateClient()`, `TestServer` being an in-memory version of a web service. – Thaoden Feb 20 '19 at 16:46
  • OK, what does the "Threads" window show? I wonder whether maybe your `TestService` isn't safe to two queries happening at the same time. What happens if you replace your call to `_httpClient.PostAsJsonAsync` with something else which doesn't try and talk to a remote server, such as `await Task.Delay(100); return new ....`. – canton7 Feb 20 '19 at 16:47
  • 1
    Apparently, that's it - replacing the whole method with `await Task.Delay(100); return null;` removed the deadlock. Sadly I don't know how to interprete the threads window. – Thaoden Feb 20 '19 at 16:51
  • Re the threads-window, just double-click on all threads and see what they're doing. Most won't be doing anything, but you'll have at least one that's stuck on *something* in your code, I suspect (or your code is somewhere in the stack trace). Then you can go through the "Call Stack" window and see how it ended up where it is. – canton7 Feb 20 '19 at 16:52
  • `TestServer being an in-memory version of a web service` <= is it thread safe? What is the implementation of this service? – Igor Feb 20 '19 at 16:54
  • 1
    @Igor ASP .Net Core Test Server Class: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver?view=aspnetcore-2.2 I would guess apparently, it is not thread safe. – Thaoden Feb 20 '19 at 16:56
  • Did you try appending `ConfigureAwait(false)` to the 2 awaitable async calls in method `CreateLdapEntry`? – Igor Feb 20 '19 at 16:58
  • Huh, interesting. Seeing where the thread is stuck will be crucial to figuring out what's going on. – canton7 Feb 20 '19 at 16:59
  • @Igor I did, without success. @canton7 When breaking on `Task.WhenAll()`, a thread seems stuck on `await _next.Invoke(context);` of some middleware, or rather on `Microsoft.AspNetCore.Routing.dll!Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke`. – Thaoden Feb 20 '19 at 17:03
  • I'm afraid I don't know enough about asp.net core to know why that might have deadlocked. If I could reproduce it I might be able to have a dig around and see if I could spot something, but sadly that looks like a reasonable amount of effort. Something that's worth trying though: maybe a different `HttpClient` for each request? – canton7 Feb 20 '19 at 17:27
  • 1
    I found the underlying error, I think. The web server opens a ldap connection, apparently I have some concurrency issues there. Thanks again! – Thaoden Feb 20 '19 at 17:31
  • Nice find. Odd about the ldap connection considering it should be a fake web server call (or maybe this connection is somewhere else in the code?). – Igor Feb 20 '19 at 18:33

1 Answers1

1

(Conclusion summarised from the comments, for any future viewers).

You're not blocking anywhere, so the only possible explanation I can think of is that at least one of the requests isn't completing.

(If you were synchronously waiting for a Task to complete then I could well expect a deadlock, since you're not using ConfigureAwait(false), but since you only ever await your Tasks, I pretty sure this isn't the cause).

Given that your requests complete successfully when they're run individually, this implies that there's some concurrency issue when multiple requests are run in parallel, possibly to do with whatever _httpClient is, or to do with the server that requests are being made against (if they're running against a real server).

Given that your Task list doesn't show anything interesting, I'm inclined to think that one of the requests has synchronously blocked the thread that called into it.

Just see whether one of the requests has synchronously blocked. Open the Threads window, and double-click each thread in turn. Most won't be doing anything, but at least one might be running your code, or running a method called from your code. Look at the call stack to try and find out what's going on. You can double-click entries in the call stack to inspect variables in scope at each point.

canton7
  • 37,633
  • 3
  • 64
  • 77