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.
EFCore a deux solution distinctes pour l’emplacement de la résilience de connexion:
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.
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 :
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))); } }
Il y a 4 stratégies d’exécution de base avec EntityFramework 6 :
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 :
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(); } } });
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.