1

In my project I'm using ASP.NET Core 3.1 with a hosted service background worker.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var result = await _messageBus.Get();
        if (result != null)
        {
            await _dbContext.UpdateData(result, stoppingToken);
        }

        await Task.Delay(5000, stoppingToken);
    }
}

Inside my DbContext I do some logic then call await command.ExecuteNonQueryAsync(stoppingToken);. On that line the worker deadlocks.

await using var connection = new SqlConnection(dbConnection);
await connection.OpenAsync(stoppingToken);

var command = connection.CreateCommand();
command.CommandText = query;
await command.ExecuteNonQueryAsync(stoppingToken);

I then changed the background worker to:

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
    _timer = new Timer(async state => await Run(state, stoppingToken), null, 
                                                TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(5));

    return Task.CompletedTask;
}

private async Task Run(object state, CancellationToken stoppingToken)
{
    var result = await _messageBus.Get();
    if (result != null) await _dbContext.UpdateData(result, stoppingToken);
}

This ended up working and avoided a deadlock. However I honestly don't know why this worked and avoided a deadlock. What makes the the Timer class different than just using Task.Delay?

Travis Boatman
  • 2,252
  • 16
  • 31
  • 2
    How are you calling `ExecuteAsync` – TheGeneral Mar 03 '20 at 21:18
  • 1
    do you ever call `ExecuteAsync` ? or is some framework calling it? – TheGeneral Mar 03 '20 at 21:22
  • None of the code you have posted will block, so won't cause deadlocks. You still haven't shown where `ExecuteAsync` is called. – Johnathan Barclay Mar 03 '20 at 21:23
  • 2
    How are you sure it is deadlocking? That's not easy to do in .NET Core which has no synchronization context. – Crowcoder Mar 03 '20 at 21:24
  • My mistake, `ExecuteAsync` is inherted from `BackgroundService` which is invoked by ASP.NET Core when adding a hosted service to the service collection. To see where `ExecuteAsync` is called see here: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/background-tasks-with-ihostedservice#implementing-ihostedservice-with-a-custom-hosted-service-class-deriving-from-the-backgroundservice-base-class – Travis Boatman Mar 03 '20 at 21:28
  • You shuld to add .ConfigureAwait(false) after your method calls, example: command.ExecuteNonQueryAsync(stoppingToken).ConfigureAwait(false) – svladimirrc Mar 03 '20 at 22:22

1 Answers1

3

I strongly suspect that your code is not actually asynchronous, which will cause startup issues (that can look like deadlocks). It's not documented well, but ExecuteAsync must be asynchronous. So if you have blocking code at the beginning of that method (e.g., if the "get message from the message bus" code is actually synchronously blocking until a message is received), then you'll need to wrap it in a Task.Run:

protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.Run(async () =>
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var result = await _messageBus.Get();
        if (result != null)
        {
            await _dbContext.UpdateData(result, stoppingToken);
        }

        await Task.Delay(5000, stoppingToken);
    }
});

In my own code, I use a separate base type to do the Task.Run so it's less ugly.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • You can add `await Task.Yield()` to the top to ensure it doesn't block. – Jeremy Lakeman Mar 05 '20 at 01:57
  • I figured out that the user being used did not have permissions to write. When `await command.ExecuteNonQueryAsync(stoppingToken);` was called an exception was thrown. I'm left confused though, why does the method not return it's task as failed with the exception? Instead it just stops and hangs. – Travis Boatman Mar 28 '20 at 05:24
  • @TravisBoatman: That behavior depends on your db provider. I would certainly expect `ExecuteNonQueryAsync` to throw if there were insufficient permissions. Are you sure it's not throwing? – Stephen Cleary Mar 28 '20 at 10:42
  • @StephenCleary I found out why it behaves this way. The background worker `ExecuteAync` method behaves differently than one might think when it comes to exceptions. It's not very well documented and is still under discussion. See here for more information. https://github.com/dotnet/extensions/issues/2017 - https://github.com/dotnet/extensions/issues/2363 - https://stackoverflow.com/questions/56871146/exception-thrown-from-task-is-swallowed-if-thrown-after-await – Travis Boatman Mar 29 '20 at 00:02
  • Oh, right. I thought you meant it actually deadlocked at `ExecuteNonQueryAsync`. If an exception is thrown from `ExecuteAsync`, that exception does not take down the entire process, by design. If you want to exit the process when the `ExecuteAsync` method throws an exception (or exits without an exception), then you'll need to code that yourself using `IHostApplicationLifetime`. – Stephen Cleary Mar 29 '20 at 01:36