Search code examples
c#entity-framework.net-coreazure-functionstransactionscope

Azure Functions w/ EF Core DbContext - how does the DI scope work?


SOLVED: This error came from a table being locked through the open transaction and the other function call tried to wait for the unlock until it timed out.

I'm currently working on an Azure Function app in .NET Core and trying to add transactions for requests. The problem is, when a function call is doing work between a context.Database.BeginTransaction(...) call and a transaction.CommitAsync(...) call, then another function call can not save to the database and gets a timeout:

[2024-01-04T07:23:29.957Z] An exception occurred in the database while saving changes for context type 'MyContext'.
[2024-01-04T07:23:29.957Z] System.InvalidOperationException: An exception has been raised that is likely due to a transient failure.
[2024-01-04T07:23:29.957Z]  ---> Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
[2024-01-04T07:23:29.958Z]  ---> Npgsql.NpgsqlException (0x80004005): Exception while reading from stream
[2024-01-04T07:23:29.958Z]  ---> System.TimeoutException: Timeout during reading attempt

This other function call can save perfectly fine, when the "long running" function call is not being called beforehand. So I suppose the open transaction is blocking the DbContext in other function calls.

Our DbContext gets registered like this:

builder.Services.AddDbContext<MyContext>(
    options => options
        .UseNpgsql(dbConnStr)
        .UseSnakeCaseNamingConvention());

From what I understand each function call should get its own dependency injection scope, just like in ASP .NET Core, where the "same" setup works without any problems.

Am I missing something?

EDIT: I've found someone with the same problem buried in an old Github issue: https://github.com/Azure/azure-functions-host/issues/5098#issuecomment-896712016 but there doesn't seem to be a followup with a solution.

EDIT 2:

Basic outline of the 2 functions (_dbContext gets injected with ctor):

public class LongerRunningFunctionClass {
    [Function("LongerRunningFunction")]
    public HttpResponseData LongerRunningFunction([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, FunctionContext executionContext) {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();
    
        try {
            // Imagine some code here inserting data into the database
            // and doing calls to external services
    
            await transaction.CommitAsync();
        }
        catch (Exception) {
            await transaction.RollbackAsync();
            throw;
        }
    } 
}


public class OtherFunctionClass {
    [Function("OtherFunction")]
    public HttpResponseData OtherFunction([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, FunctionContext executionContext) {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();
    
        try {
            // Imagine a quick insert here
    
            await transaction.CommitAsync();
        }
        catch (Exception) { // <-- I fall into this catch when the other function is still executing with above error
            await transaction.RollbackAsync();
            throw;
        }
    }
}


Solution

  • This error came from a table being locked through the open transaction and the other function call tried to wait for the unlock until it timed out.

    Big thanks to David Browne below helping me debug this.

    We'll need to rethink either how consistent our data needs to be and if we need the transaction and/or if we can offload some stuff to a queue.