Por qué una llamada lenta es una tarea
Una cola de tareas deja que una petición responda enseguida mientras el trabajo lento termina en otro sitio. Una llamada a un modelo puede tardar varios segundos, y una petición web que espera tanto caduca o bloquea un proceso. Importa porque reintentar una llamada lenta es el momento en que se cobra dos veces a un cliente.
Un usuario sube cuarenta páginas para resumir. El servidor responde «recibido, tarea-17» en unos milisegundos. La página pregunta el estado hasta que el resumen está listo.
Al final de esta lección
Nota
Puedo sacar una llamada lenta de la petición e informar de su progreso.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Tarea | una unidad de trabajo lento con un estado | |
| Cola | las tareas que esperan a un trabajador | |
| Trabajador | el código que toma una tarea y la hace | |
| Clave de idempotencia | una etiqueta del cliente que hace inofensiva una repetición | pedido-1 |
| Espera creciente | esperar más tras cada fallo |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una tarea tiene cuatro estados
Una tarea está en cola, luego en curso y después hecha o fallida. Un intento fallido que se repetirá vuelve a en cola. La tarea conserva su número de intentos y su resultado.
El cliente recibe enseguida un id de tarea. Pregunta el estado con ese id. Lee el resultado solo cuando el estado es hecho.
Ejemplo resuelto: reloj propio de tarea-2
| Paso | Estado | Intentos | Reloj |
|---|---|---|---|
| Enviada | en cola | 0 | 0 s |
| Empieza el primer intento | en curso | 1 | 2 s |
| Tiempo agotado aguas arriba | en cola | 1 | 7 s |
| Empieza el segundo intento | en curso | 2 | 9 s |
| Resultado guardado | hecha | 2 | 9 s |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una clave de idempotencia evita ejecutar dos veces
Las redes repiten peticiones. Un cliente puede pulsar enviar dos veces, o un proxy puede reenviar. Sin una protección, cada copia se convierte en una tarea, y el trabajo, igual que la factura, se duplica.
El cliente envía una clave de idempotencia. La cola guarda un mapa de clave a id de tarea. Una clave ya vista devuelve la tarea existente y no crea otra.
Los reintentos tienen tope y espera
Fija un número máximo de intentos. Después la tarea queda fallida, y una persona o una regla decide qué hacer. Espera más tras cada fallo para dar margen a un servicio con problemas.
Una tarea hecha nunca se vuelve a ejecutar, aunque una pasada la visite dos veces. Una tarea hecha es un resultado, no una petición.
Un error frecuente
Advertencia
Mucha gente cree que un reintento siempre es seguro. Solo lo es si el trabajo es idempotente o la cola lo recuerda. Reintenta una llamada ya pagada y terminada, y pagas otra vez. Comprueba el estado antes de cada ejecución.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"estado \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la cola, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const jobs = {}; // id de tarea a tarea' 'const byKey = {}; // clave de idempotencia a id de tarea' 'let clock = 0; // un reloj falso, en segundos' 'let next = 1;' 'function submit(key, payload) {' ' if (key in byKey) return byKey[key];' ' const id = \"tarea-\" + next++;' ' jobs[id] = { id: id, payload: payload, state: \"en-cola\", tries: 0, result: null };' ' byKey[key] = id;' ' return id;' '}' > queue.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la cola, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const failFirst = { \"tarea-2\": 1 }; // tarea-2 falla una vez y luego funciona' 'function work(job) {' ' if (job.tries <= (failFirst[job.id] || 0)) throw new Error(\"tiempo agotado aguas arriba\");' ' return job.payload.toUpperCase();' '}' 'function runOne(job) {' ' if (job.state === \"hecho\" || job.state === \"fallido\") return;' ' while (job.tries < 3) {' ' job.tries++;' ' job.state = \"en-curso\";' ' clock += 2;' ' try { job.result = work(job); job.state = \"hecho\"; return; }' ' catch (error) { job.state = \"en-cola\"; clock += job.tries * 5; }' ' }' ' job.state = \"fallido\";' '}' >> queue.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la cola, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'submit(\"pedido-1\", \"factura a\");' 'submit(\"pedido-2\", \"factura b\");' 'submit(\"pedido-1\", \"factura a\"); // el cliente reintenta: misma clave, misma tarea' 'Object.values(jobs).forEach(runOne);' 'Object.values(jobs).forEach(runOne); // una segunda pasada no ejecuta nada' 'for (const id in jobs) console.log(id, jobs[id].state, \"intentos\", jobs[id].tries, jobs[id].result);' 'console.log(\"tareas:\", Object.keys(jobs).length, \"reloj:\", clock + \" s\");' >> queue.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la cola, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const sample = \"Tus dos facturas están listas.\";' ' const text = \"Dos tareas han terminado. Escribe una frase corta de estado para el cliente.\";' ' console.log(\"Llamada real:\", await ask([{ role: \"user\", content: text }], sample, 40));' '}' 'main();' >> queue.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecuta la cola
docker exec -w /home/node/project agent node queue.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un sustituto, no una cola real
La cola de aquí es un objeto en memoria y el reloj es un número. Si el proceso se detiene, se pierden todas las tareas. Un sistema real guarda las tareas en un servicio hecho para eso, y los trabajadores corren en otros procesos.
Las ideas siguen valiendo: estados, un número de intentos, un mapa de claves y un tope de reintentos.
Una salida posible
| Línea | Qué muestra |
|---|---|
| tarea-1 hecho intentos 1 FACTURA A | Una tarea que salió bien a la primera |
| tarea-2 hecho intentos 2 FACTURA B | Una tarea que falló una vez y luego funcionó |
| tareas: 2 reloj: 11 s | Tres envíos crearon dos tareas |
| Llamada real | Una frase corta de estado, o una Muestra grabada con su etiqueta |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.