Skip to content

fix(guard,#15278): reprise du nom de conteneur, journal nomme sur rc!=0 - #15860

Merged
myia-ai-01 merged 2 commits into
mainfrom
fix/15278-supervise-orphan-name
Sep 13, 2026
Merged

myia-ai-01 merged 2 commits into
mainfrom
fix/15278-supervise-orphan-name

Conversation

@jsboige

@jsboige jsboige commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2024:CoursIA — prev: MED/notebook-lean #15803

See #15278

Le defaut

Le nom d'un conteneur est un verrou global au daemon, pas au client. Un systemctl restart du superviseur tue le client docker run mais pas le conteneur, qui garde son nom jusqu'a la fin de son job. La generation suivante relance donc sous un nom deja pris et boucle en rc=125 — mesure ai-01 du 2026-09-09 pendant le passage de 6 a 10 slots (#15278) : 3 slots sur 10 bloques, ~4 minutes.

Le seul message qui explique la cause (« Conflict. The container name ... is already in use ») part sur la redirection du docker run, donc dans $STATE_DIR/<nom>.log — et le journal ne nommait aucun fichier. L'operateur lisait rc=125 et n'etait oriente vers rien.

Ce que fait le garde

reclaim_container_name() est appele juste avant docker run, dans slot_loop et waiter_loop (un waiter orphelin detient son nom de la meme facon).

Trois proprietes, dans cet ordre d'importance :

  1. Il attend la liberation en sondant (docker inspect -f '{{.State.Status}}', intervalle COURSIA_RUNNER_NAME_RECLAIM_POLL_SECS, defaut 5 s). La reprise se fait dans la seconde qui suit la liberation, au lieu d'attendre la prochaine marche du backoff (BACKOFF_BASE -> BACKOFF_CAP, 300 s). C'est ce « au-dela de la duree du job orphelin » que l'acceptance ferme : la borne devient la duree du job orphelin + un intervalle de sondage.
  2. Il ne retire un conteneur que si son etat est PROUVE non en cours — allowlist created / exited / dead. docker inspect et non docker ps : le second ne voit pas un conteneur arrete, precisement le residu qu'on veut pouvoir retirer sans risque.
  3. Il n'abandonne pas et ne derive pas de nom de generation. Un second conteneur pour le meme slot doublerait la reservation CPU de ce slot, contre la clause de tete du fichier (« l'hote prime sur la CI »). Un job orphelin qui ne rend jamais son nom est un probleme docker, pas un probleme de boucle : on le dit periodiquement, avec l'etat observe et le nom du journal, pour que l'operateur puisse agir. stop reste prioritaire sur l'attente.

Le defaut 2 de l'issue est traite dans le meme geste : sur rc != 0, cycle_backoff suffixe le message d'un -- voir $STATE_DIR/<nom>.log.

Critere d'acceptation 3 — pourquoi la solution ne peut pas tuer un job legitime

L'argument tient a la forme du predicat, pas a la prudence de l'auteur.

Le retrait est borne par une allowlist (created|exited|dead), pas par une liste d'exclusion. La difference est le sens de l'erreur :

| Etat observe | Allowlist (cette PR) | Liste d'exclusion (running|paused puis * -> rm) |
|---|---|---|
| running, paused | attend | attend |
| created, exited, dead | retire (aucun job dedans) | retire |
| restarting, removing, etat inconnu d'une version future de docker | attend (fail-closed) | retire — tue un job |

Avec une liste d'exclusion, tout etat qu'on n'a pas anticipe autorise une destruction. Avec l'allowlist, il faut avoir nomme l'etat pour que la destruction soit permise : l'inconnu ne detruit rien. C'est le seul cote ou se tromper tue un travail legitime, et un runner ephemere qui travaille est en etat running par definition.

Controle de non-regression : le test 33 verifie que docker rm n'est jamais appele sur un nom detenu par un conteneur en cours (le job legitime survit), et le test 37 verifie la meme propriete sur un etat non qualifie (restarting) — la ou l'allowlist et la liste d'exclusion divergent. Le test 34 est le controle positif : sur un etat prouve mort (exited), le residu est retire, une fois, et le log le dit. Sans 34, un garde qui n'appelle jamais rm passerait 33 et 37.

L'acceptance, critere par critere

Critere de #15278 Reponse
Aucun slot bloque au-dela de la duree du job orphelin (ou borne documentee) Premiere branche tenue. La reprise survient des que docker inspect ne rend plus d'etat, soit au plus un intervalle de sondage (5 s) apres la liberation. La borne est aussi documentee en tete de la fonction.
Un rc != 0 nomme le fichier ou lire la cause Le message porte -- voir $STATE_DIR/<nom>.log. Le controle negatif (test 36) verifie qu'un cycle rc=0 ne porte pas ce rappel : la mention reste reservee aux echecs, elle ne devient pas decorative.
La solution ne peut pas tuer un conteneur executant un job legitime — argumente Section precedente : allowlist fail-closed, plus les tests 33 et 37.

Validation

Suite test_supervise_guards.sh — 81 PASS / 0 FAIL, rc=0, sur les deux plateformes :

Plateforme Resultat Note
Linux / WSL (la plateforme sur laquelle la CI execute la suite) 81 PASS / 0 FAIL, rc=0 test 25 a 70/70
MSYS / Git Bash (poste Windows) 81 PASS / 0 FAIL, rc=0 test 25 a 70/70 sur ce run

Le test 25 est un flake de cadencement hote preexistant (budget de 70 cycles en 60 s ; sur MSYS chaque spawn de stub coute ~200 ms). Il a deja ete mesure en echec sur origin/main pristine (63/70), donc sans rapport avec cette PR — et il passe sur les deux plateformes au run de validation ci-dessus.

Tests ajoutes : 33, 34, 35, 36, 37. Diff du fichier de test = 411 insertions, 0 suppression, un seul hunk contigu — aucun test existant n'est touche, aucun n'est desactive.

Diff total : 2 fichiers, +531 / -10.

Invariants verifies dans le code :

  • l'appel a reclaim_container_name est place avant local t0=$SECONDS, pour que l'attente ne soit pas comptee dans lifetime — une attente de plusieurs minutes suivie d'un echec instantane se lirait sinon comme un « cycle long », donc « pas un emballement », et masquerait l'echec reel ;
  • les deux nouvelles bornes passent _validate_backoff_value (entier decimal, <= 18 chiffres, >= 1). Le parametre ref de cette fonction etait fige sur #15166 : il est generalise pour que ces deux bornes soient attribuees a #15278 ;
  • STOP_FILE est teste a chaque tour de sondage : un stop demande pendant l'attente rend la main immediatement (continue sur return 1).

Hors scope

  • Le cas « le job orphelin ne rend JAMAIS son nom » (conteneur docker reellement bloque) reste un probleme docker : le garde le signale periodiquement avec l'etat et le journal, il ne le resout pas — c'est exactement le comportement que l'acceptance autorise en seconde branche.
  • Le rc=125 lui-meme n'est pas supprime : il est rendu diagnosticable. Le conflit de nom ne devrait plus se produire, mais s'il se produit encore pour une autre raison, le journal est nomme.

🤖 Generated with Claude Code

jsboige and others added 2 commits September 13, 2026 00:19
…l sur rc!=0

Le nom d'un conteneur est un verrou GLOBAL au daemon, pas au client : un
`systemctl restart` tue le client `docker run` mais pas le conteneur, qui garde
son nom jusqu'a la fin de son job. La generation suivante relancait sous un nom
deja pris et bouclait en rc=125 (mesure ai-01 2026-09-09, 3 slots sur 10, ~4 min),
et le seul message utile -- « Conflict. The container name ... is already in
use » -- vivait dans le log du slot, que le journal ne nommait pas.

- `reclaim_container_name` : attend la liberation du nom en sondant (reprise
  immediate au lieu d'attendre la marche suivante du backoff), avec une
  ALLOWLIST fail-closed -- seuls `created`/`exited`/`dead` autorisent un
  `docker rm -f`, tout etat non qualifie est traite comme DETENU. C'est le seul
  cote ou se tromper tue un job legitime, que l'acceptance interdit ;
- pas de derive de nom de generation : un second conteneur pour le meme slot
  doublerait sa reservation CPU, contre la clause de tete « l'hote prime sur
  la CI » ;
- `cycle_backoff` : sur `rc != 0`, le message nomme le journal du slot ;
- les deux nouvelles bornes passent la validation fail-closed, avec l'attribution
  #15278 (le parametre `ref` de `_validate_backoff_value` etait fige sur #15166) ;
- l'attente est placee AVANT `local t0=$SECONDS` : une attente de plusieurs
  minutes suivie d'un echec instantane se lirait sinon comme un cycle long,
  donc « pas un emballement », et masquerait l'echec reel ;
- tests 33-37 : conteneur en cours jamais retire, residu retire, etat non
  qualifie jamais retire (fail-closed), `rc != 0` nomme le journal, et le
  controle negatif `rc = 0` ne le nomme pas.

Suite `test_supervise_guards.sh` : 81 PASS / 0 FAIL, rc=0, sur Linux/WSL
(plateforme CI) et sur MSYS.

See #15278

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le commentaire de tete annoncait « jusqu'a 82 s de plus que le job orphelin ».
Ce chiffre n'est adosse a aucune mesure reproductible : il est remplace par la
borne structurelle, verifiable dans le bloc BORNES (`BACKOFF_BASE` -> `BACKOFF_CAP`
a 300 s), qui est ce que le garde economise reellement.

See #15278

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

jsboige commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Justification --ignore-red (exigee par le picker) — meme mesure que sur #15818.

PR gate = DWELL, pas un defaut de code. Annotation du check-run : DWELL -- plancher 120 min, leve au premier balayage suivant 2026-09-13T00:20:42Z. Le plancher n'est pas encore ecoule — etat nominal d'attente.

Aucun geste pose : gh pr update-branch remettrait les 120 min a zero (la date lue est celle du committer), ce qui eloignerait le merge. Aucun merge-dwell-waived.

Rappel : ce grain est un MED/guard (genre META) — il ne tient pas le plancher G-VAR-1, declare comme tel au rapport de cycle.

@github-actions

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #15860 (fix(guard,#15278): reprise du nom de conteneur, journal nomme sur rc!=0) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur main. L'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux livraisons, donc il ne conclut PAS a une redondance (#15768) : deux PRs peuvent toucher le meme fichier pour des raisons disjointes. L'arbitrage reste a la lane ou au coordinateur.

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Arbitrage de la path-collision : mesure de CONTENU, pas de chemins

L'organe (#13359/#13615) rend un verdict terminal et dit lui-meme ce qu'il ne fait pas :
« l'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux
livraisons [...] L'arbitrage reste a la lane ou au coordinateur. » Je le rends ici avant de
merger, plutot que de traiter son caractere advisory comme une dispense.

Ce que les deux PRs deja sur main ont livre sur ces memes chemins :

PR mergee substance
#15166 2026-09-10T23:03:20Z borner le superviseur quand Docker est indisponible
#15393 2026-09-11T08:36:37Z deriver les 3 prefixes de nom de runner depuis le hostname

Ce que cette PR-ci apporte, mesure contre main :

symbole introduit supervise.sh sur main test_supervise_guards.sh sur main
container_state 0 0
reclaim_container_name 0 0

Controle positif de l'instrument : les deux fichiers existent bien sur main
(1085 et 1503 lignes) — les zeros ci-dessus sont des absences mesurees, pas un grep
sur un chemin vide.

Deux corroborations independantes :

  • test_supervise_guards.sh est +411/-0 : addition pure, aucune ligne existante
    reecrite — une double-livraison se verrait en suppressions/modifications.
  • supervise.sh est +121/-10, et les 10 retraits portent sur la reprise du nom de
    conteneur, sujet absent des deux livraisons anterieures.

Verdict : substances disjointes, pas de double-livraison. Le recouvrement est reel sur
les chemins et nul sur le contenu. Le verdict terminal de l'organe est exact et
n'implique pas la redondance (#15768) — c'est precisement le cas qu'il decrit.

Tag : MED/guard — lane myia-po-2024:CoursIA tenu tel quel. guard est le bon genre
(le geste peut rougir), et MED tient : +411 lignes de tests neufs sur un chemin de reprise
de nom qui n'existait pas. Classe META, donc ne tient pas le plancher G-VAR-1 de contenu
du cycle de la lane — consequence a connaitre, et defaut de provisionnement qui m'appartient.
Cap sur le jour frais 2026-09-13 : cap_reached: false, budget 1, spent 0.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants