Résilience de connexion BDD SQL Azure EFCore et EF6

Antoine NAFFETAT
Publié par Antoine NAFFETAT
Catégorie : Azure / SQL Server
07/01/2021

Lors de l’utilisation d’une base de données SQL Azure serverless via une fonction Azure, il se peut que l’erreur suivante arrive « Internal .Net Framework Data Provider error 6 in SQL Azure ». Et si vous relancez la requête un peu après, cela fonctionnement normalement.

Cette erreur survient quand la BDD SQL est en pause et donc la connexion SQL échoue avant que la base de données soit active.

Afin de solutionner ce problème, l’idée est de mettre en place une stratégie d’exécution pour les conditions d’échec et une politique de redémarrage. Ces stratégies permettent, suivant une loi, de relancer la requête après un temps donné.

Si vous utilisez Entity Framework 6 ou EF Core une solution native existe. Dans le cas contraire, il est possible d’utiliser le « bloc d’application de gestion des erreurs temporaires » créé par Microsoft si vous utilisez ADO.net. Dans cet article nous verrons la mise en place pour Entity Framework 6 et EF Core.

NOTE : cet article ne traite que de la résilience des connexions pour les bases de données SQL Azure, mais il convient bien sûr d’assurer la résilience des connexions également pour tout autre service de données tel que Azure Service Bus, Azure Storage Service ou Azure Caching Service.

 

Solution Native EFCore

 

Présentation

EFCore a deux solution distinctes pour l’emplacement de la résilience de connexion:

  • La première est l’implémentation dans le start-up;
  • La deuxième est dans la méthode OnConfiguring().

Il est soit possible d’utiliser une politique native en choisissant uniquement les paramètres de base ou bien de définir une politique complètement custom.

 

Mise en place

Dans un premier temps, voici le code afin de mettre en place dans le start-up une politique de redémarrage :

 

services.AddDbContext<MyDbContext>(options =>
  {
      options.UseSqlServer(Configuration["ConnectionString"],
      sqlServerOptionsAction: sqlOptions =>
      {
          sqlOptions.EnableRetryOnFailure(
          maxRetryCount: 5,
          maxRetryDelay: TimeSpan.FromSeconds(60),
          errorNumbersToAdd: null);
      });
  });

 

Ou bien dans la méthode OnConfiguring() :

 

builder.UseSqlServer(connectionString, sqlServerOptionsAction: sqlOptions =>
{
    sqlOptions.EnableRetryOnFailure(
    maxRetryCount: 5,
    maxRetryDelay: TimeSpan.FromSeconds(60),
    errorNumbersToAdd: null);
});

 

NOTE :

  • Cette configuration de reprise sur erreur est une stratégie par défaut pour les connexions SQL et il est possible de configurer comme on le souhaite les paramètres de base (nombre de nouveaux essais, délai entre deux essais…);
  • Microsoft recommande un délai total de retry (nombre de tentatives * délai de retry) d’au moins 1 minute.

 

Comme dit précédemment, nous pouvons aussi créer une stratégie d’exécution custom :

 

public class CustomExecutionStrategy : ExecutionStrategy
  {
      public CustomExecutionStrategy(DbContext context) : base(context, 10, TimeSpan.FromSeconds(60))
      {
      }

      public CustomExecutionStrategy(ExecutionStrategyDependencies dependencies) : base(context, 10, TimeSpan.FromSeconds(60))
      {
      }

      public CustomExecutionStrategy(DbContext context, int maxRetryCount, TimeSpan maxRetryDelay) : base(context, 10, TimeSpan.FromSeconds(60))
      {
      }

      protected override bool ShouldRetryOn(Exception exception)
      {
          return exception.GetType() == typeof(InvalidOperationException);
      }
  }

 

NOTE : Dans notre cas, nous cherchons à compenser l’erreur « Internal .Net Framework Data Provider error 6 in SQL Azure ». Cette erreur qui se matérialise par une exception de type InvalidOperationException, que l’on retrouve dans la surcharge de la méthode ShouldRetryOn ci-dessus.

 

Le code suivant montre comment appliquer la stratégie d’exécution custom au dbset dans la fonction OnConfiguring().

 

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
   {
       if (!optionsBuilder.IsConfigured)
       {             optionsBuilder.UseSqlServer("Server=localhost\\SQLEXPRESS;Database=master;Trusted_Connection=True;",
               builder => builder.ExecutionStrategy(c => new CustomExecutionStrategy(c)));
       }
   }

 

Solution Native EF6

 

Présentation

Il y a 4 stratégies d’exécution de base avec EntityFramework 6 :

  • DefaultExecutionStrategy : c’est la stratégie par déaut, aucun nouvel essaye est réalisé.
  • DefaultSQLexecutionstrategy : aucun nouvel essaye est réalisé mais regroupe toutes les exceptions considérées comme transitoires afin de prévenir l’utilisateur la possibilité de mettre en place une stratégie d’exécution pour ces erreurs.
  • DbExecutionStrategy : cette stratégie à un type d’intervalle exponentielle et possède une méthode ShouldRetryOn abstraite qui permet d’implémenté sa stratégie de décision.
  • SqlAzureExecutionStrategy : Hérite de DbExecutionStrategy et relance les erreurs connues comme étant des erreurs transitoires.

 

Mise en place

Chaque stratégie se met en place de la même façon, en l’initialisant avec la méthode SetExecutionStrategy dans la classe DbConfiguration :

 

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

 

Dans l’exemple, la stratégie d’exécution définit deux paramètres :

  • Le premier est le nombre maximale de nouvelles tentative.
  • Le deuxième est le temps maximale entre la première erreur et la dernière tentative.

Mais cette stratégie a une limitation : elle ne fonctionne pas si l’utilisateur encapsule des appels dans une transaction unique. Par exemple le code suivant encapsule deux SaveChanges() dans une transaction.

 

using (DemoEntities objContext = GetDemoEntities())
{
    using (TransactionScope objTransaction = new TransactionScope())
    {
        Demo1(objContext);
        Demo2(objContext);
        objTransaction.Complete();
    }
}

public void Demo1(DemoEntities objContext)
{
    Demo1 objDemo1 = new Demo1();
    objDemo1.Title = "NEW TITLE 1";
    objContext.Demo1.Add(objDemo1);
    objContext.SaveChanges();   
}

public void Demo2(DemoEntities objContext)
{
    Demo2 objDemo2 = new Demo2();
    objDemo2.Title = "NEW TITLE 2";
    objContext.Demo2.Add(objDemo2);
    objContext.SaveChanges();   
}

 

Si l’un des deux appels tombe en erreur, aucune modification ne sera faite et EF n’appliquera pas la stratégie d’exécution. Pour résoudre ce problème, il faut instancier manuellement une stratégie d’exécution et l’exécuter sur l’ensemble de la transaction :

 

var executionStrategy = new SqlAzureExecutionStrategy();
executionStrategy.Execute(() =>
{
    using (DemoEntities objContext = GetDemoEntities())
    {
        using (TransactionScope objTransaction = new TransactionScope())
        {
            Demo1(objContext);
            Demo2(objContext);

            objTransaction.Complete();
        }
    }
});

Conclusion et mise en garde

 

Une stratégie de reprise sur erreur est efficace pour fiabiliser la connexion d’une application à sa base de données SQL Azure en gommant les erreurs temporaires de connectivité.

Néanmoins, mettre un nombre de tentatives ou un intervalle trop grand peut entraîner de nombreux problèmes. Par exemple, si l’erreur est due aux données elles-mêmes (erreur de typage, non respect de contrainte, etc), toute nouvelle tentative avec les mêmes données échouera systématiquement. La détection et donc le diagnostic seront « simplement » retardés. Dans certains cas plus extrêmes, les erreurs pourront s’accumuler et, par effet boule de neige, pourront bloquer le système.

Dans certains cas, le changement du type d’intervalle pourra permettre d’éviter une potentielle contention : ainsi une stratégie exponentielle laissera de plus en plus de temps entre chaque essai.

Enfin il est aussi possible de mettre en place des « disjoncteurs ». C’est une amélioration de la stratégie qui permet à l’application, à partir d’un certain nombre d’événements, d’entreprendre une autre action plutôt que de répéter une nouvelle fois la même chose.