Aller au contenu principal

Retry, backoff exponentiel et circuit breaker

Retry, backoff exponentiel et circuit breaker

Les erreurs transitoires sont normales en production. Le SDK OpenAI gère automatiquement 2 retries, mais pour une résilience maximale, vous devez implémenter vos propres stratégies.

Le backoff exponentiel

Le principe est simple : chaque retry attend plus longtemps que le précédent. Cela évite de surcharger l’API quand elle est sous pression.

import time
import random
from openai import OpenAI, RateLimitError, InternalServerError, APIConnectionError

client = OpenAI()

def appel_avec_backoff(prompt: str, max_retries: int = 5) -> str:
    """Appel API avec backoff exponentiel et jitter."""

    for tentative in range(max_retries):
        try:
            response = client.responses.create(
                model="gpt-5.3",
                input=prompt
            )
            return response.output_text

        except (RateLimitError, InternalServerError, APIConnectionError) as e:
            if tentative == max_retries - 1:
                raise  # Dernière tentative, on propage l'erreur

            # Backoff exponentiel : 1s, 2s, 4s, 8s, 16s
            base_delay = 2 ** tentative

            # Jitter aléatoire pour éviter le thundering herd
            jitter = random.uniform(0, base_delay * 0.5)
            delay = base_delay + jitter

            print(f"Tentative {tentative + 1}/{max_retries} échouée. "
                  f"Retry dans {delay:.1f}s...")
            time.sleep(delay)

    raise RuntimeError("Ne devrait jamais arriver")

# Utilisation
resultat = appel_avec_backoff("Expliquez le backoff exponentiel.")
print(resultat)

Utiliser la bibliothèque tenacity

Pour une gestion plus avancée des retries, tenacity est l’outil de référence :

# pip install tenacity
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep_log,
)
from openai import RateLimitError, InternalServerError, APIConnectionError
import logging

logger = logging.getLogger(__name__)

@retry(
    retry=retry_if_exception_type(
        (RateLimitError, InternalServerError, APIConnectionError)
    ),
    wait=wait_exponential(multiplier=1, min=1, max=60),
    stop=stop_after_attempt(5),
    before_sleep=before_sleep_log(logger, logging.WARNING),
)
def appel_resilient(prompt: str, model: str = "gpt-5.3") -> str:
    """Appel API avec retry automatique via tenacity."""
    response = client.responses.create(
        model=model,
        input=prompt
    )
    return response.output_text

# Utilisation — les retries sont transparents
resultat = appel_resilient("Bonjour !")
print(resultat)

Le pattern circuit breaker

Le circuit breaker empêche votre application de continuer à appeler une API défaillante. Il a trois états :

  • Fermé : tout fonctionne normalement
  • Ouvert : l’API est en panne, les appels sont bloqués immédiatement
  • Semi-ouvert : un appel test est envoyé pour vérifier si l’API est revenue
import time
from enum import Enum

class EtatCircuit(Enum):
    FERME = "ferme"
    OUVERT = "ouvert"
    SEMI_OUVERT = "semi_ouvert"

class CircuitBreaker:
    """Circuit breaker pour les appels API."""

    def __init__(self, seuil_erreurs: int = 5, timeout_reset: float = 30.0):
        self.seuil = seuil_erreurs
        self.timeout_reset = timeout_reset
        self.erreurs_consecutives = 0
        self.etat = EtatCircuit.FERME
        self.derniere_erreur = 0.0

    def peut_appeler(self) -> bool:
        """Vérifie si un appel est autorisé."""
        if self.etat == EtatCircuit.FERME:
            return True

        if self.etat == EtatCircuit.OUVERT:
            if time.monotonic() - self.derniere_erreur > self.timeout_reset:
                self.etat = EtatCircuit.SEMI_OUVERT
                return True  # Un appel test
            return False

        # Semi-ouvert : un seul appel autorisé
        return True

    def enregistrer_succes(self):
        """Enregistre un appel réussi."""
        self.erreurs_consecutives = 0
        self.etat = EtatCircuit.FERME

    def enregistrer_erreur(self):
        """Enregistre un échec."""
        self.erreurs_consecutives += 1
        self.derniere_erreur = time.monotonic()

        if self.erreurs_consecutives >= self.seuil:
            self.etat = EtatCircuit.OUVERT
            print(f"Circuit OUVERT apres {self.seuil} erreurs consecutives")

# Intégration avec le client OpenAI
circuit = CircuitBreaker(seuil_erreurs=3, timeout_reset=30.0)

def appel_avec_circuit_breaker(prompt: str) -> str:
    """Appel API protégé par un circuit breaker."""
    if not circuit.peut_appeler():
        raise RuntimeError(
            f"Circuit ouvert — API indisponible. "
            f"Retry dans {circuit.timeout_reset}s"
        )

    try:
        response = client.responses.create(
            model="gpt-5.3",
            input=prompt
        )
        circuit.enregistrer_succes()
        return response.output_text

    except (RateLimitError, InternalServerError, APIConnectionError) as e:
        circuit.enregistrer_erreur()
        raise

# Utilisation
try:
    resultat = appel_avec_circuit_breaker("Bonjour !")
    print(resultat)
except RuntimeError as e:
    print(f"Erreur : {e}")

Combiner les trois patterns

En production, combinez backoff, circuit breaker et timeout :

class APIClient:
    """Client API résilient combinant tous les patterns."""

    def __init__(self):
        self.client = OpenAI(timeout=30.0, max_retries=0)
        self.circuit = CircuitBreaker(seuil_erreurs=5, timeout_reset=60.0)

    def appeler(self, prompt: str, max_retries: int = 3) -> str:
        if not self.circuit.peut_appeler():
            raise RuntimeError("Service temporairement indisponible")

        for tentative in range(max_retries):
            try:
                response = self.client.responses.create(
                    model="gpt-5.3",
                    input=prompt
                )
                self.circuit.enregistrer_succes()
                return response.output_text

            except (RateLimitError, InternalServerError) as e:
                self.circuit.enregistrer_erreur()

                if tentative < max_retries - 1:
                    delay = (2 ** tentative) + random.uniform(0, 1)
                    time.sleep(delay)
                else:
                    raise

        raise RuntimeError("Échec apres toutes les tentatives")

# Utilisation
api = APIClient()
print(api.appeler("Expliquez la résilience en ingénierie logicielle."))

Points clés à retenir

  • Le SDK gère 2 retries par défaut — ajustez avec max_retries
  • Le backoff exponentiel avec jitter évite le thundering herd
  • Le circuit breaker protège votre application contre les pannes prolongées
  • Utilisez tenacity pour une gestion déclarative des retries en production
  • Combinez les trois patterns pour une résilience maximale
  • Ne réessayez jamais les erreurs 400/401/403 (erreurs client)