Repository navigation
fix(guard,#15278): reprise du nom de conteneur, journal nomme sur rc!=0 - #15860
Conversation
…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>
|
Justification
Aucun geste pose : Rappel : ce grain est un |
Path-collision (organ #13359/#13615)Cette PR #15860 (
Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur |
Arbitrage de la path-collision : mesure de CONTENU, pas de cheminsL'organe (#13359/#13615) rend un verdict terminal et dit lui-meme ce qu'il ne fait pas : Ce que les deux PRs deja sur
Ce que cette PR-ci apporte, mesure contre
Controle positif de l'instrument : les deux fichiers existent bien sur Deux corroborations independantes :
Verdict : substances disjointes, pas de double-livraison. Le recouvrement est reel sur Tag : |
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 restartdu superviseur tue le clientdocker runmais 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 enrc=125— mesureai-01du 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 dudocker run, donc dans$STATE_DIR/<nom>.log— et le journal ne nommait aucun fichier. L'operateur lisaitrc=125et n'etait oriente vers rien.Ce que fait le garde
reclaim_container_name()est appele juste avantdocker run, dansslot_loopetwaiter_loop(un waiter orphelin detient son nom de la meme facon).Trois proprietes, dans cet ordre d'importance :
docker inspect -f '{{.State.Status}}', intervalleCOURSIA_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.created/exited/dead.docker inspectet nondocker ps: le second ne voit pas un conteneur arrete, precisement le residu qu'on veut pouvoir retirer sans risque.stopreste prioritaire sur l'attente.Le defaut 2 de l'issue est traite dans le meme geste : sur
rc != 0,cycle_backoffsuffixe 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|pausedpuis*-> 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
runningpar definition.Controle de non-regression : le test 33 verifie que
docker rmn'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 jamaisrmpasserait 33 et 37.L'acceptance, critere par critere
docker inspectne 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.rc != 0nomme le fichier ou lire la cause-- voir $STATE_DIR/<nom>.log. Le controle negatif (test 36) verifie qu'un cyclerc=0ne porte pas ce rappel : la mention reste reservee aux echecs, elle ne devient pas decorative.Validation
Suite
test_supervise_guards.sh— 81 PASS / 0 FAIL,rc=0, sur les deux plateformes :rc=0rc=0Le 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/mainpristine (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 :
reclaim_container_nameest place avantlocal t0=$SECONDS, pour que l'attente ne soit pas comptee danslifetime— 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 ;_validate_backoff_value(entier decimal, <= 18 chiffres, >= 1). Le parametrerefde cette fonction etait fige sur#15166: il est generalise pour que ces deux bornes soient attribuees a#15278;STOP_FILEest teste a chaque tour de sondage : unstopdemande pendant l'attente rend la main immediatement (continuesurreturn 1).Hors scope
rc=125lui-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