Pourquoi un appel lent devient une tâche
Une file de tâches laisse une requête répondre tout de suite pendant que le travail lent se termine ailleurs. Un appel à un modèle peut durer plusieurs secondes, et une requête web qui attend autant échoue ou bloque un processus. C’est important, car réessayer un appel lent est le moment où un client est facturé deux fois.
Un utilisateur envoie quarante pages à résumer. Le serveur répond « reçu, tache-17 » en quelques millisecondes. La page demande l’état jusqu’à ce que le résumé soit prêt.
À la fin de cette leçon
Remarque
Je peux sortir un appel lent de la requête et rendre compte de son avancement.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Tâche | une unité de travail lent, avec un état | |
| File | les tâches qui attendent un exécutant | |
| Exécutant | le code qui prend une tâche et la fait | |
| Clé d’idempotence | une étiquette du client qui rend une répétition sans effet | commande-1 |
| Attente croissante | attendre plus longtemps après chaque échec |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une tâche a quatre états
Une tâche est en attente, puis en cours, puis terminée ou en échec. Un essai raté qui sera repris retourne à en attente. La tâche garde son nombre d’essais et son résultat.
Le client reçoit tout de suite un identifiant de tâche. Il demande l’état avec cet identifiant. Il lit le résultat seulement quand l’état est terminé.
Exemple travaillé : tache-2 seule, horloge à 0
| Étape | État | Essais | Horloge |
|---|---|---|---|
| Soumise | en attente | 0 | 0 s |
| Premier essai commence | en cours | 1 | 2 s |
| Délai dépassé en amont | en attente | 1 | 7 s |
| Second essai commence | en cours | 2 | 9 s |
| Résultat rangé | terminée | 2 | 9 s |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une clé d’idempotence évite de lancer deux fois
Les réseaux répètent les requêtes. Un client peut appuyer deux fois sur envoyer, ou un proxy peut renvoyer. Sans garde-fou, chaque copie devient une tâche, et le travail, comme la facture, est doublé.
Le client envoie une clé d’idempotence. La file garde une table qui relie la clé à l’identifiant de tâche. Une clé déjà vue renvoie la tâche existante et n’en crée pas de nouvelle.
Les reprises sont bornées et espacées
Fixez un nombre maximal d’essais. Ensuite la tâche est en échec, et une personne ou une règle décide de la suite. Attendez plus longtemps après chaque échec pour laisser souffler un service en difficulté.
Une tâche terminée n’est jamais relancée, même si un passage la visite deux fois. Une tâche terminée est un résultat, pas une demande.
Une erreur fréquente
Attention
Beaucoup pensent qu’une reprise est toujours sûre. Elle ne l’est que si le travail est idempotent ou si la file s’en souvient. Réessayez un appel déjà payé et terminé, et vous payez encore. Vérifiez l’état avant chaque lancement.
Écrire l’aide du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function demander(messages, echantillon, 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(\"statut \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"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.
Écrire l’aide du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' const liste = [].concat(echantillon);' ' const tour = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return liste[Math.min(tour, liste.length - 1)];' ' }' '}' 'module.exports = { demander };' >> model.js"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.
Écrire la file, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const taches = {}; // identifiant de tâche vers tâche' 'const parCle = {}; // clé d’idempotence vers identifiant de tâche' 'let horloge = 0; // une fausse horloge, en secondes' 'let suivant = 1;' 'function soumettre(cle, charge) {' ' if (cle in parCle) return parCle[cle];' ' const id = \"tache-\" + suivant++;' ' taches[id] = { id: id, charge: charge, etat: \"en-attente\", essais: 0, resultat: null };' ' parCle[cle] = id;' ' return id;' '}' > queue.js"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.
Écrire la file, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const echecsAvant = { \"tache-2\": 1 }; // tache-2 échoue une fois, puis marche' 'function travailler(tache) {' ' if (tache.essais <= (echecsAvant[tache.id] || 0)) throw new Error(\"délai dépassé en amont\");' ' return tache.charge.toUpperCase();' '}' 'function lancer(tache) {' ' if (tache.etat === \"termine\" || tache.etat === \"echec\") return;' ' while (tache.essais < 3) {' ' tache.essais++;' ' tache.etat = \"en-cours\";' ' horloge += 2;' ' try { tache.resultat = travailler(tache); tache.etat = \"termine\"; return; }' ' catch (erreur) { tache.etat = \"en-attente\"; horloge += tache.essais * 5; }' ' }' ' tache.etat = \"echec\";' '}' >> queue.js"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.
Écrire la file, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'soumettre(\"commande-1\", \"facture a\");' 'soumettre(\"commande-2\", \"facture b\");' 'soumettre(\"commande-1\", \"facture a\"); // le client réessaie : même clé, même tâche' 'Object.values(taches).forEach(lancer);' 'Object.values(taches).forEach(lancer); // un second passage ne lance rien' 'for (const id in taches) console.log(id, taches[id].etat, \"essais\", taches[id].essais, taches[id].resultat);' 'console.log(\"tâches :\", Object.keys(taches).length, \"horloge :\", horloge + \" s\");' >> queue.js"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.
Écrire la file, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const echantillon = \"Vos deux factures sont prêtes.\";' ' const texte = \"Deux tâches sont terminées. Écrivez une courte phrase d’état pour le client.\";' ' console.log(\"Appel réel :\", await demander([{ role: \"user\", content: texte }], echantillon, 40));' '}' 'main();' >> queue.js"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.
Lancer la file
docker exec -w /home/node/project agent node queue.jsAffiché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 doublure, pas une vraie file
La file ici est un objet en mémoire et l’horloge est un nombre. Si le processus s’arrête, toutes les tâches sont perdues. Un vrai système garde les tâches dans un service fait pour cela, et les exécutants tournent dans d’autres processus.
Les idées restent valables : des états, un nombre d’essais, une table de clés et une borne sur les reprises.
Une sortie possible
| Ligne | Ce qu’elle montre |
|---|---|
| tache-1 termine essais 1 FACTURE A | Une tâche réussie du premier coup |
| tache-2 termine essais 2 FACTURE B | Une tâche qui a échoué une fois, puis réussi |
| tâches : 2 horloge : 11 s | Trois envois ont créé deux tâches |
| Appel réel | Une courte phrase d’état, ou un échantillon enregistré étiqueté |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.