Algunos fallos pasan si esperas
Un error de API es una respuesta cuyo código de estado dice que la llamada no funcionó. Algunos errores pasan si esperas un momento y lo intentas otra vez. Otros no pasan nunca, por muchas veces que lo intentes.
Un buen programa distingue los dos. Reintenta el primer tipo con esperas crecientes, y se detiene y avisa en el segundo.
Al final de esta lección
Nota
Puedo distinguir un fallo que hay que reintentar de uno que hay que corregir.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Código de estado | un número de la respuesta que dice cómo fue la llamada | 429 |
| Límite de frecuencia | un tope de las llamadas que puedes hacer por minuto | |
| Retroceso | esperar más tras cada intento fallido | |
| Azar (jitter) | un cambio aleatorio de una espera, para que los clientes no reintenten a la vez | |
| Tiempo máximo | lo más que esperas una respuesta |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
El código de estado dice qué hacer
Un estado de la serie 200 significa que la llamada funcionó. Un estado de la serie 400 suele significar que tu llamada está mal hecha, así que la misma llamada volverá a fallar.
Otros estados señalan un problema breve y pasajero: un tiempo agotado, un servidor ocupado o un límite de frecuencia. Para esos, esperar y volver a probar es la respuesta correcta.
Ejemplo resuelto: reintentar o corregir
| Estado | Qué significa | Qué hacer |
|---|---|---|
| 400 | La petición está mal | Corregir la petición |
| 401 | No hay clave válida | Corregir la clave |
| 403 | No permitido, o sin crédito | Corregir la causa |
| 408, 504 | Un tiempo agotado | Reintentar |
| 429 | Demasiadas peticiones | Esperar y reintentar |
| 500, 502, 503 | El servidor tiene un problema | Reintentar unas veces |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Clasifica estados y lee un 429 grabado
docker exec -w /home/node/project agent node -e '/* ¿Qué fallos merecen otro intento? */ const kind = (status) => { if (status >= 200 && status < 300) return "ok"; return [408, 429, 500, 502, 503, 504].includes(status) ? "reintentar" : "corregir"; }; const notes = { 200: "funcionó", 400: "la petición está mal", 401: "no hay clave válida", 403: "no permitido", 408: "tiempo agotado", 429: "demasiadas peticiones", 500: "el servidor falló", 503: "el servidor no está listo" }; for (const status of Object.keys(notes)) { console.log(status + " " + kind(Number(status)).padEnd(10) + " " + notes[status]); } /* Un 429 grabado, escrito en este script como dato. No se hace ninguna llamada. */ const recorded = { status: 429, headers: { "retry-after": "2" }, body: { error: { message: "Demasiadas peticiones, más despacio." } } }; console.log("429 grabado: " + recorded.body.error.message + " El servidor pide " + recorded.headers["retry-after"] + " segundos.");'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
El retroceso espera más cada vez
Si reintentas al instante, añades carga a lo que causó el fallo. El retroceso espera un poco, luego el doble, luego el doble otra vez, hasta un tope.
Añade azar (jitter): cambia cada espera en una cantidad aleatoria, entre la mitad y el total. Así cien clientes que fallaron a la vez no reintentan en el mismo instante. Si la respuesta trae una cabecera Retry-After, úsala.
Imprime un calendario de retroceso
docker exec -w /home/node/project agent node -e '/* Esperar más tras cada fallo, hasta un tope, y añadir azar (jitter). */ const base = 1; const cap = 8; const delays = []; for (let attempt = 0; attempt < 6; attempt++) delays.push(Math.min(cap, base * 2 ** attempt)); console.log("Esperas en segundos: " + delays.join(", ")); const jittered = delays.map((one) => one / 2 + Math.random() * one / 2); console.log("Con azar, una ejecución posible: " + jittered.map((one) => one.toFixed(2)).join(", ")); console.log("Cada espera con azar está entre la mitad de la espera y la espera entera.");'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un error frecuente
Advertencia
Mucha gente cree que hay que reintentar todo error. No es así. Un 400 o un 401 fallará igual cada vez. Un bucle que lo reintenta solo pierde tiempo y crédito. Reintenta solo los fallos que pueden pasar, y solo unas pocas veces.
Un tiempo máximo evita esperar sin fin
Algunas llamadas no responden nunca. Fija un tiempo máximo, lo más que esperas, y trata el agotarlo como un fallo que merece un reintento.
La ayuda de abajo junta las piezas. Tiene un tiempo máximo, unos pocos intentos y retroceso con azar. Se detiene al instante ante un fallo que hay que corregir e imprime una muestra grabada cuando nada funciona.
Escribe retry.js, parte 1
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("retry.js", ["// retry.js, parte 1: qué estados reintentar y cuánto esperar.", "const lab = require(\"./lab.js\");", "const kind = (status) => {", " if (status >= 200 && status < 300) return \"ok\";", " return [408, 429, 500, 502, 503, 504].includes(status) ? \"reintentar\" : \"corregir\";", "};", "const pause = (ms) => new Promise((done) => setTimeout(done, ms));", "", "// Retroceso, la cabecera Retry-After si existe, un tope y azar.", "function waitFor(attempt, response) {", " let wait = 150 * 2 ** attempt;", " if (response) wait = Math.max(wait, Number(response.headers.get(\"retry-after\")) * 1000 || 0);", " wait = Math.min(wait, 3000);", " return wait / 2 + Math.random() * wait / 2;", "}"].join("\n") + "\n")'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Añade el bucle de reintentos
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("retry.js", ["", "// Parte 2: reintentar, salvo ante un fallo que hay que corregir.", "async function callWithRetry(body, tries) {", " for (let attempt = 1; attempt <= tries; attempt++) {", " let response = null;", " try {", " response = await lab.send(body, 15000);", " if (response.ok) return { ok: true, data: await response.json() };", " console.log(\"intento \" + attempt + \": HTTP \" + response.status);", " if (kind(response.status) === \"corregir\") return { ok: false, why: \"corrige la causa\" };", " } catch (error) {", " console.log(\"intento \" + attempt + \": sin respuesta (\" + error.name + \")\");", " }", " if (attempt < tries) await pause(waitFor(attempt, response));", " }", " return { ok: false, why: \"abandono tras \" + tries + \" intentos\" };", "}", "", "module.exports = { callWithRetry };"].join("\n") + "\n")'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Llama al modelo con reintentos
docker exec -w /home/node/project agent node -e 'const { callWithRetry } = require("./retry.js"); (async () => { const body = { max_tokens: 20, messages: [{ role: "user", content: "Saluda con una palabra." }] }; const result = await callWithRetry(body, 3); if (result.ok) console.log("Respuesta del modelo: " + result.data.choices[0].message.content); else console.log("Muestra grabada: Hola. (" + result.why + ")"); })();'Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Para ir más lejos: cambia los intentos
En retry.js, cambia la primera espera de 150 a 50. En el último Run, cambia el número de intentos de 3 a 5. Ejecútalo y lee lo que imprime.
Di qué elegirías para un botón ante el que espera una persona, y qué para un trabajo que corre por la noche.