Repository navigation
CI runners ai-01 : un quota REST epuise arrete tout le pool, et des restes de tests bloquent son redemarrage #21527
Description
Mesure du 2026-10-20 sur ai-01 (journal coursia-runner.service, heures en UTC). Le pool Linux ephemere d'ai-01 est reste a zero de 01:26Z a 03:30Z, avec 1 202 runs en file et 13 runners inscrits pour toute la flotte.
Ce qui s'est passe
| Heure (UTC) | Evenement |
|---|---|
| 01:24 | Quota REST du compte jsboige epuise (5000/5000 ; tout la flotte l'utilise). fetch_token recoit un HTTP 403 « API rate limit exceeded ». |
| 01:26:32 | Slot 13 : ABANDON : 5 echecs consecutifs HTTP 403 sur le compte '{' (compte sans droit admin ?). Le slot pose STOP_FILE et appelle die, si bien que tous les slots sortent. |
| 01:36:33 | Le superviseur s'arrete ; l'unite systemd est desactivee. |
| 01:37-01:39 | Six redemarrages refuses : sentinelle presente ET superviseur vivant -- arret en cours, on n'interfere pas. Puis Start request repeated too quickly, et l'unite reste en failed. |
| 03:30 | Reparation a la main : processus de test orphelins tues, reset-failed, start. La sentinelle perimee est purgee et le pool redemarre. |
Les quatre defauts
1. Un 403 de quota est classe « structurel ». supervise.sh L1599-1602 : tout 4xx sauf 408 est terminal, et au cinquieme le slot arrete tout le pool (L1612-1613 ; meme logique pour les waiters, L2135). Or GitHub signale un quota epuise par un 403 (ou 429) avec X-RateLimit-Remaining: 0 : c'est transitoire, avec une echeance connue (X-RateLimit-Reset). Le retry le resout, a condition d'attendre jusqu'a la remise a zero au lieu d'abandonner.
2. Le compte s'affiche '{', et un corps JSON passe pour un jeton. Le log du slot 13 se termine par New-line characters are not allowed in header values : le corps d'erreur JSON a ete lu comme nom de compte (ACCOUNT={) puis comme jeton d'inscription. fetch_token doit valider la forme du jeton avant de le rendre, et ecrire ACCOUNT= depuis le compte epingle, jamais depuis la reponse.
3. Le predicat « superviseur vivant » est global a la machine. persist/ai-01/coursia-runner-start.sh L145 et persist/coursia-runner-start.sh L212 : pgrep -f 'supervise\.sh start' reconnait n'importe quel supervise.sh start de la machine. Il reconnaissait ici trois processus d'un autre worktree (/mnt/d/CoursIA-2-wt-baseline/.../supervise.sh start 1 --force), dont l'etat vivait sous /tmp/supervise-test-13007/state-C : des restes de test_supervise_guards.sh. Le predicat doit reconnaitre le superviseur de ce COURSIA_RUNNER_STATE_DIR, par exemple par un fichier de PID dans l'etat de production ou par la lecture de l'environnement du processus. Meme defaut dans coursia-waiters-start.sh L93 et coursia-lean-start.sh L145.
4. Les tests laissent des superviseurs vivants. Les orphelins avaient 6 a 11 h, avec /init pour parent et des enfants relances en boucle : start 1 --force du worktree baseline, waiters 1 de CoursIA-2-wt-21446 et de CoursIA-21474-base. test_supervise_guards.sh doit tuer tout ce qu'il lance (trap sur EXIT, groupe de processus), y compris quand la suite est interrompue.
Le couplage de fond
Les runners obtiennent leur jeton d'inscription avec le meme compte jsboige que toutes les lanes de la flotte. Une flotte qui consomme son quota REST arrete donc la CI : c'etait le cas ce matin (signale par l'adjoint vers 01:40Z). Deux voies a mesurer, sans les trancher ici : (a) un jeton d'inscription est valable une heure et reutilisable pour plusieurs inscriptions, le partager entre slots divise les appels ; (b) un compte ou une app dedies aux runners, ce qui touche les droits du depot et releve donc d'une decision du user.
Criteres de fermeture
- Un 403 ou 429 de quota (
X-RateLimit-Remaining: 0ou corps « rate limit ») attend la remise a zero et ne pose jamaisSTOP_FILE; test qui le verrouille. fetch_tokenne rend jamais un corps JSON comme jeton ; test.- Les trois scripts de demarrage ne voient que le superviseur de leur propre etat ; test avec un
supervise.sh startleurre lance depuis un autre chemin. test_supervise_guards.shne laisse aucun processus apres une interruption ; test.- La voie (a) est mesuree (appels REST par heure avant/apres).
Activity
jsboige commentedon Oct 20, 2026
[CLAIMED] lane myia-ai-01:CoursIA-2 — corriger les quatre defauts (403 de quota transitoire, jeton JSON, predicat de demarrage par etat, nettoyage des tests) ; claim pose au dispatch par le coordinateur myia-ai-01:CoursIA. paths: scripts/ci/docker/linux-runner/**
fix(ci,#21527): le predicat de superviseur est par-checkout, pas global
jsboige commentedon Oct 20, 2026
[DELIVERED] lane myia-ai-01:CoursIA-2 -- PR #21541
Quatre defauts fermes, voir le body. La suite de garde (Test 70 inclus) tournera sur la PR ; les FAIL isoles a minuteur sous run GPU 2 sont du bruit de charge (mesure c.2237), pas une regression.
Le jeton d'inscription partage (option (a)) reste a mesurer — non tranche ici.
Riders : les commits 34a5a10 + 3dd0b0238 sont encore sur la branche fix/20727-budget-reprise-famille (pousses avant le split) ; ils deviennent redondants quand #21541 merge — le coordinateur choisit : revert de la branche #21494 ou acceptation en rider.
fix(ci,#21527): la fixture gh rend un jeton de forme reelle
fix(ci,#21527): Test 70 exercait la boucle avec 6 arguments pour 7, e…
fix(ci,#21527): quota REST transitoire + jeton valide + predicat par-…
fix(ci,#21601): le Test 24 juge la COMPLETUDE par le code de sortie +…
fix(ci,#21601): oracles temporels invariants + budgets de temps dans …
jsboige commentedon Oct 21, 2026
[CLOSURE PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
issue: 21527
verdict: KEEP
acceptance:
residue: waiver: les quatre defauts du body sont clos par la PR fix(ci,#21527): quota REST transitoire + jeton valide + predicat par-checkout #21541 (MERGED) ; il reste la voie (a), mesure des appels REST par heure avant/apres du jeton d'inscription partage entre slots, explicitement renvoyee a l'issue par la PR elle-meme (section « Hors scope (declare) »), et l'arbitrage user de l'option (b), compte ou App dedie aux runners, non pris ; dossier atteste au 2026-10-21, issue maintenue ouverte sur ce seul point
open-prs: 0
comments-reviewed: 2
[/CLOSURE PREFLIGHT]
jsboige commentedon Oct 26, 2026
[INFO] lane myia-ai-01:CoursIA-2 — voie (a) mesurée : le pool consomme 155 appels REST par heure pour des jetons d'inscription ; un jeton partagé est faisable (expérimenté). Il reste l'implémentation + le déploiement.
« Avant » — mesuré sur le pool d'ai-01
Chaque cycle de conteneur demande un jeton d'inscription, et chaque jeton est un appel REST sur le quota du compte partagé par la flotte (5000/h) :
Répartition (24 h) : slot 13 → 379, slot 7 → 347, slot 2 → 340, slot 1 → 317, slot 8 → 279, … slot 10 → 163. Les 14 slots du pool d'ai-01 tournant en continu.
Soit ~155 appels/h sur le quota de la flotte, soit 3,1 % des 5000/h — la part que ce pool contrôle, la cause directe du blocage du 2026-10-20 étant un quota à 5000/5000 partagé avec toutes les lanes.
État du compte au moment de la mesure :
core remaining = 3225 / 5000.(Note :
fetch_tokenfait un seul appel depuis #21541 — l'ancienne version en faisait deux. Le facteur 2 est déjà corrigé.)« Faisabilité » — expérience décisive, et son résultat
Question : un même jeton d'inscription peut-il enregistrer plusieurs runners ? Sans réponse, la voie (a) est morte.
Protocole : un seul appel REST pour obtenir le jeton ; deux containers jetables de l'image du runner (
coursia-linux-runner:2.338.0), chacun recevant ce même jeton ; dans chaque container,config.sh --unattended --ephemeral --replacesur une copie de/opt/runner(le.runnerdes slots vivants n'est pas touché) ; runners nomméscoursia-token-probe-A/Bavec le labelcoursia-token-probe, qu'aucun workflow ne réclame (ils ne peuvent donc pas recevoir un job) ; suppression par l'API des deux runners à la fin.Résultat :
Nettoyage vérifié :
DELETE …/actions/runners/420316et…/420317→ HTTP 204, etGET …/actions/runnersne rend plus aucun runnercoursia-token-probe.Validité du jeton, mesurée sur la réponse de l'API :
expires_at = 2026-10-26T04:19:47Zpour une émission à 03:19 → 1 heure.« Après » — projection bornée par la mesure
Soit −154 appels/h, soit −99,4 % de la consommation d'inscription du pool, et ≈ 3 % du quota REST de la flotte rendu aux autres lanes. Le « 1 par heure » est une borne haute : un jeton qui reste valide plus longtemps (jusqu'à 60 min) ne coûte pas plus.
Ce que la mesure ne dit pas : que le partage soit sans risque. Un jeton révoqué ou expiré plus tôt que prévu ferait échouer l'enregistrement de plusieurs slots d'un coup ; le remède est l'invalidation du jeton partagé au premier
docker runnon nul (le chemin d'échec existe déjà dansslot_loop), avec repli sur le comportement actuel.Ce qui reste, et à qui
$STATE_DIR/registration_token, âge max 45 min, invalidé sur échec de cycle) +shared_registration_token()appelé parslot_loopau lieu defetch_token. Repli : le comportement actuel, si le jeton partagé est absent ou trop vieux./mnt/d/CoursIA/scripts/ci/docker/linux-runner/supervise.sh) qu'il faut modifier et redémarrer — 14 slots, donc une opération de flotte, à planifier côté coordinateur et non dans une session de nuit.Je n'ai pas implémenté (1) dans ce passage : le bénéfice mesuré est de 3 % du quota, et le coût du déploiement est un redémarrage du pool — ce n'est pas un arbitrage que je prends seul à cette heure. Le dossier est prêt à être exécuté quand le coordinateur ouvre une fenêtre.
Voir aussi #21541 (les quatre défauts clos), #15154 (épinglage du compte).
jsboige commentedon Oct 28, 2026
[DELIVERED] lane myia-ai-01:CoursIA-2 -- PR #23055 -- paths: scripts/ci/docker/linux-runner/persist/**, .github/workflows/assert-memory-budget.yml
Le critere « les trois scripts de demarrage ne voient que le superviseur de leur propre etat ; test avec un
supervise.sh startleurre lance depuis un autre chemin » n'etait pas tenu parmain: #21541 avait cable l'organe sur une seule jambe (ai-01), et la forme du cable etait un exec direct d'un fichier100644dans l'index -- donc inerte sur un checkout Linux natif (Permission denied -> predicat faux -> purge a tort). Les six autres wrappers restaient sur lepgrepper-machine.PR #23055 cable les sept wrappers sur l'organe lu par
bash, conserve un repli conservateur (organe illisible -> predicat per-machine), rend la racine de scan injectable, reecrit le harnais sur un/procFAKE (avec stubpgrep) et y ajoute les cas leurre / organe-absent / organe-non-executable, puis cable ce harnais dansassert-memory-budget.yml-- il etait hors CI, donc une regression du predicat n'aurait rougi nulle part.Mesure : 66 PASS / 0 FAIL sur Git Bash ET sur WSL (Linux natif, superviseurs reels de l'hote en cours). Controles negatifs : predicat degrade en per-machine -> 8 FAIL ; wrapper revenu au
pgrepglobal -> FAIL waiters/5 ; wrapper revenu a l'exec direct -> FAIL waiters/8.Residu de deploiement, a dire : les wrappers sont des copies deployees (
/usr/local/bin/coursia-*.sh+ unites systemd). Le predicat ne sera effectif sur ai-01, po-2024, po-2025, po-2027 et po-2028 qu'apres le merge et le redeploiement du wrapper sur chaque machine, avec un checkout a jour. Cette PR livre le code et son verrou ; elle ne redeploie rien.Etat des criteres de closure au 2026-10-28 :
supervise.shet son contrat de rotation, hors du perimetre du predicat de purge. Elle reste a faire.Fix(infra,#21527): le superviseur est lance en chemin ABSOLU (reserve…
Merge origin/main dans fix/21527-per-checkout-predicate
Fix(infra,#21527): le predicat est appele par bash comme les sept wra…
jsboige commentedon Oct 30, 2026
[CLAIMED] lane myia-po-2028:CoursIA-2 -- critere 5 restant (voie (a) : jeton d'inscription partage entre slots, mesure avant/apres ; les criteres 1-4 sont livres sur main, verifies ce cycle) -- paths: scripts/ci/docker/linux-runner/supervise.sh, scripts/ci/docker/linux-runner/test_supervise_guards.sh -- 2026-10-30T13:25Z
Verification firsthand au claim (c49) : c1 (403/429 quota transitoire + attente reset) sur
supervise.sh:1749-1760; c2 (forme du compte l.1684-1689, forme du jeton l.1731, appel unique l.1726) ; c3 (PR #23055) ; c4 (trap cleanup_launched EXIT INT TERM+ kill du groupe de processus,test_supervise_guards.sh:43-55). Temoin live du correctif c1 : le pool de po-2028 a traverse l'epuisement du quota REST de ce jour (fenetre ~13:1xZ) sans aucun ABANDON/STOP_FILE, 3 slots en travail reel. Reste c5 : partager le jeton (valable 1 h) entre slots/cycles, le verrouiller par test, et mesurer les appels REST/h apres deploiement (avant : 155/h, mesure ai-01 du 26/10).jsboige commentedon Oct 30, 2026
[DELIVERED] lane myia-po-2028:CoursIA-2 -- PR #23529 -- paths: scripts/ci/docker/linux-runner/supervise.sh, scripts/ci/docker/linux-runner/test_supervise_guards.sh
Critere 5 (voie (a)) livre : cache partage du jeton d inscription dans STATE_DIR (cle compte-effectif+repo, TTL 3300 s, ecriture atomique chmod 600, miss silencieuse sur cache corrompu). Borne mecanique : 155 appels/h -> <= 1,1/h, independante du nombre de demarrages. Mesure post-deploiement (lecture de FETCHED_AT du cache sur une heure, pool en service) relevee au prochain cycle sur le poste qui porte le pool. Suite test_supervise_guards.sh complete rejouee : 181 PASS / 0 FAIL, dont Test 73 (7 assertions) et l isolation du cache dans le Test 70. Les criteres 1-4 etaient deja sur main, verifies firsthand en c.6523533265.
Doc(runner,#21527): le commentaire de fetch_token decrit le regime du…