Partager via


Résilience des connexions

La résilience de connexion retente automatiquement les 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 échecs et les commandes de nouvelle tentative. Les fournisseurs EF Core peuvent fournir des stratégies d’exécution adaptées à leurs conditions d’échec 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;ConnectRetryCount=0",
            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 nouvelle tentative en cas d’échec entraîne la mise en mémoire tampon interne du 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 une stratégie d’exécution personnalisée de votre propre 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 via EF Core devient sa propre opération retriable. C’est-à-dire, que chaque requête et chaque appel à SaveChangesAsync() sont retentés ensemble si un échec passager se produit.

Toutefois, si votre code lance une transaction à l’aide de BeginTransactionAsync() vous définissez votre propre groupe d’opérations qui doivent être traitées en tant qu’unité, et tout ce qui se trouve dans la transaction doit être lu en cas de défaillance. 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 initié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’unité retriable.

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é. Si une défaillance temporaire se produit, la stratégie d’exécution appelle à nouveau le délégué.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(
    async () =>
    {
        using var context = new BloggingContext();
        await using var transaction = await context.Database.BeginTransactionAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    });

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

await strategy.ExecuteAsync(
    async () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context2.SaveChangesAsync();

        await context1.SaveChangesAsync();

        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 annulée. Toutefois, si la connexion est supprimée pendant que la transaction est validée, l’état résultant de la transaction est inconnu.

Par défaut, la stratégie d’exécution retentera 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 altération des données si l’opération ne s’appuie 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.

Il existe plusieurs façons de traiter ce problème.

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 cette condition 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 d’utiliser une valeur GUID générée par le client ou un générateur de valeurs côté client.

Option 2 - Reconstruire l’état de l’application

  1. Abandonner le DbContext actuel.
  2. Créez une 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 correctement.

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 s’il a 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);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));

db.ChangeTracker.AcceptAllChanges();

Remarque

Ici, SaveChanges est appelée avec acceptAllChangesOnSuccess défini sur false pour éviter de modifier l’état de l’entité 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 manuellement la transaction

Si vous devez utiliser des clés générées par le magasin ou avoir besoin d’un moyen générique de gérer les échecs de validation qui ne dépendent pas de l’opération effectuée chaque transaction peut être affecté à 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 la table au début de chaque transaction.
  3. Si la connexion échoue pendant la validation, vérifiez la présence de la ligne correspondante dans la base de données.
  4. Si la validation réussit, supprimez la ligne correspondante pour éviter la croissance 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);

await strategy.ExecuteInTransactionAsync(
    db,
    operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
    verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();

Remarque

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

Ressources additionnelles