Resiliência de conexão e lógica de repetição

Observação

EF6 em diante apenas: os recursos, as APIs etc. discutidos nessa página foram introduzidos no Entity Framework 6. Se você estiver usando uma versão anterior, algumas ou todas as informações não se aplicarão.

Os aplicativos que se conectam a um servidor de banco de dados sempre foram vulneráveis a quebras de conexão devido a falhas de back-end e instabilidade de rede. No entanto, em um ambiente baseado em LAN que trabalha com servidores de banco de dados dedicados, esses erros são raros e a lógica extra para lidar com essas falhas não são frequentemente necessárias. Com o aumento de servidores de banco de dados baseados em nuvem, como o Banco de Dados SQL do Windows Azure e conexões em redes menos confiáveis, agora é mais comum que ocorram quebras de conexão. Isso pode ser devido a técnicas defensivas que os bancos de dados de nuvem usam para garantir a imparcialidade do serviço, como limitação de conexão ou instabilidade na rede, causando tempos limite intermitentes e outros erros transitórios.

Resiliência de conexão refere-se à capacidade do EF de repetir automaticamente todos os comandos que falham devido a essas quebras de conexão.

Estratégias de execução

A repetição de conexão é cuidada por uma implementação da interface IDbExecutionStrategy. As implementações do IDbExecutionStrategy serão responsáveis por aceitar uma operação e, se ocorrer uma exceção, determinar se uma repetição é apropriada e repetir a tentativa se ela for. Há quatro estratégias de execução que são enviadas com EF:

  1. DefaultExecutionStrategy: essa estratégia de execução não tenta repetir nenhuma operação novamente, é o padrão para bancos de dados diferentes do SQL Server.
  2. DefaultSqlExecutionStrategy: essa é uma estratégia de execução interna usada por padrão. No entanto, essa estratégia não terá repetição de tentativa, encapsulará quaisquer exceções que possam ser transitórias para informar aos usuários que eles podem querer habilitar a resiliência da conexão.
  3. DbExecutionStrategy: essa classe é adequada como uma classe base para outras estratégias de execução, incluindo as personalizadas. Ela implementa uma política de repetição exponencial, em que a repetição inicial ocorre com atraso zero e o atraso aumenta exponencialmente até que a contagem máxima de repetição seja atingida. Essa classe tem um método ShouldRetryOn abstrato que pode ser implementado em estratégias de execução derivadas para controlar quais exceções devem ser repetidas.
  4. SqlAzureExecutionStrategy: essa estratégia de execução herda de DbExecutionStrategy e repetirá a tentativa em exceções que são conhecidas por serem possivelmente transitórias ao trabalhar com o Banco de Dados SQL do Azure.

Observação

As estratégias de execução 2 e 4 são incluídas no provedor do SQL Server que é fornecido com o EF, que está no assembly EntityFramework.SqlServer e foi projetado para funcionar com o SQL Server.

Habilitando uma estratégia de execução

A maneira mais fácil de informar o EF para usar uma estratégia de execução é com o método SetExecutionStrategy da classe DbConfiguration :

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy());
    }
}

Esse código informa ao EF para usar o SqlAzureExecutionStrategy ao se conectar ao SQL Server.

Configurando a estratégia de execução

O construtor de SqlAzureExecutionStrategy pode aceitar dois parâmetros, MaxRetryCount e MaxDelay. A contagem do MaxRetry é o número máximo de vezes que a estratégia repetirá a tentativa. O MaxDelay é um TimeSpan que representa o atraso máximo entre as tentativas que a estratégia de execução usará.

Para definir o número máximo de repetições como 1 e o atraso máximo para 30 segundos, você executaria o seguinte:

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetExecutionStrategy(
            "System.Data.SqlClient",
            () => new SqlAzureExecutionStrategy(1, TimeSpan.FromSeconds(30)));
    }
}

O SqlAzureExecutionStrategy repetirá a tentativa na primeira vez que ocorrer uma falha transitória, mas atrasará mais tempo entre cada repetição até que o limite máximo de repetição seja excedido ou o tempo total atinja o atraso máximo.

As estratégias de execução tentarão apenas um número limitado de exceções que geralmente são transitórias. Você ainda precisará lidar com outros erros, bem como capturar a exceção RetryLimitExceededed para o caso em que um erro não é transitório ou leva muito tempo para se resolver.

Há algumas limitações conhecidas ao usar uma estratégia de execução de repetição:

Não há suporte para consultas de streaming

Por padrão, o EF6 e a versão posterior armazenarão em buffer os resultados da consulta em vez de transmiti-los. Se você quiser que os resultados sejam transmitidos, poderá usar o método AsStreaming para alterar uma consulta LINQ to Entities para streaming.

using (var db = new BloggingContext())
{
    var query = (from b in db.Blogs
                orderby b.Url
                select b).AsStreaming();
    }
}

Não há suporte para streaming quando uma estratégia de execução de repetição é registrada. Essa limitação existe porque a conexão pode remover parte dos resultados que estão sendo retornados. Quando isso ocorre, o EF precisa executar novamente toda a consulta, mas não tem uma maneira confiável de saber quais resultados já foram retornados (os dados podem ter sido alterados desde que a consulta inicial foi enviada, os resultados podem voltar em uma ordem diferente, os resultados podem não ter um identificador exclusivo, etc.).

Não há suporte para transações iniciadas pelo usuário

Quando você configura uma estratégia de execução que resulta em novas tentativas, há algumas limitações em relação ao uso de transações.

Por padrão, o EF executará todas as atualizações de banco de dados em uma transação. Você não precisa fazer nada para habilitar isso, o EF sempre faz isso automaticamente.

Por exemplo, no código a seguir, SaveChanges é executado automaticamente em uma transação. Se SaveChanges falhasse depois de inserir um dos novos Sites, a transação seria revertida e nenhuma alteração aplicada ao banco de dados. O contexto também é deixado em um estado que permite que SaveChanges seja chamado para repetir a tentativa aplicar as alterações.

using (var db = new BloggingContext())
{
    db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" });
    db.Blogs.Add(new Site { Url = "http://blogs.msdn.com/adonet" });
    db.SaveChanges();
}

Ao não usar uma estratégia de execução de repetição, você pode encapsular várias operações em uma única transação. Por exemplo, o código a seguir encapsula duas chamadas SaveChanges em uma única transação. Se qualquer parte de uma das operações falhar, nenhuma das alterações será aplicada.

using (var db = new BloggingContext())
{
    using (var trn = db.Database.BeginTransaction())
    {
        db.Blogs.Add(new Site { Url = "http://msdn.com/data/ef" });
        db.Blogs.Add(new Site { Url = "http://blogs.msdn.com/adonet" });
        db.SaveChanges();

        db.Blogs.Add(new Site { Url = "http://twitter.com/efmagicunicorns" });
        db.SaveChanges();

        trn.Commit();
    }
}

Não há suporte para isso ao usar uma estratégia de execução de repetição porque o EF não está ciente de nenhuma operação anterior e de como repeti-las. Por exemplo, se o segundo SaveChanges falhou, o EF não tem mais as informações necessárias para repetir a tentativa da primeira chamada SaveChanges.

Solução: chamar manualmente a estratégia de execução

A solução é usar manualmente a estratégia de execução e dar a ela todo o conjunto de lógica a ser executado, para que ela possa repetir tudo se uma das operações falhar. Quando uma estratégia de execução derivada de DbExecutionStrategy estiver em execução, ela suspenderá a estratégia de execução implícita usada em SaveChanges.

Observe que todos os contextos devem ser construídos dentro do bloco de código a ser repetido. Isso garante que estamos começando com um estado limpo para cada repetição.

var executionStrategy = new SqlAzureExecutionStrategy();

executionStrategy.Execute(
    () =>
    {
        using (var db = new BloggingContext())
        {
            using (var trn = db.Database.BeginTransaction())
            {
                db.Blogs.Add(new Blog { Url = "http://msdn.com/data/ef" });
                db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/adonet" });
                db.SaveChanges();

                db.Blogs.Add(new Blog { Url = "http://twitter.com/efmagicunicorns" });
                db.SaveChanges();

                trn.Commit();
            }
        }
    });