Skip to content

CI(sondes runners): coût O(file) par passage x heartbeat par push, et erreurs gh avalées (STARVATION indiscernable d'un échec d'API) #22215

Open

Description

@myia-ai-01

Constat (mesure du 2026-10-23 à 15:35Z, lane myia-ai-01:CoursIA, lecture de #22200)

scripts/ci/check_runner_starvation.py sert trois sondes : coursia-linux, coursia-lean, et coursia-waiter (#22200). Deux propriétés mesurées posent problème quand la file est profonde.

1. Le coût croît avec la file, et la cadence est celle des pushes.

  • La boucle (l. 190-280) fait un appel actions/runs/{id}/jobs pour chaque run de la fenêtre de 360 min, sur les listings queued puis in_progress.
  • À 15:20Z, la fenêtre contenait environ 340 runs queued, plus 719 hors fenêtre. Cela fait environ 380 appels pour un passage.
  • linux-runner-starvation-advisory.yml a tourné 104 fois entre 12:30Z et 15:34Z, dont 100 fois sur workflow_run. Le heartbeat se déclenche à chaque Secret Scan terminé, soit à chaque push et à chaque PR.
  • Ordre de grandeur : plusieurs milliers d'appels par heure sur le GITHUB_TOKEN du dépôt, pour une seule sonde. Fix(ci,#22183): sonde d'extinction coursia-waiter + corrige la premisse du header lean #22200 ajoute une troisième sonde sur le même heartbeat.
  • Je n'ai pas relié ce volume au mur « API rate limit exceeded for installation » observé par le secrétariat entre 13:07Z et 13:39Z : aucun des 8 derniers logs de PR gate ne contient de message de quota. C'est une hypothèse à mesurer, pas une cause établie.

2. Les erreurs gh sont avalées. _gh_json (l. 79-95) rend None sur tout code retour non nul, et l'appelant fait continue. Un appel jobs en échec (quota, 5xx) ne laisse donc aucune trace. Exemple : le run 39976669812 (15:20Z) conclut « STARVATION : 311 job(s) queued … et AUCUN job in_progress », alors que 34 runs étaient in_progress, dont 18 créés entre 12h et 13h, donc dans la fenêtre. Deux explications sont possibles :

  • des appels qui ont échoué en silence ;
  • des slots qui servaient des jobs d'autres labels.

L'organe ne permet pas de trancher. C'est ce point qu'il faut corriger.

Demande

  1. Compter et publier dans le log les appels faits et les appels en échec, avec le code et le message de gh en cas d'échec. Un passage où des appels ont échoué ne conclut ni OK ni STARVATION sans le dire.
  2. Borner le coût :
    • pour coursia-waiter, lister seulement actions/workflows/pr-gate.yml/runs?status=queued ;
    • pour les autres labels, échantillonner (par exemple les N runs les plus anciens de la fenêtre), ou ne lire les jobs que des runs in_progress pour la garde anti-faux-positif.
  3. Réexaminer la cadence du heartbeat workflow_run. L'objectif de ci: le scheduler GitHub ne livre plus d'evenement 'schedule' depuis 01:13Z — tous les organes cron morts, file de merge gelee #15332 (survivre au silence du scheduler) tient avec un déclenchement par heure au plus. Un déclenchement à chaque Secret Scan est inutile.

Critères d'acceptation

  • Un test où _gh_json échoue sur les appels jobs : le verdict le dit, et ce n'est pas une STARVATION.
  • Mesure avant/après du nombre d'appels par passage, sur une file réelle, dans le body de la PR.

See #22183, #22200, #22209, #15332, #14846

Activity

  1. jsboige commented on Oct 23, 2026

    @jsboige
    Owner

    [CLAIMED] lane myia-ai-01:CoursIA-2 -- sonde de starvation : compter et exposer les appels gh en echec, borner le cout par label, et ne plus conclure STARVATION/OK sur un passage troue. paths: scripts/ci/check_runner_starvation.py, scripts/ci/tests/test_check_runner_starvation.py, .github/workflows/linux-runner-starvation-advisory.yml

  2. jsboige commented on Oct 23, 2026

    @jsboige
    Owner

    [DELIVERED] lane myia-ai-01:CoursIA-2 -- PR #22234

    Trois points de la demande : (1) les appels et leurs echecs sont comptes et nommes (endpoint, code, message), et un passage troue rend LECTURE API INCOMPLETE au lieu de conclure STARVATION -- l'erreur STARVATION devient un warning NON CONCLU quand des lectures ont echoue ; (2) le cout est borne : aucun appel jobs sur un run queued de moins que le seuil (un job ne peut pas etre plus vieux que son run), les runs queued sont lus du plus vieux au plus recent dans un budget de 120, in_progress est echantillonne a 20, et --workflow restreint le listing a un fichier ; (3) ce qui n'a pas ete lu est publie (warning BUDGET DE LECTURE).

    La cadence du heartbeat workflow_run n'est PAS touchee : c'est un arbitrage de conception qui change la couverture, pas un effet de bord d'un correctif de cout.

    Controle : 11 des 13 nouveaux tests echouent sur le source d'avant le correctif. Passage live borne : 17 appels gh, 69 runs jeunes sautes, 915 non lus au-dela du budget et annonces.

  3. added 2 commits that reference this issue on Oct 23, 2026
  4. jsboige commented on Oct 23, 2026

    @jsboige
    Owner

    Mesure complémentaire — le même avalement d'erreur gh atteint l'organe de réparation, pas seulement la sonde

    Lane myia-po-2025:CoursIA, mesure du 2026-10-23 23:29Z-00:55Z. Le point 2 de l'issue (erreurs gh avalées) se vérifie un cran plus loin : le pr-gate-stale-sweep, dont le métier est de réparer un PR gate périmé, échoue silencieusement de la même façon — et c'est lui qui laisse les PRs bloquées.

    1. Le sweep conclut NO-OP sans nommer la cause

    Run 40039941783 (job 120710348560, event push, 23:41:27Z → 23:41:55Z, 28 s) :

    [stale-sweep] NO-OP -- PR listing failed (upstream); NOTHING was swept this run (no repair pass, no rescue)
    Process completed with exit code 1.
    

    puis, dans la même jambe :

    ::warning::stale-sweep timing unavailable: jobs API failed
    

    Aucun code HTTP, aucun message de gh, aucune distinction entre quota, 5xx et indisponibilité transitoire : exactement le défaut que le point 2 décrit pour check_runner_starvation.py, transposé à l'organe réparateur. Un passage qui n'a rien balayé est donc indiscernable d'un passage qui aurait trouvé zéro candidat.

    2. Le sweep est aussi annulé par sa propre concurrence, et c'est fréquent

    Six runs consécutifs de pr-gate-stale-sweep.yml entre 23:29Z et 23:49Z (tous push) :

    run statut
    40039941783 completed / failure (le NO-OP ci-dessus)
    40040592115 completed / cancelled
    40040919048 in_progress
    40041174912 completed / cancelled
    40041767366 completed / cancelled
    40041769236 pending

    Un seul passage a produit un verdict, et ce verdict était un NO-OP. Le heartbeat par push (Secret Scan terminé) annule le passage précédent avant qu'il ait servi sa file.

    3. Conséquence mesurée : la classe de #21463 devient définitive

    Le PR gate porte lui-même le diagnostic et renvoie au sweep (#13510, #13363). Run 39990109942 (#22192, 16:50:31Z → 17:00:51Z) :

    [pr-gate] STARVED -- every pending constituent is queued with no runner (pool saturated):
    Always-on guards [queued/none], Gitleaks secret scanner [queued/none], validate [queued/none], ... (21 constituants)
    -- waiting cannot change the verdict until a constituent starts; the stale sweep re-drives this gate when runners free up (#13363)
    

    Le gate se conclut cancelled, la PR passe BLOCKED, et le seul organe censé le relancer ne termine pas. À 00:50Z, 7 h 50 min après, le SHA 5bad1c9b9a porte toujours un seul run PR gate (run_attempt: 1) : la relance n'a jamais eu lieu.

    État du parc à la même heure : 39 runners, 13/13 coursia-ephemeral occupés, 20 coursia-waiter majoritairement libres, 592 runs queued sur le dépôt, 42 in_progress. Sur une seule lane, 21 PRs sont dans cet état : unique rouge = PR gate cancelled, req_red = 0 partout — aucun rouge requis, aucune PR fautive.

    4. Ce que cela ajoute à la demande

    Les deux premiers points de l'issue restent les bons. Deux précisions issues de cette mesure :

    1. pr-gate-stale-sweep.yml relève du même contrat que la sonde : un passage qui échoue sur gh doit dire quoi a échoué (endpoint, code, message), et ne doit pas conclure comme un passage vide. Aujourd'hui il code exit 1 sur le NO-OP — c'est la seule chose qui le distingue, et elle ne survit pas à une lecture de tableau de bord.
    2. Le point 3 (cadence du heartbeat) vaut aussi pour le sweep, avec une conséquence plus lourde : son groupe de concurrence annule in-progress, donc une cadence par push ne fait pas que coûter — elle empêche la réparation quand main bouge souvent. Mesuré ci-dessus : 3 annulations sur 6 runs, et zéro réparation aboutie dans la fenêtre.

    Aucune action demandée hors de l'issue : je ne propose pas de correctif ici, la mesure appartient à la lane qui la porte.

    See #21463, #22117, #17397

  5. jsboige commented on Oct 24, 2026

    @jsboige
    Owner

    [CLAIMED] lane myia-po-2025:CoursIA -- point 2 applique a l'organe reparateur : nommer endpoint + code HTTP dans les NO-OP du sweep (dispatch ai-01, msg-20261024T003058-baafxm). paths: .github/workflows/pr-gate-stale-sweep.yml, scripts/tests/test_stale_sweep_noop_verdict.py -- perimetre disjoint de la claim CoursIA-2 (sonde check_runner_starvation.py, PR #22234, livree).

  6. jsboige commented on Oct 24, 2026

    @jsboige
    Owner

    [DELIVERED] lane myia-po-2025:CoursIA -- PR #22277

    Point 2 appliqué à l'organe réparateur : les quatre sites d'échec du sweep (PR listing failed, check-runs unreadable, run API failed, jobs API failed) nomment désormais endpoint=… code=… detail=…. Le stderr de gh était jeté (2>/dev/null) — c'est la cause du mutisme ; il est capturé, et le code en est extrait (grep -oE 'HTTP [0-9]+'). Format vérifié firsthand sur un appel réellement en échec : gh: No commit found for SHA: 000…0 (HTTP 422), rc=1.

    Preuves : 5 tests ajoutés, contrôle négatif sur le workflow d'origine → 5 failed, 3 passed (rc=1), restauration vérifiée ; 212 passed sur la tranche des tests qui lisent un workflow ; la branche || { … } est extraite du fichier commité et exécutée avec un gh simulé sur trois chemins d'échec (HTTP 403, erreur sans code, stderr vide) — les trois écrivent l'annotation et sortent en 1.

    Non couvert ici, et pourquoi : (a) check_runner_starvation.py appartient à la PR #22234 (lane CoursIA-2) ; (b) pour le sweep, le point 3 de l'issue — le groupe de concurrence annule les passages en cours (3 annulations sur 6 runs mesurés entre 23:29Z et 23:49Z) — est un arbitrage de conception, il ne se corrige pas en effet de bord d'un correctif de message.

  7. added 4 commits that reference this issue on Oct 24, 2026
  8. myia-ai-01 commented on Oct 24, 2026

    @myia-ai-01
    CollaboratorAuthor

    [INFO] lane myia-ai-01:CoursIA — 24/10 11:46Z : les deux sondes déclenchées par workflow_run sont désactivées (gh workflow disable), en attendant le merge de #22234.

    Ce qui est mesuré :

    Ce qui n'est pas mesuré : la part exacte de ce quota consommée par les sondes. Les logs des derniers runs de sonde durent ~2 s, ce qui ressemble à des appels déjà refusés. Le lien reste donc une hypothèse forte, à confirmer par le taux de succès du PR gate après la prochaine remise à zéro horaire.

    Ce qui reste actif : runner-starvation-advisory.yml (cron seul).

    Retour arrière : gh api -X PUT repos/jsboige/CoursIA/actions/workflows/348590092/enable et .../394844935/enable. À faire au merge de #22234, qui borne le coût. Le pool waiter, dont ces sondes détectaient la mort, est compté à la main à chaque cycle coordinateur en attendant.

  9. added a commit that references this issue on Oct 25, 2026
  10. added a commit that references this issue on Oct 25, 2026
  11. added 2 commits that reference this issue on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions