Skip to content

retry-client: retentatives automatiques dans le client HTTP #6

Description

@arsenik-dtheo

title: "retry-client: retentatives automatiques dans le client HTTP"
labels: ["Epic"]

🎯 Problème

Chaque échec transitoire (429, 529, 5xx, timeout réseau) remonte immédiatement à l'appelant de Typesafe::Client#evaluate : chaque utilisateur du gem doit réécrire à la main la boucle « attendre puis rejouer », et la plupart ne le font pas. Leurs intégrations restent fragiles aux salves de surcharge, faute de résilience zéro-config.

💡 Solution

Le client retente automatiquement les erreurs retentables (429, 529, 5xx) et les erreurs de connexion (Typesafe::ConnectionError), par défaut 2 retries. Le délai suit Retry-After quand le serveur l'impose, sinon un backoff exponentiel 0,5 s → 8 s avec jitter. Toute la politique se règle via un unique Hash retry_options: sur Client.new ; retry_options: { max_retries: 0 } retrouve le comportement actuel.

🧭 Décisions arrêtées

  1. Retry actif par défaut : 2 retries (3 tentatives), opt-out via retry_options: { max_retries: 0 } (ADR-0001, qui écarte l'ancien contrat « retry 429/529 manuel »).
  2. Périmètre : toutes les erreurs retentables (APIError#retryable? : 429, 529, 5xx) plus les erreurs de connexion.
  3. Nouvelle Typesafe::ConnectionError < APIError encapsulant les échecs réseau (connexion refusée, DNS, timeout, reset), marquée retentable.
  4. Configuration : un unique argument retry_options: (Hash) sur Client.new, hérité par Typesafe::Jev ; pas d'override par appel.
  5. Clés de retry_options: : max_retries / base_delay / max_delay, défauts 2 / 0,5 / 8,0 ; nil = défauts ; Hash normalisé figé sur le client.
  6. Validation stricte : ArgumentError sur clé inconnue ou valeur invalide (numériques non négatifs, max_retries entier), à la construction.
  7. Délai : Retry-After / Retry-After-Ms honoré (429), sinon backoff exponentiel avec doublement, plafond 8 s et jitter.
  8. Pas de budget temps global : le pire cas reste borné par max_retries et le plafond de délai.
  9. Rejeu du POST à l'identique : sûr, l'évaluation est sans état (quota consommé : coût assumé).
  10. Retentatives totalement silencieuses en v1 : pas de log, pas de callback.

🧪 Seam de test

  • Seam retenu : l'API publique Typesafe::Client#evaluate / Typesafe::Jev#evaluate, avec WebMock sur l'endpoint et sleep stubbé — le seam existant de spec/typesafe/client_spec.rb, le plus haut et déjà en place.

🚫 Hors périmètre

  • Observabilité (callback, logger, métriques) — rejeté pour v1, réintroduisable sans casser l'API.
  • Override de la politique par appel dans evaluate — la signature ne change pas.
  • Budget temps global — max_retries + plafond bornent déjà le pire cas.
  • Stratégie de retry pluggable — supplantée par le Hash retry_options: à clés fixes.
  • Retentative des erreurs non retentables (400/401/403/404/422) — jamais, par construction.

📚 Références

  • Spec : docs/plans/retry-client/spec.md
  • Affinage : docs/plans/retry-client/affinage.md
  • ADR : docs/adr/0001-retentatives-automatiques-par-defaut.md
  • Glossaire : LEXIQUE.md
📎 Affinage — session complète

Affinage — Retry des requêtes HTTP dans le client

  • Date : 2026-09-20 · Sujet : ajouter une fonctionnalité de retry au client HTTP du gem · Mode : produit
  • Sources : .agents/dev-workflow.md, AGENTS.md (contrat API), lib/typesafe/client.rb, lib/typesafe/errors.rb, lib/typesafe/jev.rb

Round 1

Posé : Q1 — Périmètre des erreurs retentées · Q2 — Erreurs réseau · Q3 — Surface de configuration · Q4 — Stratégie de backoff · Q5 — Nombre de retries par défaut

  • Décision (user) Q1 : 429 + 529 + 5xx (tout ce que retryable? considère) — AGENTS.md sera mis à jour en conséquence (le contrat « 429/529 seulement » est supplanté).
  • Décision (user) Q2 : oui — encapsuler dans une Typesafe::ConnectionError < APIError retentable.
  • Décision (user) Q3 : options sur Client.new uniquement (héritées par Jev), pas d'override par appel.
  • Décision (user) Q4 : honorer Retry-After si présent, sinon backoff exponentiel 0,5 s → 8 s (doublement, plafond) + jitter.
  • Décision (user) Q5 : 2 retries par défaut (3 tentatives au total), opt-out via configuration.

Réglé jusqu'ici

Q Décision
Q1 429 + 529 + 5xx
Q2 ConnectionError, retentable
Q3 Options sur Client.new uniquement
Q4 Retry-After sinon expo 0,5 s→8 s + jitter
Q5 2 retries par défaut

Round 2

Posé : Q6 — Réglage des délais · Q7 — Observabilité · Q8 — Budget temps global · Q9 — ADR

  • Décision (user) Q6 : supplantée par l'amendement du user (voir ci-dessous) — les options de délai vivent dans retry_options:.
  • Décision (user) Q7 : totalement silencieux en v1 — pas de callback ni de log.
  • Décision (user) Q8 : pas de budget temps global — max_retries + plafond de délai bornent déjà le pire cas.
  • Décision (user) Q9 : ADR-0001 écrite (docs/adr/0001-retentatives-automatiques-par-defaut.md, accepté) + AGENTS.md mis à jour.

Amendement (user) — surface de configuration

  • Décision (user) : remplacer les kwargs plats (max_retries:, …) par un unique argument retry_options: (Hash) regroupant toutes les options de retry, sur Client.new, hérité par Jev. Q3 est amendée en conséquence ; les détails (clés exactes, validation des clés inconnues) sont reposés en Round 3.

Réglé jusqu'ici

Q Décision
Q1 429 + 529 + 5xx
Q2 ConnectionError, retentable
Q3 kwargs plats → retry_options: (Hash unique) sur Client.new (amendé)
Q4 Retry-After sinon expo 0,5 s→8 s + jitter
Q5 2 retries par défaut
Q7 Silencieux, pas de hook v1
Q8 Pas de budget global
Q9 ADR-0001 + AGENTS.md mis à jour

Round 3

Posé : Q10 — Clés de retry_options: · Q11 — Clés inconnues

  • Décision (user) Q10 : { max_retries:, base_delay:, max_delay: } — clés absentes/nil → valeurs par défaut (2 / 0,5 / 8,0) ; retry_options: nil = tous les défauts ; valeurs validées (numériques positifs, Integer pour max_retries) et hash normalisé figé sur le client.
  • Décision (user) Q11 : ArgumentError sur toute clé inconnue (typo = échec bruyant à la construction du client).

Réglé jusqu'ici

Q Décision
Q10 max_retries / base_delay / max_delay, défauts 2 / 0,5 / 8,0
Q11 ArgumentError sur clé inconnue
  • Session confirmée le 2026-09-20
  • Passage advisor proposé (sujets : retry actif par défaut, silence des retries en v1) — refusé par le user, les décisions tiennent telles quelles.

Délibérément non posé

  • Formule de jitter exacte (full vs equal jitter, amplitude) — détail d'implémentation, politique figée en v1.
  • Liste précise des exceptions réseau enveloppées par ConnectionError — détail d'implémentation pour /cadrage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    EpicÉpic : regroupe les sous-tickets d'un ensemble de work

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions