Polly n’est pas qu’une balade de Nirvana, que les jeunes générations peuvent ne pas connaître mais qui, pour les autres, vous restera maintenant en tête pendant la lecture du reste de cet article.
Polly est aussi une librairie Open Source .NET permettant de renforcer considérablement la robustesse de votre application en implémentant assez simplement des politiques de rejeu (retry), de circuit breaker, de gestion des timeouts, d’isolation et de fonctionnement de repli (fallback mode).
Il existe d’autres librairies qui se positionnent sur ce sujet (Hystrix.NET, portage de la librairie de référence sur le sujet dans le monde Java, ou quelques librairies fournies par Microsoft dans le contexte d’applications reposant sur Azure), mais cette librairie Polly est le projet le plus vivant et bénéficiant d’une très bonne réputation sur son domaine.
La mise en place de ces politiques permet de rendre notre application plus tolérante aux erreurs, que celles ci soient internes à l’application, ou liées à son environnement extérieur (échec d’appel de services tiers, défaillances réseaux etc). Cette tolérance aux erreurs est essentielle pour les applications distribuées, notamment celles hébergées dans le cloud.
L’objectif de cet article est de vous montrer quelques exemples d’implémentation de ces patterns avec Polly, et de mettre en lumière que des améliorations substantielles de la robustesse des applications que nous écrivons sont à notre portée … avec peu d’effort !
Contexte de nos futures démos
Nous allons réaliser quelques démos de ce que l’on peut faire avec Polly.
On va se placer dans un scénario où l’on désire fiabiliser la section d’un service applicatif qui gère une commande pour un site de eCommerce. Cette section de code procède à la phase de paiement en appelant un Web Service extérieur, et persiste en base de données le nouvel état de la commande une fois le paiement réalisé. On va simuler dans notre code des défaillances dans le Web Service extérieur (retour d’erreur, timeout), et dans la couche de persistance en base de données.
Polly est disponible sous forme d’un package NuGet compatible avec .NET 4.5 et .NET Standard 1.1 pour sa dernière version (5.2.0 lors de l’écriture de cet article). On crée une application vierge, on fait référence à ce package, et c’est parti…
La méthode que l’on souhaite sécuriser se présente initialement comme suit ; notez qu’elle repose sur 2 interfaces qui lui sont fournies dans le constructeur de la classe, le gestionnaire de paiement et le gestionnaire de persistance. Ce sont les implémentations de ces 2 interfaces que nous allons fragiliser artificiellement pour pouvoir démontrer l’ajout de robustesse dans notre application.
Les politiques de gestion d’exceptions de Polly
Nous allons implémenter 4 politiques de gestion d’exceptions via Polly :
- Retry Forever : rejeu infini (simpliste)
- Retry : rejouer un certain nombre de fois
- Wait and Retry : attendre un laps de temps et réessayer
- Circuit Breaker : appel d’implémentations alternatives en cas d’erreur (plus complexe)
Nous verrons que chacune de ces politiques correspond à des stratégies bien différentes, et qu’elles se configurent de manière très simple, avec des expressions Fluent (donc lisibles !).
Au fur et à mesure du passage en revue de ces familles de politiques, on verra quelques subtilités transverses (lancer des actions sur un retry etc).
De manière macro, Polly fonctionne
- en choisissant la ou les types d’Exception qui serviront de critère de déclenchement pour lancer du rejeu
- en définissant la politique de rejeu : action à dérouler sur chaque rejeu, nombre et conditions du rejeu
- en laissant Polly exécuter notre code dans son propre contexte
La politique RetryForever
Essayons le cas le plus simple pour comprendre le fonctionnement de Polly…
Dans le code ci dessous,
- on commence par définir la politique de rejeu : on intercepte les exceptions WebExceptions et on demande à la politique de tenter des rejeux de manière infinie (RetryForever).
- Ensuite, on lance l’appel à notre module de traitement en demandant à Polly de l’exécuter dans le cadre de la politique de rejeu qu’on vient de définir.
Comme on a configuré le module pour avoir recours à un service de paiement qui plante 2 fois sur 3 avec une exception WebExceptions, le code va s’exécuter 3 fois (2 appels de Web Services en échec, puis un succès), pour le rendu suivant.
On notera que si le service de paiement avait lancé une exception d’un autre type, nous nous serions retrouvé dans le catch de l’appel à Polly et le rejeu n’aurait pas été poursuivi. Pour info, on peut avec Polly imbriquer les Politiques et gérer les Exceptions différemment selon leur nature. Bien entendu, ce sont les exceptions correspondant à des erreurs transitoires qui nous intéressent, car les exceptions systématiquement reproductibles (comme une division par zéro) ne doivent pas donner lieu à rejeu.
On notera aussi un premier aspect de la puissance de Polly : la politique de rejeu est définie en dehors de l’exécution du code que l’on veut fiabiliser, ce qui permet d’avoir des politiques en commun pour de multiples modules de notre application, et de ne pas mélanger ce code « de plomberie » au code « utile » !
Alors bien sûr, vous me direz que tout cela n’est pas utile car on aurait pu gérer un rejeu infini sans Polly dans une boucle while() un peu moche, donc on va monter d’un cran pour essayer une politique plus évoluée, la Retry.
La politique Retry
Dans cet exemple, on va définir une politique de rejeu limité, et on va associer une action à déclencher à chaque rejeu.
Dans la politique de Retry, on va définir le nombre maximum de fois que l’on souhaite voir notre code être rejoué, tant que le type des Exceptions retournées correspond à notre politique. Si le code échoue sur la limite maximale de rejeu, on peut exécuter du code spécifique.
Nous allons en profiter pour illustrer l’exécution de code pour chaque rejeu (expression lambda qui peut utiliser l’Exception interceptée et le compteur de retry), et pour montrer comment gérer 2 types d’exceptions dans notre critère de rejeu (WebException ou SqlException).
Le code se présente alors ainsi :
Et l’exécution donne bien un succès après 2 essais, avec affichage du message provenant de l’action lancée à chaque échec :
Notez que si le code avait échoué pour cause d’atteinte du nombre de rejeu maximum, l’exception finale aurait été transmise depuis le bloc policy.Execute(), et on doit alors traiter l’Exception dans le catch qui encapsule l’appel à policy.Execute(), comme si il n’y avait pas eu de rejeu. Polly est alors transparent pour le développeur, ce qui est particulièrement élégant car on peut alors ajouter/supprimer les recours à Polly sans modifier la structuration de notre code.
La politique WaitAndRetry
Cette politique est plus intéressante car elle permet d’introduire des temporisations entre les rejeux, ce qui augmente les chances de succès lorsque l’on interroge des services tiers qui peuvent souffrir de surcharges temporaires.
On peut tout simplement utiliser un intervalle de temps constant entre chaque rejeu (essayer toutes les 5 secondes par exemple), ou utiliser une fonction de calcul de temps d’attente (ce qui permet d’implémenter des algorithmes classiques d’attente exponentielle pour éviter de saturer les services tiers).
Dans le code ci dessous, on indique une politique basée sur 3 rejeux, avec un intervalle de temps d’attente variable (2, 5 puis 10 secondes).
L’exécution donne bien le résultat attendu (observer les timestamps qui montrent qu’on a bien des temps d’attente variables).
La politique CircuitBreaker
La politique CircuitBreaker est plus élaborée.
Le principe du CircuitBreaker permet de cesser temporairement d’appeler un composant si celui ci échoue consécutivement un certain nombre de fois; on peut faire une analogie avec un système physique que l’on « laisserait refroidir » car il aurait trop chauffé et serait alors défaillant, avant de le solliciter de nouveau. On est donc en mesure de protéger notre code de trop fortes sollicitations si celui-ci rencontre des difficultés à traiter la charge.
Lorsque l’on utilise CircuitBreaker dans Polly et que le critère de « rupture du circuit » est atteint, c’est à dire n exceptions consécutives lors de l’utilisation d’une Policy CircuitBreaker), alors tout appel via cette Policy pendant la période de coupure générera une Exception. Une fois la période de coupure écoulée, tous les appels via la Policy seront de nouveau autorisés et exécutés.
Le code se présente ainsi :
On définit une politique où les appels seront bloqués et une exception levée si 2 echecs sur constatés, et que les appels seront alors bloqués sur une période de 5 secondes
L’exécution donne :
On voit qu’après 2 échecs, les conditions de coupure du circuit sont réunie et une exception spécifique à Polly est levée au 3ème appel (où le message indique que le circuit est ouvert : dans le monde de l’électricité, cela signifie que le courant ne passe plus). Les appels à notre composants sont alors interrompus pendant 5 secondes, et sont de nouveau transmis une fois ce temps écoulé. L’essai suivant réussit (car notre composant est codé pour échouer 2 fois sur 3).
D’autres politiques Polly : approches proactives
La version 5 de Polly apporte toute une série de nouvelles politiques.
Les politiques que nous venons de voir dans cet article sont des politiques réactives : elles définissent le comportement à adopter en réaction à un dysfonctionnement (exceptions).
En version 5, Polly apporte des possibilités de politiques proactives, c’est à dire visant à protéger l’exécution du programme pour éviter que les exceptions ne soient générées.
Je ne rentrerai pas avec le même niveau de détails pour ces nouvelles politiques, mais passons les néanmoins en revue…
- Timeout : permet de lancer une action qui sera arrêté en cas de dépassement d’une durée d’exécution donnée, afin la consommation de ressources par l’application à cause d’appels semblant voués à l’échec
- Fallback : permet de lancer une action en cas d’échec de l’appel de code exécuté dans cette policy (ce qui est donc grosso modo un try/catch avancé, avec en bonus la lisibilité de la syntaxe fluent)
- Bulkhead (« cloison » d’un bateau, en anglais) :
- l’idée de cette politique est d’offrir un environnement d’exécution très contrôlé pour optimiser la consommation de ressources.
- On définit
- le nombre d’exécutions en parallèle autorisées dans la policy,
- le nombre d’appels qui peuvent être mis dans une file d’attente de la libération d’un des slots d’exécution)
- Toute tentative d’appel une fois la file d’attente pleine génère une exception renvoyées à l’appelant.
- Cette politique vise à optimiser la consommation des ressources d’un processus : ni trop (slots d’exécutions limités), ni trop peu (file d’attente pour pouvoir réalimenter les slots d’exécutions au plus vite).
- Cette politique a du sens quand on veut gérer très finement la consommation de nos processus, notamment dans des contextes de recours au cloud où l’on veut consommer tout ce que l’on achète, sans saturer nos processus pour assurer une qualité de service. De la haute couture qui peut être difficile à tuner !
Conclusion
Polly est une librairie permettant d’exprimer de façon très simple, via une syntaxe Fluent, des conditions d’exécution de code amenant une grande résilience vis à vis de défaillances extérieures.
Il n’est bien entendu pas question d’injecter des recours à Polly partout dans une application, mais aux sections sensibles qui peuvent être confrontées à des défaillances transitoires de systèmes tiers : réseau, accès disque, base de données.
L’utilisation de Polly se fait de manière élégante en ayant recours au pattern Decorator pour enrichir de fonctionnalités résilientes une implémentation existante, sans toucher à cette implémentation ou au code l’appelant.
J’espère que vous aurez l’occasion d’utiliser cette librairie dans vos projets, pour le plus grand bonheur de vos utilisateurs et de vos équipes de gestion de la production !
PS : mais au fait, pourquoi « Polly » ?
Polly est le sobriquet utilisé pour les perroquets dans la culture anglo-saxonne (un peu comme « coco » dans la culture française). D’où le nom (et le logo) de la librairie Polly.
Et le rapport entre les perroquets et une librairie gérant la résilience ? Et bien, un perroquet … ça répète ce qu’on lui dit, non ? Et là, on a géré des répétitions d’exécutions de code, non ?
Oui, ça vient de loin et c’est un peu tiré par les cheveux. Et ça n’a donc aucun rapport avec la chanson de Nirvana… (et voilà vous l’avez de nouveau en tête !)