Resiliência de conexão

A resiliência de conexão repete automaticamente comandos de banco de dados com falha. O recurso pode ser usado com qualquer banco de dados fornecendo uma "estratégia de execução", que encapsula a lógica necessária para detectar falhas e repetir comandos. Os provedores do EF Core podem fornecer estratégias de execução adaptadas às condições de falha de banco de dados específicas e às políticas de repetição ideais.

Por exemplo, o provedor do SQL Server inclui uma estratégia de execução que é especificamente adaptada ao SQL Server (incluindo o SQL Azure). Ela está ciente dos tipos de exceção que podem ser repetidos e tem padrões sensatos para tentativas máximas, atraso entre repetições etc.

Uma estratégia de execução é especificada ao configurar as opções para o contexto. Isso normalmente está no método OnConfiguring do contexto derivado:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True",
            options => options.EnableRetryOnFailure());
}

ou em Startup.cs para um aplicativo ASP.NET Core:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

Observação

A habilitação da repetição de falha faz com que o EF faça o buffer interno do conjunto de resultados, o que pode aumentar significativamente os requisitos de memória para consultas que retornam conjuntos de resultados grandes. Consulte buffer e streaming para obter mais detalhes.

Estratégia de execução personalizada

Há um mecanismo para registrar uma estratégia de execução personalizada própria se você quiser alterar qualquer um dos padrões.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

Estratégias e transações de execução

Uma estratégia de execução que tenta novamente automaticamente falhas precisa ser capaz de reproduzir cada operação em um bloco de repetição que falha. Quando as novas tentativas são habilitadas, cada operação executada por meio do EF Core torna-se sua própria operação repetível. Cada consulta e cada chamada a SaveChanges() serão repetidas como uma unidade se ocorrer uma falha transitória.

No entanto, se seu código iniciar uma transação usando BeginTransaction(), você estará definindo seu próprio grupo de operações que precisam ser tratadas como uma unidade e tudo dentro da transação precisará ser revertido se ocorrer uma falha. Você receberá uma exceção como a seguinte se tentar fazer isso ao usar uma estratégia de execução:

System.InvalidOperationException: a estratégia de execução configurada "SqlServerRetryingExecutionStrategy" não é compatível com transações iniciadas pelo usuário. Use a estratégia de execução retornada por 'DbContext.Database.CreateExecutionStrategy()' para executar todas as operações na transação como uma unidade repetível.

A solução é invocar manualmente a estratégia de execução do EF com um delegado que representa tudo que precisa ser executado. Se ocorrer uma falha transitória, a estratégia de execução invocará o representante novamente.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context = new BloggingContext();
        using var transaction = context.Database.BeginTransaction();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        context.SaveChanges();

        transaction.Commit();
    });

Essa abordagem também pode ser usada com transações de ambiente.


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context2.SaveChanges();

        context1.SaveChanges();

        transaction.Complete();
    });

Falha de confirmação de transação e o problema de idempotência

Em geral, quando há uma falha de conexão, a transação atual é revertida. No entanto, se a conexão for descartada enquanto a transação estiver sendo confirmada, o estado resultante da transação será desconhecido.

Por padrão, a estratégia de execução tentará novamente a operação como se a transação tivesse sido revertida, mas se não for o caso, isso resultará em uma exceção se o novo estado do banco de dados for incompatível ou puder levar à corrupção de dados se a operação não depender de um estado específico, por exemplo, ao inserir uma nova linha com valores de chave gerados automaticamente.

Há várias maneiras de lidar com isso.

Opção 1: não fazer (quase) nada

A probabilidade de uma falha de conexão durante a confirmação da transação é baixa, portanto, pode ser aceitável que seu aplicativo simplesmente falhe se essa condição realmente ocorrer.

No entanto, você precisa evitar o uso de chaves geradas pelo repositório para garantir que uma exceção seja lançada em vez de adicionar uma linha duplicada. Considere usar um valor GUID gerado pelo cliente ou um gerador de valor do lado do cliente.

Opção 2: recriar o estado do aplicativo

  1. Descarte o DbContext atual.
  2. Crie um novo DbContext e restaure o estado do seu aplicativo a partir do banco de dados.
  3. Informe ao usuário que a última operação pode não ter sido concluída com êxito.

Opção 3: adicionar verificação de estado

Para a maioria das operações que alteram o estado do banco de dados, é possível adicionar um código que verifica se ele foi bem-sucedido. O EF fornece um método de extensão para facilitar isso: o IExecutionStrategy.ExecuteInTransaction.

Esse método inicia e confirma uma transação e também aceita uma função no parâmetro verifySucceeded que é invocada quando ocorre um erro transitório durante a confirmação da transação.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

db.ChangeTracker.AcceptAllChanges();

Observação

Aqui SaveChanges é invocado com o acceptAllChangesOnSuccess definido como false para evitar alterar o estado da entidade Blog para Unchanged se SaveChanges for bem-sucedido. Isso permite repetir a mesma operação se a confirmação falhar e a transação for revertida.

Opção 4: rastrear a transação manualmente

Se você precisar usar chaves geradas pelo repositório ou precisar de uma maneira genérica de lidar com falhas de confirmação que não dependam da operação executada, cada transação poderá receber uma ID que é verificada quando a confirmação falha.

  1. Adicione uma tabela não rastreada ao banco de dados utilizada para rastrear o status das transações.
  2. Insira uma linha na tabela no início de cada transação.
  3. Se a conexão falhar durante as confirmações, verifique a presença da linha correspondente no banco de dados.
  4. Se as confirmações forem bem-sucedidas, exclua a linha correspondente para evitar o crescimento da tabela.

using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();

Observação

Verifique se o contexto usado para a verificação tem uma estratégia de execução definida, pois a conexão provavelmente falhará novamente durante a verificação se ela falhou durante a confirmação da transação.

Recursos adicionais