Aller au contenu principal

Politique de retry

Pourquoi les retries sont essentiels

Dans un système distribué, les erreurs transitoires sont inévitables : timeouts réseau, limites de débit dépassées, services temporairement indisponibles. La politique de retry permet à vos activités de survivre à ces erreurs sans intervention manuelle.

La plateforme Workflows gère automatiquement les retries selon la configuration que vous définissez pour chaque activité.

Configurer une politique de retry

Le décorateur @workflows.activity() accepte plusieurs paramètres de configuration :

from datetime import timedelta
import mistralai.workflows as workflows

@workflows.activity(
    start_to_close_timeout=timedelta(minutes=5),
    retry_policy_max_attempts=3,
    retry_policy_backoff_coefficient=2.0,
)
async def appeler_api_externe(url: str, donnees: dict) -> dict:
    """Appelle une API externe avec retry automatique."""
    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.post(url, json=donnees)
        response.raise_for_status()
        return response.json()

Paramètres de retry

  • start_to_close_timeout — Durée maximale d’une seule tentative d’exécution. Si l’activité ne termine pas dans ce délai, elle est considérée comme échouée et une nouvelle tentative est lancée.

  • retry_policy_max_attempts — Nombre maximum de tentatives. La valeur par défaut dépend de la plateforme. Après ce nombre de tentatives, l’activité est définitivement en échec et le workflow reçoit une erreur.

  • retry_policy_backoff_coefficient — Multiplicateur du délai entre les tentatives. Avec un coefficient de 2.0, les délais doublent à chaque tentative (1s, 2s, 4s, 8s…). C’est le backoff exponentiel.

Backoff exponentiel : comment ça marche

Le backoff exponentiel augmente progressivement le délai entre les tentatives pour éviter de surcharger un service déjà en difficulté :

Tentative 1 : exécution immédiate
  → Échec (timeout réseau)
  → Attente 1 seconde

Tentative 2 : exécution
  → Échec (service indisponible)
  → Attente 2 secondes (1 × 2.0)

Tentative 3 : exécution
  → Échec (limite de débit)
  → Attente 4 secondes (2 × 2.0)

Tentative 4 : exécution
  → Succès !

Ce mécanisme est particulièrement efficace pour les erreurs de type “rate limiting” (429 Too Many Requests), car il donne au service le temps de récupérer.

Le paramètre start_to_close_timeout

Ce timeout est critique et devrait être configuré sur chaque activité. Il définit combien de temps une seule tentative peut durer avant d’être annulée.

Pourquoi le configurer systématiquement

Sans start_to_close_timeout, une activité qui se bloque (deadlock, connexion pendante, service qui ne répond jamais) peut rester en attente indéfiniment, bloquant tout le workflow.

# MAUVAISE PRATIQUE : pas de timeout
@workflows.activity()
async def activite_dangereuse(params: dict) -> dict:
    # Si le service ne répond jamais, l'activité attend indéfiniment
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.lente.example.com/data")
    return response.json()

# BONNE PRATIQUE : timeout explicite
@workflows.activity(
    start_to_close_timeout=timedelta(minutes=2)
)
async def activite_sure(params: dict) -> dict:
    async with httpx.AsyncClient(timeout=90) as client:
        response = await client.get("https://api.lente.example.com/data")
    return response.json()

Comment choisir le timeout

  • Mesurez le temps d’exécution normal de votre activité
  • Ajoutez une marge de 2x à 3x pour les cas lents
  • Tenez compte du timeout du client HTTP interne (il doit être inférieur au start_to_close_timeout)

Idempotence : la condition obligatoire

Les retries impliquent qu’une activité peut être exécutée plusieurs fois avec les mêmes paramètres. Pour éviter les effets de bord indésirables, chaque activité doit être idempotente.

Exemple non idempotent (dangereux)

@workflows.activity(retry_policy_max_attempts=3)
async def ajouter_credit(user_id: str, montant: float) -> dict:
    """DANGEREUX : si retried, le crédit est ajouté plusieurs fois !"""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"https://api.billing.com/credits/add",
            json={"user_id": user_id, "montant": montant}
        )
    return response.json()

Exemple idempotent (correct)

@workflows.activity(retry_policy_max_attempts=3)
async def ajouter_credit(user_id: str, montant: float, idempotency_key: str) -> dict:
    """CORRECT : la clé d'idempotence empêche la duplication."""
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"https://api.billing.com/credits/add",
            json={
                "user_id": user_id,
                "montant": montant,
                "idempotency_key": idempotency_key
            }
        )
    return response.json()

Stratégies d’idempotence

  • Clé d’idempotence — Passez un identifiant unique (généré par workflow.uuid4() dans le workflow) que le service externe utilise pour détecter les doublons
  • Upsert — Utilisez INSERT ... ON CONFLICT UPDATE au lieu de INSERT seul
  • Vérification préalable — Vérifiez si l’action a déjà été effectuée avant de la refaire

Exemples de configurations typiques

Appel API rapide (paiement, notification)

@workflows.activity(
    start_to_close_timeout=timedelta(seconds=30),
    retry_policy_max_attempts=3,
    retry_policy_backoff_coefficient=2.0,
)
async def traiter_paiement(params: dict) -> dict:
    # ...
    pass

Traitement long (génération de rapport, ML)

@workflows.activity(
    start_to_close_timeout=timedelta(minutes=15),
    retry_policy_max_attempts=2,
    retry_policy_backoff_coefficient=3.0,
)
async def generer_rapport_ml(params: dict) -> dict:
    # ...
    pass

Service instable (API tierce avec rate limiting)

@workflows.activity(
    start_to_close_timeout=timedelta(minutes=2),
    retry_policy_max_attempts=5,
    retry_policy_backoff_coefficient=2.0,
)
async def appeler_api_tierce(params: dict) -> dict:
    # ...
    pass

Points clés à retenir

  • Configurez toujours start_to_close_timeout pour éviter les activités bloquantes
  • Le backoff exponentiel (retry_policy_backoff_coefficient) augmente le délai entre les tentatives
  • retry_policy_max_attempts définit le nombre maximum de tentatives avant échec définitif
  • Les activités avec retry doivent être idempotentes — même résultat si exécutées plusieurs fois
  • Utilisez des clés d’idempotence ou des upserts pour garantir l’idempotence
  • Adaptez les timeouts et le nombre de retries au type d’opération (rapide vs. longue, stable vs. instable)