Certains échecs passent si on attend
Une erreur d’API est une réponse dont le code d’état dit que l’appel n’a pas marché. Certaines erreurs passent si on attend un peu et qu’on réessaie. D’autres ne passent jamais, quel que soit le nombre d’essais.
Un bon programme sait distinguer les deux. Il réessaie le premier cas avec des attentes croissantes, et il s’arrête et signale le second.
À la fin de cette leçon
Remarque
Je peux distinguer un échec à réessayer d’un échec à corriger.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Code d’état | un nombre dans une réponse qui dit comment l’appel s’est passé | 429 |
| Limite de débit | un plafond du nombre d’appels permis par minute | |
| Recul | attendre plus longtemps après chaque essai raté | |
| Aléa | un changement au hasard d’une attente, pour que les clients ne réessaient pas ensemble | |
| Délai d’attente | le temps maximal pendant lequel on attend une réponse |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le code d’état dit quoi faire ensuite
Un statut en 200 veut dire que l’appel a marché. Un statut en 400 veut le plus souvent dire que votre appel est mal fait, donc le même appel échouera encore.
D’autres statuts signalent un problème bref et passager : un délai dépassé, un serveur occupé ou une limite de débit. Pour ceux-là, attendre puis réessayer est la bonne réaction.
Exemple travaillé : réessayer ou corriger
| Statut | Ce que cela veut dire | Que faire |
|---|---|---|
| 400 | La requête est fausse | Corriger la requête |
| 401 | Pas de clé valide | Corriger la clé |
| 403 | Pas autorisé, ou plus de crédit | Corriger la cause |
| 408, 504 | Un délai dépassé | Réessayer |
| 429 | Trop de requêtes | Attendre, puis réessayer |
| 500, 502, 503 | Le serveur a un problème | Réessayer quelques fois |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Trier des statuts et lire un 429 enregistré
docker exec -w /home/node/project agent node -e '/* Quels échecs méritent un nouvel essai ? */ const genre = (statut) => { if (statut >= 200 && statut < 300) return "ok"; return [408, 429, 500, 502, 503, 504].includes(statut) ? "réessayer" : "corriger"; }; const notes = { 200: "cela a marché", 400: "la requête est fausse", 401: "pas de clé valide", 403: "pas autorisé", 408: "délai dépassé", 429: "trop de requêtes", 500: "le serveur est en panne", 503: "le serveur n’est pas prêt" }; for (const statut of Object.keys(notes)) { console.log(statut + " " + genre(Number(statut)).padEnd(9) + " " + notes[statut]); } /* Un 429 enregistré, écrit dans ce script comme donnée. Aucun appel n’est fait. */ const enregistre = { statut: 429, entetes: { "retry-after": "2" }, corps: { error: { message: "Trop de requêtes, ralentissez." } } }; console.log("429 enregistré : " + enregistre.corps.error.message + " Le serveur demande " + enregistre.entetes["retry-after"] + " secondes.");'Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Le recul attend plus longtemps à chaque fois
Si vous réessayez aussitôt, vous ajoutez à la charge qui a causé l’échec. Le recul attend un peu, puis deux fois plus, puis deux fois plus encore, jusqu’à un plafond.
Ajoutez de l’aléa : changez chaque attente d’une valeur au hasard, entre la moitié et le tout. Ainsi cent clients qui ont échoué ensemble ne réessaient pas au même instant. Si la réponse a un en-tête Retry-After, servez-vous-en.
Afficher un calendrier de recul
docker exec -w /home/node/project agent node -e '/* Attendre plus longtemps après chaque échec, jusqu’à un plafond, avec de l’aléa. */ const base = 1; const plafond = 8; const delais = []; for (let tentative = 0; tentative < 6; tentative++) delais.push(Math.min(plafond, base * 2 ** tentative)); console.log("Délais en secondes : " + delais.join(", ")); const avecAleas = delais.map((un) => un / 2 + Math.random() * un / 2); console.log("Avec aléa, une exécution possible : " + avecAleas.map((un) => un.toFixed(2)).join(", ")); console.log("Chaque délai avec aléa est compris entre la moitié du délai et le délai entier.");'Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Une erreur fréquente
Attention
Beaucoup pensent qu’il faut réessayer toute erreur. Ce n’est pas le cas. Un 400 ou un 401 échouera de la même façon à chaque fois. Une boucle qui le réessaie ne fait que perdre du temps et du crédit. Réessayez seulement les échecs qui peuvent passer, et quelques fois seulement.
Un délai d’attente évite d’attendre sans fin
Certains appels ne répondent jamais. Fixez un délai d’attente, le temps maximal que vous attendez, et traitez le dépassement comme un échec qui mérite un nouvel essai.
L’aide ci-dessous réunit les pièces. Elle a un délai d’attente, quelques essais et un recul avec aléa. Elle s’arrête aussitôt pour un échec à corriger, et elle affiche un exemple enregistré quand rien ne marche.
Écrire reessai.js, partie 1
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("reessai.js", ["// reessai.js, partie 1 : quels statuts réessayer, et combien de temps attendre.", "const labo = require(\"./labo.js\");", "const genre = (statut) => {", " if (statut >= 200 && statut < 300) return \"ok\";", " return [408, 429, 500, 502, 503, 504].includes(statut) ? \"réessayer\" : \"corriger\";", "};", "const pause = (ms) => new Promise((fini) => setTimeout(fini, ms));", "", "// Recul, en-tête Retry-After s’il existe, plafond, et aléa.", "function attenteAvant(tentative, reponse) {", " let attente = 150 * 2 ** tentative;", " if (reponse) attente = Math.max(attente, Number(reponse.headers.get(\"retry-after\")) * 1000 || 0);", " attente = Math.min(attente, 3000);", " return attente / 2 + Math.random() * attente / 2;", "}"].join("\n") + "\n")'Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ajouter la boucle de nouveaux essais
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("reessai.js", ["", "// Partie 2 : réessayer, sauf pour un échec à corriger.", "async function appelerAvecReessai(corps, essais) {", " for (let i = 1; i <= essais; i++) {", " let reponse = null;", " try {", " reponse = await labo.envoyer(corps, 15000);", " if (reponse.ok) return { ok: true, donnees: await reponse.json() };", " console.log(\"essai \" + i + \" : HTTP \" + reponse.status);", " if (genre(reponse.status) === \"corriger\") return { ok: false, raison: \"à corriger, pas à réessayer\" };", " } catch (erreur) {", " console.log(\"essai \" + i + \" : pas de réponse (\" + erreur.name + \")\");", " }", " if (i < essais) await pause(attenteAvant(i, reponse));", " }", " return { ok: false, raison: \"abandon après \" + essais + \" essais\" };", "}", "", "module.exports = { appelerAvecReessai };"].join("\n") + "\n")'Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Appeler le modèle avec des nouveaux essais
docker exec -w /home/node/project agent node -e 'const { appelerAvecReessai } = require("./reessai.js"); (async () => { const corps = { max_tokens: 20, messages: [{ role: "user", content: "Dites bonjour en un mot." }] }; const resultat = await appelerAvecReessai(corps, 3); if (resultat.ok) console.log("Réponse du modèle : " + resultat.donnees.choices[0].message.content); else console.log("Exemple enregistré : Bonjour. (" + resultat.raison + ")"); })();'Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Pour aller plus loin : changer les essais
Dans reessai.js, remplacez la première attente de 150 par 50. Dans le dernier Run, passez le nombre d’essais de 3 à 5. Lancez-le et lisez ce qu’il affiche.
Dites ce que vous choisiriez pour un bouton devant lequel une personne attend, et ce que vous choisiriez pour une tâche qui tourne la nuit.