Résilience des connexions

La résilience des connexions effectue automatiquement de nouvelles tentatives de commandes de base de données ayant échoué. La fonctionnalité peut être utilisée avec n’importe quelle base de données en fournissant une « stratégie d’exécution », qui encapsule la logique nécessaire pour détecter les défaillances et les nouvelles tentatives de commandes. Les fournisseurs EF Core peuvent fournir des stratégies d’exécution adaptées à leurs conditions de défaillance de base de données spécifiques et aux stratégies de nouvelle tentative optimales.

Par exemple, le fournisseur SQL Server inclut une stratégie d’exécution spécifiquement adaptée à SQL Server (y compris SQL Azure). Il est conscient des types d’exceptions qui peuvent être retentés et a des valeurs par défaut sensibles pour les nouvelles tentatives maximales, le délai entre les nouvelles tentatives, etc.

Une stratégie d’exécution est spécifiée lors de la configuration des options de votre contexte. Cela se trouve généralement dans la méthode OnConfiguring de votre contexte dérivé :

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

ou dans Startup.cs pour une application ASP.NET Core :

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

Remarque

L’activation d’une nouvelles tentative en cas d’échec entraîne EF à mettre en mémoire tampon en interne le jeu de résultats, ce qui peut augmenter considérablement les besoins en mémoire pour les requêtes retournant des jeux de résultats volumineux. Pour plus d’informations, consultez Mise en mémoire tampon et diffusion en continu.

Stratégie d'exécution personnalisée

Il existe un mécanisme permettant d’inscrire votre propre stratégie d’exécution personnalisée si vous souhaitez modifier l’une des valeurs par défaut.

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

Stratégies d’exécution et transactions

Une stratégie d’exécution qui retente automatiquement en cas d’échecs doit pouvoir lire chaque opération dans un bloc de nouvelle tentative qui échoue. Lorsque les nouvelles tentatives sont activées, chaque opération que vous effectuez avec EF Core devient sa propre opération de nouvelle tentative. C’est-à-dire, que chaque requête et chaque appel à SaveChanges() sont retentés ensemble si un échec passager se produit.

Toutefois, si votre code lance une transaction à l’aide de BeginTransaction(), vous définissez votre propre groupe d’opérations à traiter ensemble, et tout le contenu de la transaction doit être lu si une défaillance se produit. Vous recevrez une exception semblable à ce qui suit si vous tentez d’effectuer cette opération lors de l’utilisation d’une stratégie d’exécution :

InvalidOperationException : La stratégie d’exécution configurée « SqlServerRetryingExecutionStrategy » ne prend pas en charge les transactions lancées par l’utilisateur. Utilisez la stratégie d’exécution retournée par « DbContext.Database.CreateExecutionStrategy() » pour exécuter toutes les opérations de la transaction en tant qu’ensemble pouvant être retenté.

La solution consiste à appeler manuellement la stratégie d’exécution avec un délégué représentant tout ce qui doit être exécuté. En cas d’échec passager, la stratégie d’exécution appelle de nouveau le délégué.


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();
    });

Cette approche peut également être utilisée avec des transactions ambiantes.


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();
    });

Échec de validation des transactions et problème d’idempotence

En règle générale, en cas d’échec de connexion, la transaction actuelle est restaurée. Toutefois, si la connexion est annulée alors que la transaction est en train d’être validée, l’état résultant de la transaction est inconnu.

Par défaut, la stratégie d’exécution réessaye l’opération comme si la transaction a été restaurée, mais si ce n’est pas le cas, cela entraînera une exception si le nouvel état de la base de données est incompatible ou peut entraîner une corruption des données si l’opération ne repose pas sur un état particulier, par exemple lors de l’insertion d’une nouvelle ligne avec des valeurs de clé générées automatiquement.

Pour cela, différentes possibilités s'offrent à vous.

Option 1 : ne (presque) rien faire

La probabilité d’un échec de connexion lors de la validation des transactions est faible. Il peut donc être acceptable que votre application échoue simplement si le cas se produit réellement.

Toutefois, vous devez éviter d’utiliser des clés générées par le magasin pour vous assurer qu’une exception est levée au lieu d’ajouter une ligne dupliquée. Envisagez l’utilisation d’une valeur GUID générée par le client ou un générateur de valeurs côté client.

Option 2 : régénérer l’état de l’application

  1. Abandonner le DbContext actuel.
  2. Créez un nouveau DbContext et restaurez l’état de votre application à partir de la base de données.
  3. Informez l’utilisateur que la dernière opération n’a peut-être pas été effectuée avec succès.

Option 3 : ajouter la vérification de l’état

Pour la plupart des opérations qui modifient l’état de la base de données, il est possible d’ajouter du code qui vérifie si elles ont réussi. EF fournit une méthode d’extension pour faciliter cette opération : IExecutionStrategy.ExecuteInTransaction.

Cette méthode commence et valide une transaction, et accepte également une fonction dans le paramètre verifySucceeded qui est appelé lorsqu’une erreur temporaire se produit pendant la validation de la transaction.


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();

Remarque

Ici, SaveChanges est appelée avec acceptAllChangesOnSuccess défini sur false pour éviter de modifier l’état de l’entité de Blog en Unchanged si SaveChanges réussit. Cela permet de réessayer la même opération si la validation échoue et que la transaction est restaurée.

Option 4 : suivre la transaction manuellement

Si vous devez utiliser des clés générées par le magasin ou si vous avez besoin d’un moyen générique pour gérer les échecs de validation qui ne dépendent pas de l’opération effectuée, chaque transaction peut être attribuée un ID vérifié lors de l’échec de la validation.

  1. Ajoutez une table à la base de données utilisée pour suivre l’état des transactions.
  2. Insérez une ligne dans cette table au début de chaque transaction.
  3. Si la connexion échoue pendant la validation, vérifiez que la ligne correspondante apparaît dans la base de données.
  4. Si la validation est réussie, supprimez la ligne correspondante pour éviter l’extension de la table.

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();

Remarque

Assurez-vous que le contexte utilisé pour la vérification a une stratégie d’exécution définie, car la connexion est susceptible d’échouer à nouveau lors de la vérification si elle a échoué lors de la validation de transaction.

Ressources supplémentaires