Resiliência da 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 personalizadas para suas condições de falha de banco de dados específicas e políticas de repetição ideais.

Por exemplo, o provedor de SQL Server inclui uma estratégia de execução especificamente personalizada para SQL Server (incluindo SQL Azure). Ele 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 seu contexto. Normalmente, isso está no OnConfiguring método do contexto derivado:

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

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

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

Observação

Habilitar novamente a tentativa de falha faz com que o EF faça buffer interno do conjunto de resultados, o que pode aumentar significativamente os requisitos de memória para consultas que retornam grandes conjuntos de resultados. Consulte o buffer e o 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 repete automaticamente as 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 se torna sua própria operação retriável. Ou seja, cada consulta e cada chamada a SaveChanges() ser tentada novamente como uma unidade se ocorrer uma falha transitória.

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

InvalidOperationException: a estratégia de execução configurada 'SqlServerRetryingExecutionStrategy' não dá suporte a 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 com um representante que representa tudo o 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 idempotency

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 fosse 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 poderá 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 de transação é baixa, portanto, pode ser aceitável que seu aplicativo falhe apenas 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 – Recompilar o estado do aplicativo

  1. Descarte o atual DbContext.
  2. Crie um novo DbContext e restaure o estado do 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 código que verifica se ele foi bem-sucedido. O EF fornece um método de extensão para facilitar isso – IExecutionStrategy.ExecuteInTransaction.

Esse método começa e confirma uma transação e também aceita uma função no verifySucceeded parâmetro que é invocado 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 acceptAllChangesOnSuccess conjunto para false evitar alterar o estado da Blog entidade para Unchanged se SaveChanges tiver êxito. Isso permite repetir a mesma operação se a confirmação falhar e a transação for revertida.

Opção 4 – controlar manualmente a transação

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 dependem da operação executada, cada transação poderá receber uma ID que é verificada quando a confirmação falha.

  1. Adicione uma tabela ao banco de dados usado para controlar 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 a confirmação, verifique a presença da linha correspondente no banco de dados.
  4. Se a confirmação for bem-sucedida, 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 é provável que a conexão falhe novamente durante a verificação se ela falhou durante a confirmação da transação.

Recursos adicionais