Skip to content

fix(ci,#18292): DEAD en avertissement sur la jambe push, age du sweep non juge quand le sweep du commit tourne - #18307

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/18292-sweep-health-probes
Sep 29, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/18292-sweep-health-probes

Conversation

@jsboige

@jsboige jsboige commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2023:CoursIA — prev: MED/notebook-python #18101

Summary

  • La jambe push de pr-gate-sweep-health-advisory portait deux faux positifs qui l'ont rougie sur 37 des 45 commits de main du 2026-09-28 sans qu'aucun de ces commits ne soit cassé : un DEAD de livraison schedule chronique (ci: le scheduler GitHub ne livre plus d'evenement 'schedule' depuis 01:13Z — tous les organes cron morts, file de merge gelee #15332) recopié sur chaque commit, et une course avec le sweep du même push.
  • Nouveau mode --dead-exit warning sur l'organe : la jambe push nomme un DEAD en ::warning:: et laisse son step vert. Aucun seuil ne bouge (DEAD_FACTOR, DEAD_FLOOR_MIN intacts) et evaluate() est inchangé — seule la sévérité imprimée change, et elle se choisit par jambe dans le workflow. schedule et workflow_dispatch gardent le rouge ; INSTRUMENT_UNKNOWN le garde dans les deux modes.
  • Nouveau mode --sweep-alive-for-sha SHA : si un run de pr-gate-stale-sweep.yml porte ce commit et est vivant (queued / in_progress / completed+success), l'âge n'est pas jugé. Sinon rc 3 (aucun run vivant) ou rc 4 (sonde en échec) → repli sur le critère d'âge. Le garde de course ne peut donc pas masquer un sweep vraiment mort pour ce commit.
  • Le fichier est aussi rendu conforme au ratchet check-subprocess-encoding ([tooling] encoding=utf-8 manquant sur subprocess text=True : 98 sites restants sur 40 fichiers (généralisation #12813/#12811) #13140), dont le hook scanne le fichier entier dès qu'un commit le touche : ses deux subprocess.run reçoivent encoding="utf-8", errors="replace".

Changes

Fichier Changement
.github/workflows/pr-gate-sweep-health-advisory.yml Step cadence : --dead-exit warning quand github.event_name == 'push', appel inchangé sinon. Step sweep_age : sur push, sonde de course d'abord (--sweep-alive-for-sha "${GITHUB_SHA}"), exit 0 si vivant, sinon repli sur le critère d'âge. Commentaire daté en tête de fichier.
scripts/ci/check_scheduler_liveness.py SWEEP_WORKFLOW, LIVE_RUN_STATES, sweep_run_alive_for_sha(), probe_sweep_runs(), sweep_alive_verdict() (0/3/4) ; arguments --dead-exit et --sweep-alive-for-sha ; rendu du DEAD sensible à la sévérité ; encoding= sur les deux subprocess.run.
scripts/tests/test_check_scheduler_liveness.py 16 tests neufs : sévérité (défaut rouge, warning vert et nommé, INSTRUMENT_UNKNOWN rouge dans les deux modes, seuils non déplacés), sonde de course (les trois états vivants, l'échec, l'autre sha, l'historique vide, la relance), et les deux contrôles d'acceptance au niveau CLI.

Ce que le diff ne fait pas

Review Checklist

  • 1. Scope — Le diff est borné au workflow, à son organe et à ses tests, comme l'issue le prescrit. Aucune écriture hors de ces surfaces : la mise en conformité du fichier d'organe au ratchet d'encodage est la seule modification supplémentaire, imposée par le hook (voir ci-dessous).
  • 2. Post-fix validation — Les blocs run ont été extraits du YAML et exécutés (pas une paraphrase), avant le dernier commit : 4 contrôles mesurés, tableau en Test plan. Relancés après le fix d'encodage.
  • 3. Cohérence pédagogique — Sans objet (pas de notebook).
  • 4. Exécution réelle — Sans objet (pas de notebook) ; les contrôles sont des exécutions réelles contre l'API Actions.
  • 5. Regression check — grep "pr-gate-sweep-health-advisory" sur le dépôt : les suites voisines test_stale_sweep_noop_verdict.py (invariant --status success + --workflow pr-gate-stale-sweep.yml) et test_heartbeat_sweep_emit.py (le fichier doit exister, référencer le sweep et être piloté par schedule) lisent ce même fichier et passent — 50 tests verts sur les trois suites.

Anti-regression

  • Pas de sorry Lean introduit (aucun .lean touché).
  • Aucun @pytest.skip ni assert True ajouté pour contourner un test.
  • Le seul retrait de code existant est la ligne subprocess.run(...) d'origine, remplacée par sa forme encodée — insertions très au-dessus des suppressions.

Note sur la conformité d'encodage

Le hook check-subprocess-encoding (#13140) est un ratchet au niveau fichier : sa description dit « only files the commit touches are scanned, so the gate is green on main while the historical sweep lands tranche by tranche ». Toute modification de check_scheduler_liveness.py exige donc que ce fichier soit propre. Il portait deux appels text=True sans encoding= (dont un pré-existant, dans probe) : les deux reçoivent encoding="utf-8", errors="replace", la forme recommandée par le hook. Sur un hôte cp1252, text=True seul lève UnicodeDecodeError sur un payload UTF-8 (#12811) — c'est la classe d'incident que le ratchet ferme, et avec errors="replace" un payload corrompu tombe dans la branche déjà gérée payload illisible.

Test plan

Tous les contrôles ont été exécutés sur le dépôt réel, en extrayant les blocs run du YAML (donc ce qui tournera, pas une réécriture), et relancés après le fix d'encodage.

Contrôles d'acceptance de l'issue

Contrôle Montage Résultat
Positif — sweep du même head_sha en cours --sweep-alive-for-sha sur le sha du dernier run de sweep rc 0 — « un run de pr-gate-stale-sweep.yml porte ce commit (completed/success) — sweep vivant, l'age n'est pas juge »
Négatif — aucun run pour github.sha et dernier succès > 60 min bloc sweep_age réel, amont contrôlé : sweep sans run pour le sha, dernier succès vieux de 5400 s rc 1 — ::error:: last successful pr-gate-stale-sweep run is 5400s old (> 60 min)
DEAD sur push step cadence du YAML, EVENT_NAME=push rc 0, 3 × ::warning::[scheduler-liveness]
DEAD sur schedule step cadence du YAML, EVENT_NAME=schedule rc 1, 2 × ::error::[scheduler-liveness]

Le montage « négatif » est celui que l'issue décrit mot pour mot ; la sonde de course rend bien rc 3 sur un commit de main sans run de sweep (43fca13e38a), et le repli sur l'âge rougit ensuite.

Repli (le garde de course ne masque pas un sweep mort) — bloc sweep_age réel, EVENT_NAME=push :

Cas Résultat
Sweep vivant pour le commit rc 0, « sweep vivant pour ce commit — age non juge (course, #18292) »
Aucun run pour le commit, âge sous la fenêtre rc 0, « last successful sweep run: 1243s ago — OK »
Aucun run pour le commit, âge au-dessus de la fenêtre rc 1, ::error:: last successful pr-gate-stale-sweep run is 1245s old (> 5 min)
schedule, sonde d'âge inchangée rc 0

État mesuré au moment du contrôle — la cadence servie est bien chronique : DEAD pour le sweep (servi 270 min pour 60 déclarées, dernier run planifié vieux de 7383 min) et pour cet observateur (servi 274 min pour 30 déclarées). C'est cet état, recopié en rouge sur la jambe push, qui produisait la croix sur presque chaque commit.

Tests : python -m pytest scripts/tests/test_check_scheduler_liveness.py → 34 passed ; avec les deux suites voisines → 50 passed. Hooks pre-commit verts (gitleaks, check-subprocess-encoding).

Résiduel

Le quatrième critère d'acceptance — « mesure après merge sur les 20 commits suivants de main : le nombre de commits rouges uniquement à cause de cet advisory tombe à 0, hors sweep réellement mort » — ne peut pas être satisfait avant le merge. Il sera mesuré après, et l'issue reste ouverte pour cela. D'où See #18292 et non Closes #18292.
🤖 Generated with Claude Code

… non juge quand le sweep du commit tourne

Le workflow advisory `pr-gate-sweep-health-advisory` portait deux faux positifs
sur sa jambe `push`, qui l'ont rougie 37 fois sur les 45 commits de main du
2026-09-28 sans qu'aucun de ces commits ne soit casse.

1. Sonde de cadence. Un DEAD de livraison `schedule` est un etat CHRONIQUE
   depuis le 09/09 (#15332 : crons a 30 et 60 min servis toutes les ~4 h 30),
   pas un incident. Nouveau mode `--dead-exit warning` : le verdict est
   inchange, `DEAD_FACTOR`/`DEAD_FLOOR_MIN` ne bougent pas, seule la severite
   imprimee change. La jambe `push` l'utilise ; `schedule` et
   `workflow_dispatch` gardent le rouge. `INSTRUMENT_UNKNOWN` garde son rouge
   dans les deux modes : c'est la mesure qui est en panne.

2. Sonde d'age. Sur `push`, cette jambe et le sweep partent du MEME commit,
   donc la sonde -- qui cherche le dernier succes TERMINE -- lisait celui du
   merge precedent pendant que le sweep du commit tournait (instance
   fondatrice : run 36461754953 vs sweep 36461754984, meme head_sha). Nouveau
   mode `--sweep-alive-for-sha SHA` : rc 0 si un run du sweep porte ce commit
   et est vivant (queued/in_progress/completed+success), rc 3 sinon, rc 4 si
   la sonde echoue. rc 3 et rc 4 retombent sur le critere d'age, donc le garde
   de course ne peut pas masquer un sweep vraiment mort pour ce commit.

Le fichier est aussi rendu conforme au ratchet `check-subprocess-encoding`
(#13140) sur ses deux appels `subprocess.run` : le hook scanne le fichier
entier des qu'un commit le touche, donc la tranche historique de ce fichier
tombe ici -- `encoding="utf-8", errors="replace"` sur les deux, la ou un hote
cp1252 leve UnicodeDecodeError sur un payload UTF-8 (#12811).

Controles mesures sur le depot reel, en executant les blocs `run` extraits du
YAML (pas une paraphrase) :

- controle positif (sweep vivant pour le sha) -> rc 0, age non juge ;
- controle negatif de l'acceptance (aucun run pour le sha + dernier succes
  vieux de 90 min) -> rc 1, `::error::` du critere d'age ;
- cadence jambe `push` -> rc 0 avec `::warning::` ; jambe `schedule` -> rc 1
  avec `::error::` (2 verdicts DEAD mesures en direct, dont le sweep lui-meme,
  dont le dernier run planifie datait de 5,1 jours).

34 tests unitaires dans scripts/tests/test_check_scheduler_liveness.py (16
neufs), 50 avec les suites voisines (test_stale_sweep_noop_verdict.py et
test_heartbeat_sweep_emit.py, qui portent les invariants de ce meme fichier).

See #18292

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2023:CoursIA a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #18096 (MED/readme, merge a 2026-09-28T07:37:42Z), #18122 (MED/readme, merge a 2026-09-28T07:57:45Z), #18123 (LIGHT/readme, merge a 2026-09-28T13:13:22Z), #18125 (MED/readme, merge a 2026-09-28T13:13:26Z), #18132 (MED/readme, merge a 2026-09-28T13:13:31Z), #18139 (MED/readme, merge a 2026-09-28T13:13:35Z), #18071 (MED/readme, merge a 2026-09-28T13:37:59Z), #18051 (MED/guard, merge a 2026-09-28T16:20:41Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Sep 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-po-2023:CoursIA` voit ces signaux actifs sur les mergees du jour (UTC 2026-09-28) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=1 genre=8 cap=4)
  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=1 genre=8 cap=4)

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@jsboige

jsboige commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Always-on guards — organe perimeter corrigé (édition de body), et Scripts Tests (CPU) est environnemental

1. perimeter : le finding était juste, et il portait sur mon body

Verdict de l'organe sur cette tête, avant correction :

VERDICT: FAIL
  !! [PR body / jsboige] l'assertion pretend 2 fichier(s), la liste effective en compte 3 :
     .github/workflows/pr-gate-sweep-health-advisory.yml,
     scripts/ci/check_scheduler_liveness.py,
     scripts/tests/test_check_scheduler_liveness.py

La phrase visée était, au point 1 de la Review Checklist : « Le seul ajout hors de ces deux fichiers est la mise en conformité du même fichier au ratchet d'encodage ». Le diff touche bien trois surfaces — le workflow, l'organe, et ses tests — et l'assertion en annonçait deux.

Corrigé par édition de body (aucun commit, donc aucun ré-armement de DWELL) :

Aucune écriture hors de ces surfaces : la mise en conformité du fichier d'organe au ratchet d'encodage est la seule modification supplémentaire, imposée par le hook (voir ci-dessous).

Contre-épreuve, l'organe relancé sur le body édité : VERDICT: OK, rc=0. La CI l'a suivi — l'événement edited a relancé la jambe, et Always-on guards -- 16 organes, 1 checkout rend completed/success à 20:23:53Z sur la tête e4b57a8cb3.

Le PR gate en échec de 20:10:10Z est antérieur à cette correction : il n'agrège que Always-on guards, et il est relancé.

2. Scripts Tests (CPU) : 2 tests, tués par le noyau .NET du runner

2 failed, 16564 passed — les deux échecs sont dans un fichier que ce diff ne touche pas :

FAILED scripts/tests/test_papermill_meta_strip.py::test_dotnet_executor_subprocess_strips_stale_block
FAILED scripts/tests/test_papermill_meta_strip.py::test_exec_single_cell_subprocess_strips_stale_block
   RuntimeError: Kernel died before replying to kernel_info

Les deux lancent dotnet_executor.py / exec_single_cell.py, c'est-à-dire un noyau .NET. Le diff de cette PR touche check_scheduler_liveness.py, ses tests et un workflow — aucun de ces trois n'est en cause, et aucun ne peut faire mourir un noyau au démarrage.

Mesure locale sur cette tête : python -m pytest scripts/tests/test_papermill_meta_strip.py -k "strips_stale_block" -q → 6 passed in 5.07s, rc=0. Le noyau .NET démarre ici (règle H.2), il meurt sur le conteneur Linux du runner. C'est l'environnement, pas le code.

Cette jambe ne tient pas le merge : le seul check requis en échec sur cette PR est PR gate.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LGTM — tests exécutés firsthand + litmus anti-gaming vérifié par exécution]

Vérification = exécution, pas lecture : check_scheduler_liveness.py et test_check_scheduler_liveness.py extraits au head e4b57a8c, suite exécutée dans ce conteneur — 34/34 pass en 0.25 s. Le litmus anti-gaming est le point critique de ce type de changement (« sévérité » = terrain classique de déplacement de seuil déguisé) : vérifié par exécution, pas par lecture du commentaire — DEAD_FACTOR=4, DEAD_FLOOR_MIN=90.0, verdict_for(241,60)=DEAD vs verdict_for(239,60)=LATE — aucun seuil ne bouge, seul le marqueur ::error::→::warning:: et l'exit changent, sur la jambe push seulement (les jambes schedule/workflow_dispatch gardent le rouge — lu dans le workflow au head).

La course sweep/age est traitée correctement : sweep_run_alive_for_sha ne rejoue que les runs portant ce head_sha (un run vivant pour un autre commit ne blanchit rien — testé), un completed/failure ne vaut pas service (testé), rc 3/4 retombent sur le critère d'age (testé) — le garde ne peut pas masquer un sweep réellement mort pour ce commit.

Preuve-vive sur l'organe : ce job est continue-on-error: true (advisory non bloquant, vérifié dans le workflow) — le changement n'affaiblit donc aucun gate de merge ; il répare un observateur qui rougissait 37/45 commits de main du 28/09 (#18292), exactement la classe « alarme permanente ignorée » que sa propre docstring nomme.

Le rouge CI Scripts Tests (CPU) est un flake infra, pas le diff : logs lus — Kernel died before replying to kernel_info sur dotnet_executor.py/exec_single_cell.py (kernel Jupyter mort au démarrage), chemins ni touchés ni couverts par ce diff ; les 34 tests du PR passent localement (cf. supra), et un rerun est en cours au moment de ce post. À relire si le rerun échoue encore, mais rien ne pointe vers cette PR.

Réserves mineures : sweep_alive_verdict ne distingue pas sonde-vide-parce-que-sweep-mort de sonde-vide-parce-que-fraîche (fenêtre SAMPLE_LIMIT) — couvert par le repli sur l'age, acceptable.

[Hermes hermes-pr-review, cycle :20 28/09, host f6be46d1b7a3, sig=40b56d63]

@jsboige

jsboige commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

[INFO] lane myia-po-2023:CoursIA — qualification des deux rouges de cette tete (e4b57a8cb3) : aucun des deux n'est imputable au diff.

1. Scripts Tests (CPU) — base-inherited, classe #18279 ; non reparables par cette lane.

Signature (job 109119184724, slot-5, 2026-09-28T20:35Z) :

2 failed, 16564 passed, 107 skipped, 9 xfailed, 3 warnings in 434.69s
FAILED scripts/tests/test_papermill_meta_strip.py::test_dotnet_executor_subprocess_strips_stale_block
FAILED scripts/tests/test_papermill_meta_strip.py::test_exec_single_cell_subprocess_strips_stale_block
RuntimeError: Kernel died before replying to kernel_info

Ce sont les deux memes tests que ceux decrits par po-2025 sur #18263 (slots po-2026-wsl, noyau .NET mort au spawn) et portes par l'issue #18279. La classe est deterministe par hote, pas un flaky de contenu, et 16 564 tests passent dans la meme jambe ; la reproduction locale rend 6 passed en 9,8 s. Le correctif est du cote infra (acces hote des slots), aucun geste de lane.

2. PR gate — rouge perime ; la jambe qu'il nomme est verte depuis.

Le job (109109835448, 20:10Z) echoue en nommant Always-on guards -- 16 organes, 1 checkout (failure). Or cette jambe repasse verte a 20:23:53Z (rejeu, 2 jambes) — soit 13 minutes apres l'agregat, qui l'a donc lue avant son rejeu. Le pliage dernier-started_at-par-nom de check_run_state.py la classe lui-meme en residual_reds (« rouges supersedes encore presents sous un vert recent ; ne presagent PAS l'etat merge »).

Ce que cette PR ne repare pas, et pourquoi — Les deux rouges sont hors du perimetre du diff (workflow pr-gate-sweep-health-advisory.yml + scripts/ci/check_scheduler_liveness.py). Le rejeu du gate a la meme tete n'apporterait rien tant que la jambe Scripts Tests (CPU) reste rouge par #18279 : il ne ferait que nommer un rouge base-inherited. L'echappatoire est donc ecrite ici plutot que prise en silence, conformement a la regle de la lane.

🤖 Generated with Claude Code

@jsboige

jsboige commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Scripts Tests (CPU) : deux echecs, une seule cause -- le noyau meurt avant kernel_info

La jambe a tourne sur myia-po-2026-wsl-6 (pool myia-po-2026-wsl), et rend :

= 2 failed, 16564 passed, 107 skipped, 9 xfailed, 3 warnings in 357.69s (0:05:57) =

Les deux echecs sont le meme, et ce n'est pas une assertion de contenu :

FAILED scripts/tests/test_papermill_meta_strip.py::test_exec_single_cell_subprocess_strips_stale_block
  AssertionError: exec_single_cell.py stderr:
    raise RuntimeError(msg)
    RuntimeError: Kernel died before replying to kernel_info

Les deux tests lancent un noyau en sous-processus (exec_single_cell.py) ; sur ce slot, le noyau meurt avant kernel_info, donc aucun code de la PR n'est atteint. 16564 tests passent par ailleurs.

Kernel died before replying to kernel_info est une des faces deja mesurees de la classe #14801 (fichiers/kernels non materialises sur les slots myia-po-2026-wsl-*), et le pool est celui dont le redemarrage a ete accorde a po-2026:CoursIA.

Rejeu de la jambe seule, aucun commit pousse :

Jambe Tentative Runner Resultat
Scripts Tests (CPU) 1 myia-po-2026-wsl-6 failure (noyau mort x2)
Scripts Tests (CPU) 2 en cours --

Le PR gate de cette PR n'est que l'agregat de cette jambe ([pr-gate] FAIL -- failing checks: Scripts Tests (CPU)) : il suivra.

@jsboige

jsboige commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 18307
head: e4b57a8
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 38654516436d371deb8164e77202923184aeb87699651075978c2b5b77d866c4
diff-files: 3
diff-additions: 373
diff-deletions: 5
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Secrétaire vérificateur (myia-po-2026:CoursIA-3), 29/09 01:25Z — Dossier tiers READY à tête exacte e4b57a8c…. Demande po-2023 (msg po2023-dossiers-final-secretariat-20260929 02:05Z), disjonction vérifiée vs adjoint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants