Création d’API résilientes avec le modèle de nouvelle tentative

L’envoi et la réception de données sont l’essence même de toute API Web. Mais que se passe-t-il lorsque quelque chose ne va pas et que la communication échoue ? Pour éviter les problèmes inattendus et permettre une plus grande résilience, nous pouvons utiliser le modèle de nouvelle tentative. Découvrez comment implémenter ce modèle dans ASP.NET Core pour rendre vos API plus fiables.
Le modèle de nouvelle tentative aide les développeurs à créer des API préparées pour faire face à des situations défavorables lors de la communication avec des services externes, tels que des bases de données et même d’autres API.
Dans cet article, nous explorerons les principaux défis rencontrés lorsque nous travaillons avec des applications distribuées qui communiquent entre elles et découvrirons comment le modèle de nouvelle tentative peut être utilisé comme stratégie de résilience pour faire face aux échecs courants dans ces scénarios.
Nous verrons également comment implémenter le modèle de nouvelle tentative dans une application ASP.NET Core à l’aide d’un pipeline de nouvelle tentative via la bibliothèque Polly.
Problèmes d’intégration courants
Toute application qui communique avec des API ou des services externes est exposée à des risques d’échecs de communication, notamment des délais d’attente, une indisponibilité temporaire, des erreurs réseau ou des limites de requêtes. Bien que souvent sous-estimés, ces problèmes sont courants et peuvent compromettre l’expérience utilisateur et la fiabilité du système.
Dans ce contexte, nous pouvons souligner les problèmes suivants auxquels les systèmes modernes sont confrontés lors de la gestion des intégrations :
Erreurs réseau temporaires
Lors de l’accès au réseau, une disponibilité à 100 % n’est pas toujours garantie. Des paquets peuvent être perdus, des connexions peuvent être interrompues et les fournisseurs peuvent connaître une instabilité momentanée. Par exemple, une API de paiement met plus de temps que prévu à répondre et son appel expire (TimeoutException). Dans la pratique, la transaction peut avoir été traitée, mais la demande n’a pas reçu de confirmation.
Indisponibilité Temporaire des Services Externes
Les services peuvent subir une maintenance, être surchargés ou être hors ligne pendant quelques minutes. Par exemple, un site Web de commerce électronique interroge une API de tarification pour mettre à jour le prix d’un produit. Si l’API n’est pas disponible, le système peut échouer et afficher le prix obsolète au client, causant ainsi du préjudice et du stress à la fois au consommateur et à l’entreprise qui vend le produit.
Données incohérentes ou invalides
Il existe toujours un risque que les API externes renvoient des données incomplètes, dans un format inattendu ou contenant des erreurs commerciales. Par exemple, une API d’enregistrement renvoie des adresses sans code postal ou avec des caractères invalides, interrompant ainsi les validations internes du système.
Ce ne sont là que quelques-uns des nombreux scénarios dans lesquels une API peut ne pas accomplir la tâche prévue, entraînant des problèmes et des pertes importants pour l’équipe de développement et l’entreprise. Heureusement, certaines solutions permettent d’atténuer ces risques et d’autres en réessayant automatiquement une opération ayant échoué dans l’espoir qu’elle réussira lors d’une deuxième tentative. Nous explorerons ensuite le modèle de nouvelle tentative, qui s’impose comme une solution pour gérer ces échecs temporaires.
Comprendre le modèle de nouvelle tentative
Le Retry Pattern est un modèle de résilience dont l’idée centrale est de réessayer une opération qui a échoué, plutôt que de simplement abandonner dès la première tentative. Ceci est utile et, dans de nombreux cas, indispensable, car les systèmes distribués ont tendance à rencontrer des problèmes temporaires, tels qu’une instabilité momentanée du réseau ou même une surcharge du serveur. Par conséquent, lorsqu’un échec de communication ou de traitement survient entre deux services, il est conseillé au moins de réessayer.
Ces échecs sont généralement corrigés après un certain temps. Si l’action qui a déclenché l’échec est retentée après un délai raisonnable, elle a de très fortes chances de réussir. Par exemple, imaginez qu’un service de paiement soit momentanément surchargé et renvoie une erreur « Service non disponible (503) ». Dans ce cas, cela ne signifie pas que le service est définitivement indisponible, mais plutôt qu’il n’a pas pu traiter la demande à ce moment-là. Si l’application réessaye après quelques secondes, il est très probable que le paiement soit effectué avec succès.
Des scénarios comme celui-ci montrent que le modèle de nouvelle tentative peut être extrêmement utile, car il permet d’éviter que des problèmes temporaires n’entraînent des pertes importantes, de maintenir une expérience utilisateur stable et de réduire le besoin d’intervention manuelle potentielle.
L’image ci-dessous montre deux scénarios, avec et sans l’utilisation du modèle de nouvelle tentative.


Implémentation du modèle de nouvelle tentative dans ASP.NET Core
Pour implémenter le modèle de nouvelle tentative, nous considérerons un scénario dans lequel une API de catalogue de produits doit accéder à une autre API pour récupérer les données d’image de produit. Le principe est que nous ne pouvons pas laisser le client sans l’image du produit. Ainsi, même si la première requête échoue, nous devons réessayer.
Donc, dans un premier temps, nous allons créer une API simple de catalogue de produits et implémenter une politique de nouvelle tentative et une API secondaire pour renvoyer des images de produits. Enfin, nous forcerons une erreur aux deux premières tentatives et un succès à la troisième, pour valider que la politique fonctionne.
Vous pouvez accéder au code source complet dans ce référentiel GitHub : Code source de PollyProducts.
🦜 Polly pour la résilience
Polly est une bibliothèque axée sur la résilience et la tolérance aux pannes pour les applications .NET.
Il est largement connu et aide les systèmes à gérer les pannes temporaires lors de l’appel d’API externes, de bases de données, de services réseau, etc., permettant aux développeurs de mettre en œuvre des politiques de résilience pour anticiper les scénarios d’indisponibilité temporaire.
En plus des fonctionnalités de base telles que la nouvelle tentative automatique d’opération, Polly propose des fonctionnalités avancées telles que le disjoncteur, qui arrête temporairement les nouveaux appels après un certain nombre d’échecs consécutifs, évitant ainsi de surcharger les services indisponibles. Une autre fonctionnalité importante est le repli, qui définit une action alternative lorsque l’opération principale échoue, par exemple, renvoyer les données mises en cache si l’API est en panne.
Création de l’API du catalogue de produits
Alors, commençons par créer l’API qui fera des requêtes à l’API secondaire pour renvoyer les données du catalogue de produits. C’est ici que nous allons créer un pipeline avec une politique de nouvelle tentative.
Pour créer l’application de base, vous pouvez exécuter les commandes suivantes dans votre terminal :
dotnet new web -o PollyProducts
Ensuite, exécutez les commandes suivantes pour télécharger et installer les packages Polly NuGet :
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly
Créons ensuite la classe de stratégie de nouvelle tentative. Pour cela, créez un nouveau dossier appelé « Policies » et, à l’intérieur, créez la classe suivante :
using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
using System.Net;
namespace PollyProducts.Policies;
public static class ContextRetryPolicy
{
public static ResiliencePipeline<HttpResponseMessage> CreatePipeline()
{
var builder = new ResiliencePipelineBuilder<HttpResponseMessage>();
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
DelayGenerator = args =>
{
var delay = TimeSpan.FromSeconds(Math.Pow(4, args.AttemptNumber));
return new ValueTask<TimeSpan?>(delay);
},
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(response =>
(int)response.StatusCode >= 500 ||
response.StatusCode == HttpStatusCode.RequestTimeout
),
OnRetry = args =>
{
var reason = args.Outcome.Exception?.Message
?? args.Outcome.Result?.StatusCode.ToString()
?? "Unknown reason";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Retry {args.AttemptNumber}] Retrying in {args.RetryDelay.TotalSeconds}s due to {reason}");
Console.ResetColor();
return default;
}
});
return builder.Build();
}
}
Analysons le code ci-dessus. Dans celui-ci, nous utilisons ResiliencePipelineBuilder<HttpResponseMessage> pour implémenter le concept de pipeline disponible dans Polly 8. Cela permet la création de flux configurables pouvant combiner de manière transparente plusieurs stratégies telles que la nouvelle tentative, le disjoncteur, le repli et le délai d’attente. Dans ce cas, le pipeline est spécialisé dans l’utilisation de réponses HTTP.
Le AddRetry() la configuration ajoute une stratégie de nouvelle tentative. Nous définissons trois tentatives (MaxRetryAttempts = 3) et un intervalle exponentiel entre eux, où : première tentative : 4¹ = 4s, deuxième tentative : 4² = 16s et troisième tentative : 4³ = 64s. Cela évite de bombarder le service de requêtes successives et lui laisse le temps de récupérer.
De plus, nous utilisons le ShouldHandle pour définir le moment où la nouvelle tentative doit avoir lieu, couvrant deux scénarios : les exceptions réseau (HttpRequestException) et erreurs de serveur (5xx)/timeouts (408). Cela évite les tentatives inutiles sur les erreurs client (4xx), qui ne sont généralement pas temporaires.
Enfin, chaque fois qu’une nouvelle tentative se produit, nous imprimons un message coloré sur la console, que nous utiliserons plus tard pour vérifier le comportement de la nouvelle tentative en action.
L’étape suivante consiste à configurer la classe Program et à créer le point de terminaison à demander. Dans la classe Program, ajoutez donc le code suivant :
using PollyProducts.Policies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("LocalProductImageClient", client =>
{
client.BaseAddress = new Uri("http://localhost:5005/");
});
var retryPipeline = ContextRetryPolicy.CreatePipeline();
var app = builder.Build();
app.MapGet("/products/{id}/", async (int id, IHttpClientFactory httpClientFactory) =>
{
var client = httpClientFactory.CreateClient("LocalProductImageClient");
int attempt = 0;
HttpResponseMessage response = await retryPipeline.ExecuteAsync(async token =>
{
attempt++;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"[Attempt {attempt}] Requesting /photos/{id}");
Console.ResetColor();
var resp = await client.GetAsync($"/photos/{id}", token);
Console.WriteLine($"[Attempt {attempt}] Response: {(int)resp.StatusCode} {resp.StatusCode}");
return resp;
});
if (!response.IsSuccessStatusCode)
{
return Results.Problem("Image service is temporarily unavailable.");
}
var content = await response.Content.ReadAsStringAsync();
return Results.Ok(new
{
ProductId = id,
ImageInfo = content.Substring(0, Math.Min(content.Length, 120)) + "...",
RetrievedAt = DateTime.UtcNow,
Attempts = attempt
});
});
app.Run();
Dans le ContextRetryPolicy classe, nous avons créé un pipeline avec une politique de nouvelle tentative en utilisant Polly. Voyons maintenant cette politique en action.
Dans le code ci-dessus, nous utilisons l’API pour consommer un autre service HTTP local et récupérer des images de produits, que nous créerons plus tard.
Tout d’abord, nous enregistrons le HttpClient (LocalProductImageClient) pointant vers l’URL de base du service d’imagerie. Ensuite, l’application crée le pipeline à l’aide de ContextRetryPolicy.CreatePipeline(). Dans le pipeline, la politique de nouvelle tentative est créée, qui sera utilisée pour encapsuler l’appel au service externe, de sorte qu’en cas d’échec, le système réessaiera avant d’abandonner.
Ainsi, chaque fois que le /products/{id}/images le point de terminaison est appelé, l’application obtient un HttpClient de l’usine et lance la requête vers /photos/{id} sur le service externe.
À chaque nouvelle tentative, le numéro de tentative, la route demandée et le résultat de la réponse sont imprimés sur la console. Cela sera utile pour comprendre le comportement des nouvelles tentatives en temps réel, nous permettant de voir quand Polly intervient lors du test de l’application.
Si, après toutes les tentatives, la réponse échoue toujours, l’API renverra une erreur indiquant que le service d’imagerie est temporairement indisponible. Sinon, il lit le contenu de la réponse, extrait un extrait à afficher et renvoie un objet JSON contenant l’ID du produit, un résumé de la réponse, l’heure de la demande et le nombre de tentatives effectuées.
Création de l’API d’image de produit
L’API Product Image simulera le retour des données d’image du produit. Nous l’utiliserons pour tester la politique de nouvelle tentative dans l’API Catalog. Donc, pour créer l’application de base, exécutez la commande ci-dessous :
dotnet new web -o BaseImages
Ensuite, dans la classe Program, ajoutez le code suivant :
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/photos/{id}", (int id) =>
{
return Results.Ok(new
{
Id = id,
Url = $"https://samplepics.photos/id/{id}/200/200",
Title = $"Photo {id}"
});
});
app.Run("http://localhost:5005");
Dans le code ci-dessus, nous définissons une API minimale qui agit comme un service d’imagerie local.
Tout d’abord, nous définissons une route GET (/photos/{id}) qui reçoit un identifiant d’image et renvoie une réponse fictive au format JSON. Enfin, l’application s’exécute sur un port fixe (http://localhost:5005). La définition du port est importante dans ce contexte car elle conserve l’adresse de base utilisée dans la stabilité HttpClient de l’API principale.
Ainsi, tout ce dont nous avions besoin pour mettre en œuvre la politique de nouvelle tentative est prêt. Exécutons maintenant les deux API et testons les scénarios possibles.
🧪 Simulation de tentatives
Pour simuler des tentatives, exécutez d’abord l’API Catalog et envoyez une requête à la route : https://localhost:PORT/products/1 et observez les journaux dans la console. Les première et deuxième tentatives apparaîtront dans la console :

L’erreur de tentative s’est produite car l’API d’images n’est pas activée. Exécutez ensuite la deuxième API (BaseImage), l’API Catalog pourra désormais se connecter à l’API Images et la troisième tentative réussira, comme le montre l’image ci-dessous :

Notez que les premières tentatives ont abouti à une erreur, mais la troisième a réussi à se connecter à l’API de l’image et a renvoyé les données demandées.
🌱 Conclusion et prochaines étapes
Les scénarios dans lesquels les applications communiquent entre elles, recevant et envoyant des données, sont courants dans les systèmes distribués. Il est donc essentiel de concevoir un système qui vérifie que cette communication a réellement lieu. C’est là que le Retry Pattern brille, pour la construction d’API Web résistantes aux pannes inattendues.
Dans cet article, nous avons abordé les principaux problèmes pouvant survenir dans les applications distribuées et comment les résoudre en implémentant une stratégie de nouvelle tentative avec la bibliothèque open source Polly pour ASP.NET Core.
Et cela ne s’arrête pas là. Polly propose de nombreuses autres fonctionnalités puissantes que vous pouvez explorer, telles que le repli, qui vous permet de faire autre chose lorsqu’une requête échoue, ou la couverture, qui vous permet d’envoyer plusieurs requêtes à la fois et d’utiliser la réponse la plus rapide.
Source link
