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 UPDATEau lieu deINSERTseul - 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_timeoutpour éviter les activités bloquantes - Le backoff exponentiel (
retry_policy_backoff_coefficient) augmente le délai entre les tentatives retry_policy_max_attemptsdé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)