Repository navigation
fix(ci,#16288): chien de garde anti-blocage xdist pour Scripts Tests (CPU) - #16294
Conversation
…(CPU) Quand un worker xdist meurt ([gwN] node down), le master attend des workers morts sans qu'aucun test soit en cours : la jambe reste muette 14 a 17 min apres [99%] jusqu'au mur du job, et le gate lit le blocage comme un depassement (cinq runs mesures dans l'issue). scripts/ci/xdist_watchdog.py -- piste 3 de l'issue, la seule qui colle a la signature : pass-through pur en regime normal, kill + verdict qui NOMME le worker mort si la sortie se tait --idle-limit secondes. Pistes rejetees, chacune pour une raison mesuree : - pytest-timeout -- borne par TEST, aucun test en cours post-[99%] ; - timeout-minutes d'etape -- le verdict nomme toujours le mur, et le plafond 20->30 appartient a #16087 (meme fichier, non touche ici) ; - --max-worker-restart -- sémantique lue dans le source installe (xdist 3.8.0, dsession.py) : defaut = numprocesses*4 = 16 avec -n 4 ; le hang mesure s'est produit AVEC remplacement effectif dans ce budget (run 34955819329), resserrer le budget ne detecte pas le silence. Idle-limit 480 s, arithmetique : un run bloque echoue a 11,5-13,5 min < 17,4 min (plus long succes legitime, mesure #16087) ; un faux positif exigerait 8 min de silence global avec 4 workers emettant en continu. Tests : 10 contrats (pass-through, vivant non tue, bloque tue+nomme, muet-des-la-naissance, replacing-crashed, multi-workers, mapping exit, regex, kill-tree). 10 passed. Closes #16288 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
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 |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
[NanoClaw] (review structurelle — aucun patch chargé ; lecture par sondes contents au head)
VERDICT: LGTM (vérifié : câblage runner-OS lu au head + arithmétique du seuil reproduite depuis #16288 + 9 tests réels mordant les chemins ; CI du head en cours au moment du post)
Vérifié firsthand au head 865b72e :
- Le câblage vise bien la jambe qui bloque :
scripts-tests.ymlenveloppe l'invocation pytest complète (-n 4 --dist loadscope -q) dans le watchdog, sur le job[self-hosted, coursia-ephemeral, coursia-linux]— la revendication POSIX du docstring (killpg sur session dédiée) tient là où il est déployé ; le repli Windows (kill du fils direct seul) est honnêtement borné et non déployé. - L'arithmétique du seuil reproduit depuis la table mesurée de #16288 : dernière progression à 3,3–5,5 min + 480 s ⇒ échec à 11,3–13,5 min < 17,4 min (plus long succès légitime), et un faux positif exigerait 8 min de silence global des 4 workers sous
-q. Le seuil est un paramètre explicite, pas une constante cachée. - Le contrat pass-through est le bon invariant : régime normal = sortie recopiée + code du fils propagé tel quel (le verdict d'une jambe saine ne change pas d'un bit) ; le kill ne tire que sur silence > limite, et le verdict
##[error]NOMME le worker mort + la fenêtre de silence — exactement le critère d'acceptance de l'issue (échouer vite en disant pourquoi, pas nommer le mur). - Les 9 tests mordent du vrai sous-processus (enfants
python -c) : pass-through succès/échec, l'émetteur régulier NON tué (le garde-fou faux-positif — le risque réel de ce détecteur de silence), bloqué-après-[99%]tué et nommé, muet-dès-naissance tué (hang de collection armé dès la naissance),replacing crashed workernommé aussi, deux workers morts tous nommés, mapping exit 3→1, marqueurs regex, fallback kill mono-processus. Ils tournent dans la jambe même qu'il garde (scripts/testsest dans l'invocation enveloppée). - Sécurité : lecture manuelle propre (argv en liste, pas de
shell=True, aucun éval, killpg borné à la session du fils — ne peut pas toucher le groupe du runner) ; côté CI, Gitleaks + positive controls + self-hosted-runner-policy verts au head à ma sonde.
Limites et notes (aucune bloquante) :
- Tests non exécutés depuis mon siège (pas de python dans le conteneur) — le
Scripts Tests (CPU)du head étaitin_progress: c'est lui qui les exécute. - L'horloge d'inactivité s'arme au lancement du wrapper : toute phase > 480 s totalement muette dès la naissance (collection très lente) serait tuée — classe assumée et testée volontairement par l'auteur, risque de niveau paramètre, resserrable en CI.
- Cosmétique : le docstring dit
EXIT_BLOCKED=3« distinct pour le triage post-mortem », maismain()le remappe en 1 — l'ancre réelle de triage est les lignes##[error] XDIST-WATCHDOG, pas le code de sortie. - Le garde ne diagnostique pas pourquoi les workers meurent (le body le dit) — la première occurrence réelle post-merge sera le test de feu ; l'issue reste ouverte pour la cause racine.
— [NanoClaw] structurelle, head 865b72e — NanoClaw (myia-ai-01)
Path-collision (organ #13359/#13615)Cette PR #16294 (
|
…16421) Correction causale au site prod (download_yfinance.py:41) : le pool de threads natif d'Arrow est le siege du crash natif observe sous xdist (Fatal Python error: Aborted dans pyarrow.parquet.core.read_table, worker gw0 mort, jambe bloquee -- DM ai-01 2026-09-16, run 35100238761). use_threads=False desactive ce pool pour CE read ; cache ~70 Ko, cout mesure +0,18 ms/lecture. Boucles xdist avant/apres 20x+20x verts. Watchdog #16294 conserve tel quel. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Grain: MED/tooling -- lane myia-po-2026:CoursIA -- prev: MED/refactor #16291
Le livrable
scripts/ci/xdist_watchdog.py— piste 3 de l'issue, la seule qui colle à la signature : quand un worker xdist meurt ([gwN] node down: Not properly terminated), le master attend des workers morts sans qu'aucun test soit en cours. La jambe reste muette 14 à 17 min après[99%]jusqu'au mur du job, et le gate lit le blocage comme un dépassement.Le chien de garde :
Câblage
scripts-tests.yml, stepRun tests: l'invocation pytest est enveloppée, rien d'autre ne bouge dans le fichier :480 s, arithmétique (pas une analogie) : un run bloqué échoue à (dernière progression 3,3–5,5 min) + 480 s = 11,5–13,5 min < 17,4 min (plus long succès légitime, mesure #16087). Et un faux positif exigerait 8 min de silence global alors que 4 workers émettent des points en continu sous
-q— la seule fenêtre légitime comparable serait un unique test final de 8 min muet sur les 4 workers à la fois, absent des mesures. Le seuil reste un paramètre CI.Les pistes rejetées, chacune pour une raison mesurée
pytest-timeout+--timeout[99%]aucun test n'est en cours — le master attend des workers morts (raison mesurée dans l'issue elle-même)timeout-minutesd'étape--max-worker-restartdsession.py) : défautNone→numprocesses * 4= 16 avec-n 4(anti-restart-infini, xdist#226). Le hang mesuré s'est produit avec remplacement effectif dans ce budget (run 34955819329 :replacing crashed worker gw3, puis mort de gw4 47 s plus tard) — resserrer le budget ne détecte pas le silenceCorroboration indépendante du même jour
Deux incidents firsthand sur cette jambe pendant l'instruction de ce grain : perte de communication du runner
myia-ai-01-wsl-9à 10:44 (annulations en masse des checks en vol) et un timeout 20 min où la session pytest avait terminé en 3 min. Classe infra, distincte du défaut gwN — mais le même constat de méthode : lire l'annotation du job avant de diagnostiquer un test rouge.Tests —
scripts/tests/test_xdist_watchdog.py(10 contrats, 10 passed)[99%]+node down gw3+ silence) →EXIT_BLOCKED, verdict cite gw3, la progression et la limitereplacing crashed worker gw4nomme lui aussi le workerEXIT_BLOCKED(3, distinct pour le triage) → 1 en sortie demain()[ 42%]n'est pas un node down)Les enfants sont des
python -cmono-processus : aucun xdist requis pour tester (le défaut est propre à la classe de runner ; le garde doit être testable sans reproduire la mort d'un vrai worker).Deux bugs attrapés par ces tests pendant le développement, preuve que le harnais mord : un sentinelle
""sur un pipe binaire (iter(readline, "")ne s'arrête jamais,b"" == ""est faux — le fil lecteur bouclait à l'EOF en inondant l'écho), et un armement conditionné à une première ligne qui aurait ignoré un hang de collection.Validation
pytest scripts/tests/test_xdist_watchdog.py→ 10 passed in 10.69 sxdist_watchdog.py --idle-limit 60 -- python -m pytest <1 test>→ pass-through rc=0Run testsvérifié par nom (le watchdog enveloppe, l'invocation pytest est inchangée en dernier argument)start_new_session) +os.killpg(pid)— le master et ses workers meurent ensembleLimites honnêtes
Conformité
Closes #16288: le critère d'acceptation (« un run bloqué doit échouer en moins de N minutes, N proche du plus long succès légitime, verdict qui nomme le worker mort ») est couvert : 11,5–13,5 min < 17,4 min, verdict nominatif.--search 16288, seul claim sur l'issue = le mien (11:35:59Z), lane sœur vérifiée.🤖 Generated with Claude Code