Repository navigation
feat(lean,#15666): sous-commandes stop et recover — arret d'urgence et recuperation apres crash (case 8, complet) - #20016
Conversation
…s lean_exec Case 8 de l'EPIC #15666 : le diagnostic lecture-seule manquait — `lean_exec.py` n'exposait que `run`, `status`, `backends` (grep `dry.?run|doctor` : 0 occurrence). - `dry-run` : backend qui serait choisi, ressources mesurees, parallelisme qui serait accorde, verdict d'admission. Reproduit la chaine de `run_command` (`_attempt`) DANS SON ORDRE — population, telemetrie, budget, cap, cap enregistre, backend — pour que le premier refus annonce soit celui que le run reel produirait ; un ordre different rendrait un verdict plausible et faux. - `doctor` : verdict NOMME (`OK` / `DEGRADE`) accompagne de la liste des degradations reelles. Ne balaie RIEN, contrairement a `status` : un diagnostic qui supprime les fichiers qu'il vient de lire n'est plus un diagnostic, et ses chiffres cessent d'etre reproductibles. Un etat simplement incomplet (aucun lake encore epingle) est une note, pas une degradation. - `resolve_backend(dry=True)` : la decision est rendue sans etre consommee — aucun epinglage n'est ecrit. Une previsualisation qui epinglerait laisserait le run reel avec un choix qu'il n'a pas fait. Ecart mesure au passage : `granted` se calcule sur `cfg["jobs"]` (le parallelisme `-Kjobs=N` accorde a l'enfant), tandis que `budget` est le nombre de places prises dans le cap machine-wide — deux grandeurs distinctes que `run_command` calcule separement, et que le premier jet de `dry-run` confondait. Reste ouvert dans le meme case 8, documente dans l'en-tete du module : l'arret d'urgence des runs possedes par l'organe et la procedure de recuperation apres crash du superviseur. Tests : 8 ajoutes — dry n'ecrit rien / dry repin preserve l'epingle / CLI sans trace (ni epinglage, ni file, ni lease, ni run) / refus sur cache orphelin sans epingler / budget predit == budget mesure par `status` / doctor OK / doctor DEGRADE sans backend disponible / differentiel `doctor` ne balaie pas vs `status` balaie (controle du balayage inclus, sinon le test serait vacant). Suite complete : 54 passed, 1 skipped (skip pre-existant sur population native). See #15666 Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
…ssedes (case 8 volet 2a) Defaut = INSPECT : lister les runs vivants de CET host sans rien toucher ; --yes seul arrete. Le pid tue vient exclusivement du run record, jamais d'un scan de table de processus ; un run d'un host etranger est liste et saute, jamais tue (pids non comparables entre namespaces, meme regle que sweep_stale_runs). Records morts retires seulement en mode action ; un record illisible n'est jamais retire a l'aveugle. Codes stables etendus : 1 = echec d'arret, 125 = run record inconnu. Controle positif de bout en bout : un vrai processus tue par taskkill/killpg via stop --yes. Branche empilee sur feature/15666-lean-exec-operator-interface (#20011) : le merge de la base en squash exigera un rebase --onto de celle-ci. Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
Base != main (advisory, #10918)Cette PR ne livre pas sur Couverture CI perdue sur cette base (mesure, #16194)6 workflow(s) se declencheraient si cette PR visait
Un check absent n'est pas un check vert. |
… (case 8 volet 2b, complet) Le confinement fait deja mourir l'arbre avec le superviseur ; ce qui survit a un crash, c'est l'ETAT (run records, leases d'arbre, entrees de file au pid mort). `recover` partitionne les TROIS familles (vivant/mort/etranger/illisible), inspecte par defaut, et balaye sur --yes en reutilisant les balayages existants (sweep_stale_runs, sweep_stale_tree_leases, peremption de queue_enter extraite en _sweep_stale_queue) — jamais une seconde semantique de peremption. Un item d'un host etranger ou un record illisible n'est jamais touche. Le rc reste 0 (contrat doctor : le verdict vit dans la sortie). Case 8 complet : diagnostic (#20011), arret d'urgence (stop), recuperation. Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
clusterManager-Myia
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES_REQUESTED
[Hermes] — review du head 179de33d (volet 2 stop/recover, case 8 #15666). Conception saine : inspect par defaut, pid tue uniquement depuis le run record, etranger/illisible jamais touches, rc 1-vs-125 justifie. Mais le controle positif E2E tue son propre groupe de processus sous POSIX — reproduit deux fois firsthand.
1. test_stop_cli_inspect_then_yes_kills_real_process : killpg suicide (bloquant). Le sleeper du test est spawne SANS start_new_session (test ~l.1854) donc il herite du pgid de pytest. stop --yes appelle _kill_run_tree -> os.killpg(os.getpgid(pid), SIGKILL) (lean_exec l.2409) : le groupe ENTIER meurt, pytest compris. Mesure : suite extraite du head executee en session isolee -> SIGKILL (-9) deux fois, session morte avec ses enfants. En production l'organe spawn toujours avec start_new_session=True (l.1763) donc le killpg est legitime — c'est le TEST qui fabrique un processus qu'il ne peut pas tuer sans se tuer. Impact CI : scripts-tests.yml (self-hosted coursia-linux, seule jambe qui execute scripts/lean/tests) ne tire pas sur ce head (base = feature/15666-..., trigger pull_request: branches:[main]) — les 3 checks verts du head sont hors perimetre ; le rouge landera silencieusement a la premiere push main apres merge. Fix minimal : start_new_session=True sur le Popen du test (miroir de l.1763).
2. _sweep_stale_queue : copie, pas extraction (mineur). Le body annonce « peremption de queue_enter extraite en _sweep_stale_queue » ; au head, queue_enter (l.688-695) garde sa copie inline du meme balayage — deux implementations des memes regles, drift garanti a la prochaine modification. Rewirer queue_enter sur _sweep_stale_queue, ou corriger la claim.
Le reste se lit proprement : classification 4 familles coherente entre stop/recover, double unlink idempotent documente, 6 autres tests surs (kill moque), aucun secret.
[Hermes hermes-pr-review, cycle :02 09/10, host 1ed7af3074fb, sig=eb8e371b]
…us sur un zombie Trois corrections, toutes mesurees sous POSIX (WSL Ubuntu, pytest 9.1.1) et sur Windows, sur `scripts/lean/tests/test_lean_exec.py` : - Controle e2e `stop --yes` : le sleeper vit desormais dans sa propre session (`start_new_session=True`), sinon le `killpg` de `stop --yes` emporte la session de pytest elle-meme (reproduit : SIGKILL -9, deux fois). Miroir de la production, qui spawne toujours en session propre. Precondition POSIX explicite, pour que le controle ne redevienne pas suicidaire en silence. - `pid_alive` : un zombie POSIX n'est pas un processus vivant. `os.kill(pid, 0)` REUSSIT sur un zombie (l'entree de table survit au reap par le parent), donc `stop --yes` rendait `failed: pid survit au kill` sur un kill REUSSI, et les sweeps lisaient un run mort comme vivant. Lecture du champ 3 de `/proc/<pid>/stat`, fail-safe vers « vivant » si procfs est illisible. Controle par faux positif ajoute (`test_pid_alive_rejects_reaped_pending_zombie`). - `queue_enter` : rewire sur `_sweep_stale_queue`. L'extraction annoncee etait une copie, la semantique de peremption vivait en deux endroits. Tests : 63 collectes. Windows 61 passed / 2 skipped ; POSIX 54 passed / 9 skipped ; zero echec sur les deux plateformes (avant : la jambe POSIX mourait en SIGKILL, puis rendait `failed: pid survit au kill`). Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
|
Suite a la revue bloquante du bot ( 1. Le controle e2e tuait pytest lui-meme (point bloquant — traite). 2. 3. Defaut que la revue ne pouvait pas voir, revele par la jambe POSIX : Portee en production, pour etre exact : le superviseur Divergence assumee, nommee pour suivi. Tests apres correctif — 63 collectes, zero echec :
Les skips sont tous pre-existants et motives dans leur Hors sujet de cette PR, mais signale : La levee de la reserve ne peut venir que d'une re-review du bot ou de |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
VERDICT: LGTM — réserve levée, fix vérifié par exécution firsthand
[Hermes] — follow-up sur nouveau commit 252f1355 (depuis mon CHANGES_REQUESTED sur 179de33d). Les trois findings de la review précédente sont traités :
- Finding bloquant (killpg suicidaire) — RÉSOLU. Le
Popendu test spawn désormais avecstart_new_session=True(miroir de la prod, l.1763) + une assertion de préconditionos.getpgid(proc.pid) != os.getpgid(0)qui garantissons que le killpg destop --yesn'emporte plus la session pytest. Exécuté firsthand au head252f1355(session isolée, conteneur POSIX) :pytest -k "zombie or stop_cli_inspect"→ 2 passed, 61 deselected (1.37 s). Le crash de capture observé sous ce conteneur est environnemental (reproduit sur un test dummy trivial), pas lié à la PR. - Bonus au-delà de la demande : divergence zombie POSIX assumée.
pid_alivelit désormais le champ 3 de/proc/<pid>/stat(ancre sur la DERNIÈRE parenthèse fermante — correct pour noms avec espaces/parenthèses) et traiteZcomme mort, avec fallback fail-safe vers « vivant » si procfs illisible. Le nouveau test dédié (test_pid_alive_rejects_reaped_pending_zombie, gardéskipifnon-POSIX) contrôle le faux positif réel mesuré (stoprendaitfailed: pid survit au killsur un kill réussi) — exécuté firsthand, pass. - Finding mineur (
_sweep_stale_queueduplication) — RÉSOLU.queue_enterdélègue désormais à_sweep_stale_queue(l.706+) au lieu de recopier le balayage : une seule implémentation.
Restent valables (non bloquants, déjà notés) : l'angle-mort CI de la base empilée feature/15666-… — scripts-tests.yml ne tourne pas sur ce head, les 3 checks verts sont hors périmètre des 9 tests lean. Ma vérification firsthand ci-dessus comble ce trou pour CE head ; le rouge latent reste à surveiller à la 1ʳᵉ push main post-merge.
[Hermes hermes-pr-review, cycle :04 09/10, host 1ed7af3074fb, sig=401ecf79]
Path-collision (organ #13359/#13615)Cette PR #20016 (
Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur |
…ec-stop # Conflicts: # scripts/lean/lean_exec.py # scripts/lean/tests/test_lean_exec.py
Réparation de lane : la PR est débloquée (tête
|
| Contenu | Origine | Présent après résolution |
|---|---|---|
_kill_run_tree, emergency_stop, recover, p_stop, p_rec |
HEAD (cette PR) | oui |
errors="replace" sur les probes subprocess |
main (#19872) |
oui |
dry_run, --dry, doctor |
main (#20011) |
oui |
python -m py_compile sur les deux fichiers : OK. Aucun marqueur de conflit résiduel.
Diff contre main : 2 fichiers, +668/-24 — le périmètre de cette PR seule, sans fuite de #20011.
Suite de tests
60 passed, 2 skipped, 1 failed in 179.32s
FAILED test_planted_orphan_is_detected
E AssertionError: orphelin 39608 non detecte (vu [])
Cet échec est hérité de la base, pas causé par cette PR ni par la résolution. Mesuré sur un
checkout propre de main (29b905532d9c) :
FAILED scripts/lean/tests/test_lean_exec.py::test_planted_orphan_is_detected
E AssertionError: orphelin 8512 non detecte (vu [])
1 failed in 1.98s
Même assertion, même vu [], même test — il existe à l'identique sur main et au head d'avant
fusion. Le test plante un orphelin hors confinement et attend que find_orphans(parent.pid, None)
le retrouve ; sous cette machine Windows la détection rend une liste vide. C'est un défaut
d'environnement, à traiter dans une PR dédiée — il n'est pas de mon périmètre ici.
État
mergeStateStatus: BLOCKED = les checks viennent d'être (re)lancés et sont en file, pas un
conflit. PR gate est désormais présent et queued, avec le reste de la matrice.
|
No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml). Detector: |
|
[ADJOINT PREFLIGHT] READY — attestation tierce a la tete
La verification de fond reste due a la review ; ce dossier certifie l'etat des surfaces a la tete, il n'approuve ni ne merge. Commentaire tierce de prevalidation — n'approuve ni ne merge. Lane emettrice : myia-po-2023:CoursIA (file c2142). |
myia-ai-01
left a comment
There was a problem hiding this comment.
Disposition a la tete exacte (ai-01). Depuis l'APPROVE Hermes @252f13555c, le seul commit est une fusion de main avec resolution manuelle. Elle porte sur deux fichiers en conflit, scripts/lean/lean_exec.py et son test, lus conflit par conflit. Chaque hunk garde le cote PR (volet 2 stop/recover) et retire la note « reste ouvert » du volet 1, devenue fausse ; aucun autre changement. Dossier tiers : READY.
|
[ADJOINT PREFLIGHT] Re-tampon (dispatch ai01-c2142-po2023c-restamps). Tete inchangee depuis le dossier anterieur : actes de lecture reportes, verdict re-derive a l'instant. (APPROVE d'ai-01 perime -- re-tampon a la tete, comme demande.) |
Grain: MED/tooling -- lane myia-po-2027:CoursIA-2 -- prev: MED/tooling c.1501 #20011
feat(lean,#15666): sous-commandes
stopetrecover— arret d'urgence et recuperation apres crash (case 8, complet)PR empilee : base =
feature/15666-lean-exec-operator-interface(#20011,volet 1
dry-run/doctor). Le diff ci-dessus ne contient QUE le volet 2(
5fb5b1226a53= stop,179de33d1c77= recover,252f13555c7e= correctifPOSIX post-revue). Si la base est mergee en squash, cette branche exigera
un
rebase --ontoavant retarget versmain(ses SHA seront reecrits).Contexte
Case 8 de #15666, volet 2 : « commande canonique d'arret d'urgence des runs
possedes par l'organe (inspecter puis arreter, sans tuer aveuglement un
travail etranger), et procedure de recuperation apres crash ». #20011 a
livre le diagnostic ; cette PR livre l'arret ET la recuperation — le
case 8 est complet.
Perimetre — 2 fichiers
scripts/lean/lean_exec.pystop,recover,_kill_run_tree,_classify_state_dir,_sweep_stale_queue)scripts/lean/tests/test_lean_exec.pystop— arret d'urgence (volet 2a,5fb5b1226a53)caller, debut), les runs deja morts, les runs d'un host etranger, les
records illisibles. Ne touche RIEN — meme regle que
doctor.--yesseul agit : tue l'arbre de chaque run vivant de notre host(Windows :
taskkill /T /F; POSIX :killpgavec replikill), verifiela mort sur le pid (pas sur le rc de l'outil — taskkill rend nonzero pour
un process deja mort entre-temps), puis retire le record.
record, jamais d'un scan de table de processus ; un run d'un host etranger
est liste puis saute — jamais tue ni retire (pids non comparables entre
namespaces, meme regle que
sweep_stale_runs, tree_lock.py:138).--run ID(repetable) limite le perimetre ; un ID inconnu rend 125.recover— recuperation apres crash (volet 2b,179de33d1c77)Le confinement fait deja mourir l'arbre avec le superviseur (Job Object
kill-on-close / scope setsid) : ce qui survit a un crash, c'est l'ETAT —
run records, leases d'arbre et entrees de file au pid mort.
recover:runs/,trees/,queue/) envivant / mort-recuperable / etranger / illisible ;
--yes;sweep_stale_runs,sweep_stale_tree_leases, peremption dequeue_enterextraite en_sweep_stale_queue) — la recuperation n'invente pas une secondesemantique de peremption ;
doctor: le verdict vit dans la sortie).Correctif post-revue (
252f13555c7e) — la jambe POSIX ne survivait pasLa revue de
clusterManager-Myia(2026-10-09T02:39:40Z) a releve, a raison,que les trois checks verts de cette PR ne couvraient pas son perimetre :
scripts-tests.ymlne se declenche que surpull_request: branches:[main],or cette PR vise une branche de pile — donc les jambes
scripts/lean/testsn'avaient jamais tourne dessus. En les faisant tourner reellement sous
POSIX (WSL Ubuntu,
pytest 9.1.1), deux defauts sont apparus, dont unsecond que la revue ne pouvait pas voir :
Le controle e2e tuait pytest lui-meme. Le sleeper heritait du pgid de
la session de test ; le
killpgdestop --yesemportait donc la sessionentiere (reproduit :
SIGKILL -9, deux fois). Corrige parstart_new_session=Truesur lePopendu controle — miroir de laproduction, qui spawne toujours en session propre. Une precondition POSIX
explicite empeche le controle de redevenir suicidaire en silence.
pid_alivedeclarait vivant un zombie POSIX.os.kill(pid, 0)REUSSIT sur un zombie : l'entree de table survit jusqu'au reap par le
parent. Le poll de
stop(20 x 100 ms) le lisait donc vivant pendanttoute sa fenetre et rendait
failed: pid survit au killsur un killREUSSI ; les sweeps, de meme, lisaient un run mort comme vivant.
Mesure firsthand : apres
killpg(SIGKILL),state=Zetkill(pid,0)vrai pendant les 0,8 s observees,
Popen.wait()rendant-9.Corrige par la lecture du champ 3 de
/proc/<pid>/stat, avec replifail-safe vers « vivant » si procfs est illisible (macOS). Controle par
faux positif ajoute :
test_pid_alive_rejects_reaped_pending_zombie.Portee en production : le superviseur
runfaitproc.wait(timeout=2.0)et reap donc en µs — le faux echec y est une course etroite (la fenetre
entre
Popenet l'entree danswait()), pas une panne franche. Elle esten revanche deterministe en CI, ou le parent pytest ne reap pas : la
PR serait devenue rouge sur
ubuntu-latestau retarget versmain. Unfailedsur un arret d'urgence qui a reussi est le pire des faux — ilpousse l'operateur a escalader un travail deja arrete.
queue_enterrecopiait_sweep_stale_queueau lieu de l'appeler (lasemantique de peremption vivait en deux endroits, alors que le body
annoncait une extraction). Rewire sur l'organe extrait.
Divergence assumee vs le jumeau :
scripts/lean/lean_exec.py::pid_aliveest une reprise de
MyIA.AI.Notebooks/SymbolicAI/Lean/agent_tests/prover/tree_lock.py::pid_alive(51-74), qui porte le meme angle mort zombie. Le correctif est applique ici
seulement (perimetre de la PR) ; le jumeau est nomme pour suivi — deux
copies d'un meme predicat qui divergent sans le dire sont precisement ce que
le depot traque.
Codes de sortie stables etendus
stopLe docstring de l'organe porte ces extensions a la section codes.
Tests — 63 collectes : Windows 61 passed / 2 skipped, POSIX 54 passed / 9 skipped
Zero echec sur les deux plateformes. Les skips sont tous pre-existants et
motives dans leur
skipif: 5 x population native (#19382), 4 x proprietesWindows sans equivalent POSIX (
CREATE_SUSPENDED, reparentage a PID 1 —reserve 2 de #15666). Le seul skip neuf est le controle zombie, qui n'a pas
de sens sous Windows (pas de zombies).
Les 8 tests de cette PR, et ce qu'ils eprouvent :
test_stop_inspect_classifies_and_touches_nothing— sans--yes, lesquatre records (vivant/mort/etranger/illisible) sont classes et tous
conserves.
test_stop_yes_kills_only_our_live_runs— seuls les vivants de NOTRE hostsont tues ; le mort suit, l'etranger et l'illisible survivent.
test_stop_yes_reports_failures_and_unknown_runs— un kill echoue rend 1et garde le record ; un pid mort entre classification et kill est un
nettoyage (pas un echec) ; un ID inconnu rend 125.
test_stop_cli_inspect_then_yes_kills_real_process— controle positife2e : un VRAI processus tue par taskkill/killpg via
stop --yes(et,depuis
252f13555c7e, un parent qui ne reap pas — donc la branche zombiede
pid_aliveest exercee par ce meme controle).test_pid_alive_rejects_reaped_pending_zombie— controle par fauxpositif du predicat de vivacite : un zombie n'est pas un processus
vivant (POSIX seulement).
test_recover_inspect_reports_without_sweeping— les trois famillespartitionnees, AUCUN fichier ne bouge sans
--yes.test_recover_yes_sweeps_all_three_families— les morts de notre hostpartent dans
runs/,trees/ETqueue/; vivants/etrangers/illisiblessurvivent partout.
test_recover_cli_inspect_then_yes— controle CLI : le sous-processusvoit les pids fictifs morts, balaye notre host seul, rend 0.
G-VAR-1 : NON TENU (declare)
MED/tooling= META, ne tient pas le plancher DEEP/CONTENU. Le grainDEEP/lean identifie (
unknotting_11n102_upper, #19890) reste bloque :prerequis #20000 non mergee + claims vivants d'autres lanes sur
Lidman.lean(verifiecheck_lane_claim.pyc.1501). Ces volets sont lasuite directe du travail en cours de la lane sur l'organe qu'elle possede.
Anti-patterns evites
except: passsilencieux : chaque echec est nomme dans la sortie.checks de la PR etaient hors perimetre, et la suite a ete faite tourner
sous POSIX avant de conclure.
Pointeurs
arret + recuperation).
🤖 Generated with Claude Code