Shipper en production, c’est bien. Y survivre, c’est mieux. La loi de Murphy résume l’état d’esprit à adopter : « Anything that can go wrong, will go wrong. » Tôt ou tard, une interaction avec un service externe va échouer, et la vraie question n’est pas de savoir si cela arrivera, mais comment votre application encaissera le coup.
Ce premier volet ouvre une série consacrée à la résilience : cinq patterns concrets pour rendre vos applications plus robustes face aux défaillances inévitables de la production. On commence par le plus fondamental d’entre eux, celui sur lequel s’appuieront tous les autres : le Retry.
Le principe : réessayer plutôt que d’abandonner
L’idée du Retry est aussi simple que son nom l’indique : ne pas s’arrêter à la première erreur. Une interaction entre deux services peut échouer pour mille raisons : un réseau instable, un service momentanément indisponible, ou tout simplement trop lent parce qu’il encaisse un pic de charge. Aucune de ces erreurs n’est forcément définitive.
N’essayer qu’une seule fois, c’est faire le pari que tout se passera bien du premier coup. C’est exactement le genre de supposition que la loi de Murphy se charge de démentir. Le pattern Retry consiste donc à mettre en place une stratégie pour relancer une requête qui vient d’échouer, en partant du principe qu’une nouvelle tentative a de bonnes chances d’aboutir.
Réessayer, oui, mais pas n’importe comment
Attention toutefois : retenter ne veut pas dire harceler. Si un service est déjà sous l’eau, le bombarder de requêtes en boucle ne fera qu’aggraver sa situation, jusqu’à le faire tomber complètement. C’est le piège classique du Retry mal pensé : transformer sa propre stratégie de résilience en une attaque par déni de service (DDoS) contre un dépendant déjà en difficulté.
Tout l’enjeu se joue donc dans le délai que l’on laisse entre deux tentatives, ce qu’on appelle le backoff. C’est là que se cachent les vraies décisions de conception. Passons en revue les trois stratégies les plus répandues, de la plus naïve à la plus robuste.
Pour les illustrer, on s’appuie sur la bibliothèque Effect, qui facilite grandement la définition et la composition de ces différentes stratégies de retry.
Fixed Backoff
La stratégie la plus simple : le temps entre chaque nouvelle tentative est fixe : 1s, 2s, 3s, 4s, etc.
/**
* Pour modéliser les stratégies de Retry on utilise la bibliothèque Effect,
* qui facilite grandement la définition et la composition de différentes
* stratégies.
*/
import { Duration, Effect, pipe, Schedule } from "effect";
const networkCall = Effect.tryPromise(() =>
fetch("https://jsonplaceholder.typicode.com/todos/1")
);
// FIXED BACKOFF (base * n)
const fixedBackoff = Schedule.fixed(Duration.seconds(1));
const networkCallWithRetry = networkCall.pipe(Effect.retry(fixedBackoff));
/**
* Tentative n°1 : 1s
* Tentative n°2 : 2s
* Tentative n°3 : 3s
* Tentative n°4 : 4s
* Tentative n°5 : 5s
*/
Son avantage est évident : c’est la stratégie la plus immédiate à mettre en œuvre. Une nouvelle tentative est effectuée à intervalle régulier, point.
Son défaut l’est tout autant : ce rythme constant maintient une pression importante sur le service. Or, le fait qu’une tentative continue d’échouer est précisément le signal que le service n’est toujours pas rétabli : autant en profiter pour temporiser davantage plutôt que de revenir frapper à la même cadence.
Exponential Backoff
Cette stratégie répond directement à la limite précédente : au lieu d’un délai fixe, on augmente le temps d’attente selon un facteur défini à chaque tentative : par exemple 1s, 2s, 4s, 8s, 16s.
// EXPONENTIAL BACKOFF (base * factor ^ n)
const factor = 2;
const exponentialBackoff = Schedule.exponential(Duration.seconds(1), factor);
const networkCallWithExponentialRetry = networkCall.pipe(
Effect.retry(exponentialBackoff)
);
/**
* Tentative n°1 : 1s
* Tentative n°2 : 2s
* Tentative n°3 : 4s
* Tentative n°4 : 8s
* Tentative n°5 : 16s
*/
L’intérêt est net : plus les échecs s’enchaînent, plus on espace les tentatives, laissant au service d’en face une vraie chance de reprendre son souffle.
Mais cette stratégie garde un talon d’Achille : son rythme reste parfaitement déterministe, cadencé sur le calcul d’un exponentiel. Si plusieurs clients adoptent la même stratégie au même moment, ils vont tous retenter dans les mêmes fenêtres de temps. Le service tiers se retrouve alors à encaisser des vagues de requêtes synchronisées, exactement les pics de charge récurrents qu’on cherchait à éviter.
Jitter Exponential Backoff
La parade à ce problème de synchronisation tient en un mot : le jitter. On reprend l’Exponential Backoff, mais on ajoute une part d’aléatoire au délai de chaque tentative : par exemple 1s, 2.7s, 4.5s, 8.3s, 15.2s.
// JITTERED EXPONENTIAL BACKOFF (base * factor ^ n * jitterInInterval)
const jitteredExponentialBackoff = pipe(
exponentialBackoff,
Schedule.jitteredWith({
min: 0.5,
max: 1.5,
})
);
const networkCallWithJitterExponentialRetry = networkCall.pipe(
Effect.retry(jitteredExponentialBackoff)
);
/**
* Tentative n°1 : 1s
* Tentative n°2 : 2.7s
* Tentative n°3 : 4.5s
* Tentative n°4 : 8.3s
* Tentative n°5 : 15.2s
*/
Cette variation aléatoire désynchronise les tentatives des différents clients : même s’ils ont tous échoué en même temps, ils ne reviendront pas frapper à la même seconde. On lisse ainsi la charge au lieu de la concentrer en pics alignés sur une même cadence.
La contrepartie est mineure mais réelle : le comportement devient moins prévisible, et le jitter peut introduire un léger délai supplémentaire dans le cas où le service serait justement redevenu disponible. Un compromis très largement favorable dès qu’on a plusieurs clients en jeu.
Conclusion
Le Retry est le réflexe de base de toute application résiliente : accepter qu’une erreur passagère n’a pas à condamner une opération, et lui donner une seconde chance. Mais comme souvent, le diable se cache dans les détails : ici, dans la stratégie de backoff. Un délai fixe maintient une pression inutile, un délai exponentiel temporise intelligemment, et l’ajout de jitter évite que tous vos clients ne synchronisent leurs assauts sur un service déjà fragile.
Dans la grande majorité des cas, le Jitter Exponential Backoff constitue le meilleur point de départ. Reste qu’un Retry seul ne suffit pas : retenter sans borner chaque tentative dans le temps revient à empiler des requêtes qui n’aboutissent jamais. C’est précisément le rôle du pattern suivant de cette série, le Timeout, que nous verrons dans le prochain épisode.