Robustesse logicielle et paradigmes de timeout/retry avec backoff

Robustesse logicielle et paradigmes de timeout/retry avec backoff

Table des matières

Introduction

Les programmes informatiques doivent souvent accéder à des ressources externes, comme des périphériques E/S (Input/Output) (interfaces réseau, disques de stockage) gérés par le système d’exploitation, et des services distants sur Internet.

Bien que ces ressources permettent d’interagir avec le monde extérieur, elles sont souvent peu fiables en raison de problèmes tels que la congestion réseau, les pannes de service, les redémarrages de périphériques, ainsi que toute faute de ressource.

Note

Les ressources logiques comme les verrous, en particulier les spinlocks, font aussi partie des ressources peu fiables en raison de leur nature d’acquisition par essais et erreurs.

Puisque les ressources peu fiables sont inévitables, les programmes doivent être conçus pour gérer cette instabilité. Plus précisément, ils doivent traiter les défis suivants :

  1. L’accès à la ressource peut prendre trop de temps ou ne jamais se terminer.
  2. L’accès à la ressource peut renvoyer une erreur.
  3. Une ressource peut être surchargée lorsqu’elle est sollicitée par de nombreux programmes simultanément, entraînant des erreurs ou une absence de réponse.

Pour rendre les programmes plus robustes face à ces problèmes, les stratégies suivantes sont couramment utilisées :

  1. Timeouts : définir un timeout pour l’accès à la ressource afin d’éviter d’attendre indéfiniment. Si le timeout est écoulé, on considère qu’une erreur s’est produite.
    Timeout illustré
    Illustration du timeout
  2. Politiques de retry : si un accès à la ressource échoue, réessayer selon une politique prédéfinie qui indique quand et comment retenter.
  3. Backoff et jitter : pour éviter de surcharger les ressources ou le CPU pendant les retries, utiliser une stratégie de backoff qui introduit un délai avant chaque tentative. Ajouter du jitter (variation aléatoire) au backoff permet d’éviter que plusieurs programmes ne surchargent la ressource en reprenant en même temps.
    Retry avec backoff illustré
    Illustration du retry avec backoff

Le tableau ci-dessous résume les problèmes courants de fiabilité des ressources et les stratégies associées.

Problème de fiabilité de la ressourceMesure de robustesse du programmeDescription
L’accès ne se termine jamaisTimeout d’accèsDéfinir un timeout égal au délai attendu pour obtenir un résultat. Si aucun délai n’est spécifié, le timeout est infini.
Erreur d’accèsPolitique de retryRéessayer l’accès jusqu’à réussite pour les programmes autonomes, ou arrêter après un nombre défini de tentatives pour les programmes interactifs. Réessayer sélectivement en fonction du type d’erreur.
Surcharge de la ressourceRetry avec backoffRéessayer l’accès après un délai (backoff) pour éviter de surcharger la ressource, si la politique de ressource le permet.
Surcharge due aux accès concurrentsJitter dans le backoffAjouter une perturbation aléatoire au backoff pour éviter des retries simultanés. Cela aide à prévenir l’aggravation des erreurs d’accès causée par la surcharge des ressources.

Comment appliquer les mesures de robustesse ?

Les mesures de robustesse sont configurées à l’aide de paramètres spécifiques.

Chaque ressource accédée par un programme dispose de sa propre mesure de robustesse, ce qui signifie que différentes ressources sollicitées par le même programme peuvent avoir des configurations différentes. De même, on peut appliquer des réglages différents à plusieurs accès d’une même ressource.

La mesure de robustesse globale pour un programme accédant à une ressource suit le flux de travail ci-dessous :

Flux global de mesure de robustesse
Flux global de mesure de robustesse

D’abord, la ressource est sollicitée avec un timeout, qui peut être fini ou infini.

Si l’accès réussit, le programme continue comme prévu.

Si l’accès expire ou génère une erreur, la politique de retry est vérifiée.

Si aucun retry n’est autorisé, le programme considère l’accès comme un échec et gère l’erreur (par exemple en l’affichant à l’utilisateur, en interrompant le traitement ou en la journalisant et en l’ignorant).

Si un retry est autorisé, le temps de backoff est calculé, et le programme attend ce délai avant de retenter l’accès à la ressource.

Stratégies d’accès fiable aux ressources

Utilisation du timeout

Pour empêcher un programme d’attendre indéfiniment une ressource qui risque de ne pas répondre, on définit un délai maximal d’attente, appelé timeout. Si la ressource ne répond pas pendant la période du timeout, elle est traitée comme une erreur.

Il est recommandé d’utiliser systématiquement un timeout lors de l’accès à des ressources susceptibles de répondre lentement.

Comment choisir la valeur du timeout ?

Quand un programme tente d’accéder à une ressource, il doit attendre la réponse avant de poursuivre les tâches qui en dépendent.

Pendant cette attente, le programme est essentiellement « en pause » pour cette fonction. Mais si le timeout est trop court, le programme peut abandonner trop vite et provoquer des échecs inutiles.

Ainsi, en définissant un timeout, il faut trouver le bon équilibre entre deux éléments : le temps de réponse habituel de la ressource et la réactivité attendue du programme.

Ces deux facteurs vont souvent dans des directions opposées. Un timeout plus long réduit le risque de fausses alertes, mais ralentit la réaction du programme lorsque quelque chose tourne mal. Inversement, un timeout plus court rend le programme plus réactif, mais peut provoquer des abandons prématurés.

Si vous utilisez des bibliothèques tierces pour accéder aux ressources, surveillez leurs paramètres de timeout par défaut. Certaines bibliothèques peuvent définir des timeouts très longs ou même infinis par défaut.

Comme le rappelle judicieusement un article du blog Bearer, “Libraries can’t be trusted”.

C’est pourquoi il est important de fixer le timeout en fonction d’une analyse précise du comportement réel de votre système.

Pour les connexions et les requêtes de service, un article du blog d’ingénierie Zalando du 26 juillet 2023 propose des recommandations solides sur le réglage des timeouts.

You can setup a connection timeout which is some multiple of your expected RTT. Connection timeout = RTT * 3 is commonly used as a conservative approach, but you can adjust it based on your specific needs.
In general, the connection timeout for a microservice should be set low enough so that it can quickly detect an unreachable service, but high enough to allow the service to start up or recover from a short-lived problem

Zalando engineers

Ils recommandent de fixer le timeout de connexion en fonction du round-trip time (RTT) et de le maintenir aussi court que possible. Vous pouvez déterminer le RTT avec l’utilitaire réseau ping.

Set request timeout based on collected metrics and SLA.

Zalando engineers

Ils suggèrent de définir le timeout de requête en fonction de l’accord de niveau de service (SLA) du programme, du SLA de la ressource, et des mesures de latence collectées.

Dans le contexte des services, Kevin Ahlquist explique comment ils ont déterminé les timeouts chez Bluestem dans un article du 21 juillet 2016.

1. Load test to find the service’s behavior under maximum steady-state load (i.e. how much load can this service sustain indefinitely before it tips?).
2. Gather empirical data on throughput, latency, and error rate from those load tests.
3. Set baseline timeouts (and alerts) based on this data.
4. Refine timeouts based on continued data-gathering to bring error rates to acceptable levels.

Kevin Ahlquist

La leçon principale est de fixer les timeouts à partir de données d’essais solides. Pour plus de détails sur la manière dont ils ont utilisé ces données pour déterminer les timeouts, consultez l’article complet.

Doit-on réessayer ?

Quand un accès à une ressource génère une erreur — qu’elle provienne d’un timeout, d’une indisponibilité ou d’un problème interne — une façon de traiter le cas est de réessayer. Mais avant de retenter, il est important de décider si le retry vaut le coût. Cette décision est guidée par une politique de retry.

Si la politique prévoit un retry, une période de backoff est calculée et utilisée. La valeur du backoff est déterminée selon les stratégies de backoff et les stratégies de jitter.

Deux éléments clés contrôlent la gestion des retries : les politiques de retry et le délai de backoff.

Politiques de retry

Les politiques de retry décident s’il faut retenter un accès après une erreur. En termes simples, elles définissent jusqu’à quel point les tentatives peuvent se poursuivre. Une politique de retry précise combien de fois (max-retries) ou pendant combien de temps (max-delay) un accès à une ressource doit être relancé.

Une politique de retry peut définir différents max-retries ou max-delay selon le type d’erreur observée. Par exemple, la politique peut autoriser 10 retries pour un timeout, mais aucun retry pour d’autres erreurs.

  • max-retries est une valeur entière telle que :

    • 0 signifie aucun retry.
    • Une valeur négative signifie des retries infinis.
    • Une valeur positive représente le nombre de tentatives avant d’abandonner.
  • max-delay est un nombre réel configuré de manière similaire à max-retries.

Comment décider des valeurs max-retries/max-delay ?

Avant de choisir des retries, prenez en compte les éléments suivants :

  • Les erreurs doivent être transitoires : ne réessayez pas si l’erreur est permanente, car le retry ne la résoudra pas.
  • Les accès doivent être idempotents : le retry ne doit pas poser de problème si la même opération est répétée. La ressource doit se comporter comme si les tentatives échouées n’avaient jamais eu lieu.
  • Tenez compte des accès sensibles au temps : il est inutile de réessayer si le résultat sera périmé au moment où il arrive. Par exemple, si des données en temps réel sont nécessaires sous 2 secondes, retenter au-delà de ce délai n’a pas de sens.

    Cela est particulièrement vrai pour les programmes interactifs où une réponse rapide est essentielle à une bonne expérience utilisateur, comme le définit l’accord de niveau de service (SLA).

    Pour les accès sensibles au temps, vous pouvez fixer le seuil de retry avec max-delay, qui représente le temps total écoulé depuis la première tentative.

Suivez ces directives générales pour fixer le seuil de retry :

  1. Si l’erreur est permanente ou si l’accès à la ressource n’est pas idempotent, ne réessayez pas.
  2. Si le programme peut attendre indéfiniment (comme certains microservices), réessayez infiniment.
  3. Sinon, fixez max-retries/max-delay à une valeur positive basée sur le SLA (utile pour les programmes plus interactifs).

Note

Il existe des politiques de retry plus avancées, telles que :

L’article de blog de 2022 de Marc Brooker évalue diverses politiques de retry.

Choisir la bonne politique est crucial, notamment pour l’accès aux services.

That doesn’t mean that backing off between retries is a bad idea. It’s a good idea, but only helps for short term overload (spikes, etc). It does not reduce the total work in the system, or total amplification factor of retries.

Marc Brooker

Stratégies de backoff

Les stratégies de backoff sont des algorithmes utilisés pour calculer le temps d’attente avant de retenter une opération échouée. Explorons quelques stratégies de backoff courantes.

Tout d’abord, voici quelques termes clés à connaître :

  • Valeur de base de backoff vbasev_{base} : c’est le temps d’attente minimal possible. C’est le point de départ du calcul du backoff. Sa valeur est non négative.
  • Valeur maximale de backoff vmaxv_{max} : c’est la limite supérieure du temps de backoff. Le backoff ne peut pas dépasser cette valeur.
  • Nombre de retries xx : c’est le nombre de retries consécutifs effectués jusqu’à présent. Il commence à 0 et augmente à chaque retry, puis revient à 0 après un accès réussi à la ressource.
  • Temps de backoff vxv_{x} : c’est le temps de backoff après xx retries. Il est calculé selon la stratégie utilisée.

On note aussi le minimum de deux nombres aa et bb par min(a,b)min(a, b).

Note

Dans les stratégies de backoff, les valeurs de vbasev_{base}, vmaxv_{max} et xx sont les entrées utilisées par les algorithmes. Le résultat de ces algorithmes est vxv_x, qui est le temps de backoff calculé.

Essentiellement, vxv_x est la durée d’attente avant de retenter après xx retries, telle que déterminée par la stratégie choisie.

Maintenant, explorons les différentes stratégies de backoff.

Toutes les stratégies de backoff
Stratégies de backoff avec vbase=1,vmax=60,m=2,L=2,p1=2v_{base}=1, v_{max}=60, m=2, L=2, p1=2
Backoff exponentiel

Le backoff exponentiel est l’une des stratégies les plus courantes dans les systèmes distribués. Ici, le temps de backoff augmente de manière exponentielle à chaque retry.

Étant donné un multiplicateur mm (où m1m \ge 1) :

vx=min(vmax,vbasemx)v_x = min(v_{max}, v_{base} * m^{x})

Soyez prudent avec le backoff exponentiel, car il peut rapidement ralentir votre programme en raison de l’augmentation rapide des délais d’attente.

La valeur la plus courante pour mm est 2.

Note rapide : certaines implémentations simplifient cela en prenant toujours le multiplicateur égal à la base, ce qui donne la formule : vx=min(vmax,(vbase)x+1)v_x = min(v_{max}, (v_{base})^{x+1}).

Backoff linéaire ou par intervalle

Avec le backoff linéaire, le temps d’attente augmente d’un intervalle fixe LL après chaque retry.

Étant donné un intervalle LL (où L0L \ge 0) :

vx=min(vmax,vbase+Lx)v_x = min(v_{max}, v_{base} + L * x)

Note rapide : certaines implémentations fixent toujours l’intervalle sur la valeur de base, ce qui donne la formule : vx=min(vmax,vbase(x+1))v_x = min(v_{max}, v_{base} * (x + 1)).

Backoff de Fibonacci

Cette stratégie utilise la suite de Fibonacci pour calculer le temps de backoff, ajoutant davantage de variabilité qu’un backoff linéaire.

Étant donné le nombre de Fibonacci Fib(i)Fib(i) tel que Fib(0)Fib(0) est le premier nombre de Fibonacci :

vx=min(vmax,vbase+Fib(x))v_x = min(v_{max}, v_{base} + Fib(x))

Backoff polynomial

Le backoff polynomial utilise une fonction polynomiale pour calculer le temps de backoff, ce qui offre une augmentation plus personnalisable des délais d’attente.

Étant donné nn exposants polynomiaux p1,p2,...,pnp_{1},p_{2},...,p_{n} (où chaque p>1p > 1) :

vx=min(vmax,vbase+xp1+xp2+...+xpn)v_x = min(v_{max}, v_{base} + x^{p_{1}} + x^{p_{2}} + ... + x^{p_{n}})

Backoff aléatoire

Le backoff aléatoire ajoute un élément d’imprévisibilité en choisissant une valeur aléatoire entre vbasev_{base} et vmaxv_{max} pour chaque retry.

vx=random(vbase,vmax)v_x = random(v_{base}, v_{max})

Backoff constant

Ici, le temps de backoff reste constant, toujours égal à la valeur de backoff initiale.

vx=vbasev_x = v_{base}

Note rapide : le backoff constant peut être implémenté avec un backoff exponentielm=1m=1, un backoff linéaireL=0L=0, ou en fixant vmax=vbasev_{max} = v_{base} dans toute autre stratégie.

Note rapide : certaines implémentations incluent une stratégie Sans Backoff, qui est simplement un cas particulier de backoff constant où vbase=0v_{base} = 0.

Utilisation du jitter

Quand plusieurs programmes tentent d’accéder à la même ressource en même temps, cela peut provoquer des erreurs dues aux accès concurrents.

Si ces programmes utilisent le même timeout et les mêmes politiques de retry, ils risquent de retenter au même moment, ce qui peut générer de nouvelles erreurs d’accès.

C’est particulièrement fréquent avec des serveurs sollicités par de nombreux clients identiques, comme des interfaces web.

Quand beaucoup de clients envoient des requêtes en même temps, le serveur peut se trouver surchargé et renvoyer des erreurs. Comme ces clients suivent la même stratégie de backoff, ils retenteront également en même temps, ce qui aggrave le problème.

Pour l’éviter, une solution courante consiste à ajouter une composante aléatoire au temps de backoff — appelé jitter.

Le jitter introduit des variations aléatoires sur le temps de backoff vxv_x, calculé par les stratégies de backoff, et/ou sur le nombre de retries xx.

Plusieurs algorithmes permettent d’ajouter du jitter au backoff, produisant un jittered_backoffjittered\_backoff.

Ci-dessous se trouvent des algorithmes de jitter couramment utilisés. Chacun prend le backoff vxv_{x} calculé par les stratégies de backoff et retourne un jittered_backoffjittered\_backoff. Ils peuvent aussi modifier la valeur de xx.

Tous les algorithmes de jitter
Algorithmes de jitter illustrés pour un backoff exponentiel, avec vbase=1,vmax=60,m=2v_{base}=1, v_{max}=60, m=2

Les # retries attempted (x) marqués en rouge sont les points où le jitter a eu un impact, c’est-à-dire où de l’aléatoire a été ajouté au calcul final du backoff.

Illustration de l’utilité du jitter
Illustration de l’utilité du jitter : backoff exponentiel avec Full Jitter
Full Jitter

Cet algorithme choisit aléatoirement soit le temps de backoff calculé, soit aucun backoff du tout. Le jittered_backoffjittered\_backoff est calculé comme suit :

jittered_backoff=random_choice(0,backoff)jittered\_backoff = random\_choice(0, backoff)

random_choice(a,b)random\_choice(a, b) est une fonction qui retourne aléatoirement l’un de ses arguments.

Equal Jitter

Cet algorithme applique un jitter complet à la moitié du backoff calculé :

jittered_backoff=(backoff/2)+random_choice(0,backoff/2)jittered\_backoff = (backoff / 2) + random\_choice(0, backoff / 2)

Jitter décorrelé

Cet algorithme réinitialise le nombre de retries xx (utilisé comme paramètre dans les stratégies de backoff) avec une probabilité de 0.50.5.

L’idée est de faire « oublier » les retries précédents à l’algorithme de backoff, puis d’utiliser le nouveau temps de backoff calculé.

(1) deˊfinir x=random_choice(0,x) puis (2) calculer backoff puis(3) deˊfinir jittered_backoff=backoff\text{(1) définir } x = random\_choice(0, x)\text{ puis }\\\text{(2) }\text{calculer }backoff\text{ puis}\\\text{(3) définir } jittered\_backoff = backoff

Pas de jitter

Aucun jitter n’est appliqué. Le jittered_backoffjittered\_backoff est simplement le backoff calculé :

jittered_backoff=backoffjittered\_backoff = backoff

Mise à l’échelle du backoff avec jitter

Pour mettre à l’échelle le backoff avec jitter par un facteur, soit ff un nombre réel strictement positif. Le backoff final devient :

final_backoff=jittered_backoff×ffinal\_backoff = jittered\_backoff \times f

Ce facteur est utile pour augmenter ou diminuer les valeurs de backoff calculées sans modifier les autres paramètres du calcul.

Implémentation

Les stratégies robustes d’accès aux ressources sont généralement implémentées sous forme de bibliothèques qui enveloppent les accès aux ressources pour gérer les timeouts et les retries. Dans cette section, nous verrons comment concevoir de telles bibliothèques.

Conception principale

Au cœur de la conception, chaque accès à une ressource doit être implémenté dans une fonction. Le programme utilise ensuite la bibliothèque de robustesse pour appeler cette fonction.

La bibliothèque fournit une fonction de haut niveau, que l’on appellera access_wrapper, qui entoure l’accès à la ressource.

La fonction access_wrapper prend la fonction d’accès à la ressource comme paramètre, appelée ici resource_access.

Vous pouvez retrouver un exemple d’implémentation dans la section Exemple d’implémentation.

Cœur de la robustesse

La bibliothèque doit également permettre de configurer les mécanismes de timeout et de retry.

Timeout

Le timeout doit être configurable, où :

  • une valeur négative indique un timeout infini.
  • une valeur non négative fixe la durée réelle du timeout.

Retry

Le mécanisme de retry implique plusieurs configurations clés :

  • Configuration de la politique de retry : définit les politiques pour décider quand réessayer.
  • Configuration de la stratégie de backoff : spécifie la stratégie de backoff à appliquer.
  • Configuration de la stratégie de jitter : définit la stratégie de jitter pour ajouter de l’aléatoire au backoff.
  • Facteur d’échelle de backoff : détermine le facteur d’échelle pour ajuster le backoff.
Configuration des politiques de retry

Chaque politique de retry inclut des paramètres de configuration pour initialiser son état. Les variables d’état importantes peuvent inclure max-retry, max-delay, le nombre actuel de retries et l’horodatage de la première tentative.

La politique définit une fonction retry_allowed pour décider s’il faut retenter. Cette fonction renvoie un booléen et met à jour l’état de la politique de retry en fonction des paramètres comme l’état courant et l’erreur observée.

Elle définit aussi une fonction new_access, qui réinitialise le compteur de retries et l’horodatage de la première tentative.

Différentes politiques de retry devraient être configurables pour des types d’erreurs spécifiques.

Configuration de la stratégie de backoff

Chaque stratégie de backoff dispose de paramètres de configuration pour calculer le backoff. Ceux-ci incluent généralement le backoff de base, le backoff maximal, et des paramètres spécifiques à la stratégie comme le multiplicateur pour le backoff exponentiel.

Une fonction calculate_backoff calcule la valeur brute du backoff en fonction du nombre de retries et des paramètres de configuration.

La bibliothèque devrait permettre de choisir des stratégies de backoff différentes selon les types d’erreurs, afin de ne pas appliquer la même stratégie à toutes les erreurs.

Configuration de la stratégie de jitter

Chaque stratégie de jitter comprend une fonction apply_jitter qui ajoute du jitter au backoff brut.

Cette fonction prend en entrée une référence au nombre de retries (qu’elle peut modifier) et la fonction calculate_backoff. Elle renvoie le jittered_backoff.

La bibliothèque devrait permettre de choisir des stratégies de jitter différentes pour des types d’erreurs différents.

Mise à l’échelle du backoff

Le jittered_backoff est mis à l’échelle en le multipliant par un facteur configuré positif. Une fonction scale_backoff effectue cette mise à l’échelle, en prenant le jittered_backoff et le facteur d’échelle, puis en renvoyant le final_backoff mis à l’échelle.

La bibliothèque doit permettre de choisir des facteurs d’échelle différents pour des types d’erreurs différents.

Fonctionnalités additionnelles

Parfois, le programme doit être informé des retries et effectuer des actions spécifiques lors des erreurs d’accès ou des succès.

Pour cela, la bibliothèque doit permettre à la fonction access_wrapper d’accepter deux paramètres supplémentaires : des callbacks déclenchés en cas de succès ou d’erreur :

  • on_success : ce callback est appelé lorsque l’accès à la ressource réussit. Il ne prend aucun paramètre et ne renvoie rien.
  • on_error : ce callback est appelé lorsque l’accès à la ressource échoue. Il prend en paramètre l’erreur observée et éventuellement le nombre de retries, et il ne renvoie rien.

La bibliothèque Python Backoff-Utils implémente ces fonctionnalités.

Exemple d’implémentation

Voici un exemple d’implémentation d’un timeout et d’un retry avec backoff dans une bibliothèque d’exemple appelée robustlib.

Cette bibliothèque utilise un backoff exponentiel avec full jitter. La politique de retry est configurée pour ne réessayer que lors d’une ValueError ou d’une TimeoutError Python.

  1import time
  2import random
  3
  4# Bibliothèque pour l’accès robuste aux ressources
  5
  6class RetryPolicy:
  7    def __init__(self, max_retry, max_delay):
  8        self.max_retry = max_retry
  9        self.max_delay = max_delay
 10        self.current_retries = 0
 11        self.first_try_timestamp = time.time()
 12    
 13    def retry_allowed(self, error):
 14        """Détermine si un retry est autorisé en fonction de l’état actuel et de l’erreur."""
 15        if not isinstance(error, (ValueError, TimeoutError)):
 16            # Réessayer seulement pour ValueError
 17            return False  
 18
 19        if self.current_retries >= self.max_retry:
 20            # Ne pas réessayer si le nombre maximum de retries est atteint
 21            return False  
 22
 23        if time.time() - self.first_try_timestamp > self.max_delay:
 24            # Ne pas réessayer si le délai maximum est dépassé
 25            return False  
 26
 27        return True
 28    
 29    def new_access(self):
 30        """Réinitialise l’état des retries pour une nouvelle tentative d’accès à la ressource."""
 31        self.current_retries = 0
 32        self.first_try_timestamp = time.time()
 33
 34    @property
 35    def retry_count(self):
 36        return self.current_retries
 37
 38    @retry_count.setter
 39    def retry_count(self, new_value):
 40        assert new_value >= 0, "Invalid new_value"
 41        self.current_retries = new_value
 42
 43class BackoffStrategy:
 44    def __init__(self, *, base_backoff, max_backoff, multiplier=2):
 45        self.base_backoff = base_backoff
 46        self.max_backoff = max_backoff
 47        self.multiplier = multiplier
 48    
 49    def calculate_backoff(self, retry_count):
 50        """Calcule le délai de backoff en fonction du nombre de retries."""
 51        backoff = min(
 52            self.base_backoff * (self.multiplier ** retry_count), 
 53            self.max_backoff
 54        )
 55        return backoff
 56
 57class JitterStrategy:
 58    def apply_jitter(self, calculate_backoff_func, retry_count_ref):
 59        """Applique un full jitter au backoff calculé par la stratégie."""
 60        base_backoff = calculate_backoff_func(retry_count_ref.retry_count)
 61        jittered_backoff = random.uniform(0, base_backoff)
 62        return jittered_backoff
 63
 64class AccessWrapper:
 65    def __init__(self, *, retry_policy, backoff_strategy, jitter_strategy, scaling_factor=1.0, timeout=-1, 
 66                 on_success=None, on_error=None):
 67        self.timeout = timeout
 68        self.retry_policy = retry_policy
 69        self.backoff_strategy = backoff_strategy
 70        self.jitter_strategy = jitter_strategy
 71        self.scaling_factor = scaling_factor
 72        self.on_success = on_success
 73        self.on_error = on_error
 74    
 75    def wrap(self, resource_access):
 76        """Enveloppe la fonction d’accès à la ressource avec les stratégies de robustesse."""
 77        def wrapper(*args, **kwargs):
 78            retry_policy_state = self.retry_policy
 79            retry_policy_state.new_access()
 80
 81            while True:
 82                try:
 83                    # Tenter l’accès à la ressource
 84                    result = resource_access(self.timeout, *args, **kwargs)
 85
 86                    # Appeler le callback de succès si fourni
 87                    if self.on_success:
 88                        self.on_success()
 89
 90                    return result
 91                except Exception as error:
 92                    # Appeler le callback d’erreur si fourni
 93                    if self.on_error:
 94                        self.on_error(error, retry_policy_state.retry_count)
 95                    
 96                    # Vérifier si un retry est autorisé
 97                    if not retry_policy_state.retry_allowed(error):
 98                        raise
 99
100                    # Calculer le backoff avec jitter et mise à l’échelle
101                    calculate_backoff = self.backoff_strategy.calculate_backoff
102                    jittered_backoff = self.jitter_strategy.apply_jitter(calculate_backoff, retry_policy_state)
103                    final_backoff = jittered_backoff * self.scaling_factor
104
105                    # Attendre avant de retenter
106                    time.sleep(final_backoff)
107                    
108                    # Incrémenter le compteur de retries
109                    retry_policy_state.retry_count += 1
110        
111        return wrapper

Voici un exemple d’utilisation de la bibliothèque robustlib :

 1import random
 2from datetime import datetime
 3import robustlib
 4
 5# Configurer le wrapper d’accès
 6retry_policy = robustlib.RetryPolicy(max_retry=5, max_delay=60)
 7backoff_strategy = robustlib.BackoffStrategy(base_backoff=1, max_backoff=32, multiplier=2)
 8jitter_strategy = robustlib.JitterStrategy()
 9
10access_wrapper = robustlib.AccessWrapper(
11    retry_policy=retry_policy,
12    backoff_strategy=backoff_strategy,
13    jitter_strategy=jitter_strategy,
14    scaling_factor=1.0,
15    on_success=lambda: print(f"[{datetime.now().time()}] on_success called"),
16    on_error=lambda e, c: print(f"[{datetime.now().time()}] on_error: attempt {c} failed with: {e}")
17)
18
19# Définir une fonction d’accès à la ressource
20def example_resource_access(timeout):
21    # Simuler un accès à la ressource qui peut expirer
22    if random.random() < 0.3:
23        raise TimeoutError("Simulated timeout error")
24
25    # Simuler un accès à la ressource qui peut échouer
26    if random.random() < 0.3:
27        raise ValueError("Simulated resource access error")
28
29    return "Resource accessed successfully"
30
31# Envelopper la fonction d’accès à la ressource
32wrapped_resource_access = access_wrapper.wrap(example_resource_access)
33
34# Tenter d’accéder à la ressource
35try:
36    result = wrapped_resource_access()
37    print(result)
38except Exception as e:
39    print(f"Failed to access resource: {e}")

Sample output:

[05:34:16.749457] on_error: attempt 0 failed with: Simulated timeout error
[05:34:16.895572] on_error: attempt 1 failed with: Simulated timeout error
[05:34:17.063954] on_error: attempt 2 failed with: Simulated resource access error
[05:34:21.044946] on_error: attempt 3 failed with: Simulated timeout error
[05:34:27.702886] on_error: attempt 4 failed with: Simulated resource access error
[05:34:41.832590] on_success called
Resource accessed successfully

Conclusion

A good approach to retries combines backoff, jitter, and a good retry policy.

Marc Brooker

Dans ce guide, nous avons exploré les stratégies pour améliorer la robustesse des programmes lors de l’accès aux ressources, en insistant sur l’importance de combiner timeout, backoff, jitter et politiques de retry.

Ces éléments, correctement configurés, offrent une approche flexible et adaptable pour gérer l’accès aux ressources, en particulier dans les systèmes distribués.

Autres applications

Ces stratégies ne se limitent pas à l’accès aux services — elles sont aussi essentielles dans des domaines comme les protocoles réseau, l’accès aux bases de données, les verrous de fichiers et les interactions matérielles. Voici quelques exemples :

  • Protocoles réseau : le backoff exponentiel avec full jitter aide à prévenir la congestion réseau lors des retries.
  • Accès aux bases de données : une politique de retry fixe (retries non infinis) avec un backoff croissant peut réduire l’impact sur les verrous et l’état des transactions.
  • Verrous de fichiers : des retries immédiats avec un backoff minimal sont utiles quand on s’attend à ce que les verrous soient libérés rapidement.

En plus des stratégies de retry courantes disponibles dans des bibliothèques comme Python’s Backoff-Utils, Rust’s Backoff et Rust’s Tokio-Retry, il est également possible de créer des stratégies personnalisées adaptées à des besoins spécifiques.

Une bibliothèque flexible doit offrir une interface permettant aux développeurs d’implémenter facilement ces stratégies personnalisées.

Lectures complémentaires

En comprenant et en appliquant correctement ces stratégies, vous pouvez renforcer significativement la résilience et la fiabilité de vos systèmes dans de nombreuses applications.

Partager :

Articles Connexes

Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité

Les structures de données en microservices évoluent en permanence. Découvrez pourquoi concevoir pour l’évolutivité et appliquer une stratégie de …

Lire la suite about Évolution des données en microservices : éviter les changements cassants grâce à la compatibilité
Explication Illustrative de la Faute, de l'Erreur, de la Défaillance, du Bug et du Défaut Logiciel

Explication Illustrative de la Faute, de l’Erreur, de la Défaillance, du Bug et du Défaut Logiciel

Les logiciels ne se comportent pas toujours comme prévu. Des erreurs dans l’implémentation ou dans la spécification des exigences peuvent …

Lire la suite about Explication Illustrative de la Faute, de l'Erreur, de la Défaillance, du Bug et du Défaut Logiciel