Aller au contenu

Appeler un service extérieur

Votre module appelle un transporteur, un fournisseur de taux, une API de facturation. Ça marche pendant six mois. Puis un jour, le service d’en face ne répond plus : il ne refuse pas, il ne répond pas.

Sans garde-fou, voilà ce qui se passe. Chaque appel attend. Les travailleurs qui traitent la file attendent tous. Les requêtes web qui déclenchent l’appel attendent aussi. Au bout de quelques minutes, votre instance ne répond plus non plus, pour une panne qui n’est pas la vôtre.

C’est l’interdiction n°18 du projet, et elle est courte : tout appel sortant a un délai et un disjoncteur.

Le délai

from kewos.resilience import DEFAULT_HTTP_TIMEOUT_SECONDS
response = httpx.get(url, timeout=DEFAULT_HTTP_TIMEOUT_SECONDS)

Les valeurs du noyau, pour ne pas les réinventer par appel :

Ce qu’on attendDélai
un appel HTTP sortant30 s
une requête à la base10 s
un travail de fond300 s

Un client HTTP sans timeout attend indéfiniment. C’est le défaut de la plupart des bibliothèques, et c’est le premier maillon de la panne décrite plus haut.

Le disjoncteur

Le délai borne un appel. Le disjoncteur arrête d’appeler un service qu’on sait tombé.

from kewos.resilience import CircuitBreaker, CircuitOpenError
# Un par dépendance, construit UNE fois au démarrage du module : il est
# stateful, c'est tout son intérêt.
TARIFS = CircuitBreaker(name="fournisseur.tarifs", failure_threshold=3, timeout_seconds=30)
def tarif(colis):
try:
return TARIFS.call(appel_http, colis)
except CircuitOpenError:
return None # le service est connu tombé : on n'y va même pas

Exécuté, avec un service qui échoue à chaque fois :

appel 1: parti, échoué -> le service ne répond pas (état closed, 1 échec)
appel 2: parti, échoué -> le service ne répond pas (état closed, 2 échecs)
appel 3: parti, échoué -> le service ne répond pas (état open, 3 échecs)
appel 4: refusé AVANT de sortir -> CircuitOpenError: circuit breaker 'fournisseur.tarifs' is open
appel 5: refusé AVANT de sortir -> CircuitOpenError: circuit breaker 'fournisseur.tarifs' is open
appels réellement sortis: 3

Trois appels sont partis, deux ont été refusés sans sortir. C’est la différence entre une dépendance lente et une instance lente : au quatrième appel, votre code répond immédiatement.

Et il se referme tout seul :

après 2 échecs, état: open
31 s plus tard, un appel sonde passe: réponse
état après la sonde réussie: closed

Passé le délai de récupération, le disjoncteur laisse passer une sonde. Si elle réussit, il se referme ; si elle échoue, il se rouvre pour un tour. Personne n’a rien à réarmer à la main.

Le réessai, et quand il ne faut pas

from kewos.resilience import retry_with_backoff
resultat = retry_with_backoff(appel, attempts=3, retry_on=(TimeoutError,))
retry_with_backoff -> réponse en 3 tentatives

Le repli est exponentiel avec une part d’aléatoire, pour ne pas synchroniser tous vos travailleurs sur la même seconde.

Ce que ça n’excuse pas

Un disjoncteur ouvert n’est pas une panne silencieuse. L’utilisateur doit voir que le service est partiel (interdiction n°25), et le module doit décider quoi faire sans le service : un tarif estimé, une file d’attente, un écran qui le dit. Le disjoncteur vous rend la main vite pour que vous ayez ce choix ; il ne le fait pas à votre place.

Et les appels que vous ne passez pas vous-même

Un appel à un modèle d’IA passe par kewos/ai, un envoi de courriel par la file, une lecture de document par kewos/ocr. Ces façades portent déjà leur délai et leur disjoncteur : c’est une bonne raison de plus de passer par elles plutôt que d’appeler un fournisseur directement.