Skip to content

fix(ci,#15163): purger la sentinelle perimee au demarrage des jambes ai-01 -- predicat par-jambe + harnais - #15188

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/15163-sentinel-purge-boot
Sep 8, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/15163-sentinel-purge-boot

Conversation

@myia-ai-01

Copy link
Copy Markdown
Collaborator

Grain: MED/guard — lane myia-ai-01:CoursIA — prev: MED/tooling #15103

Le fait

Un arret gracieux pose $STATE_DIR/stop. C'est un fichier : il survit au reboot. Au demarrage suivant, supervise.sh le trouve, refuse de demarrer, et l'unite systemd echoue. Le pool reste a zero jusqu'a intervention humaine.

Mesure ai-01, journal systemd du 2026-09-07 :

22:37:01  stop gracieux (sentinelle posee)
          <reboot>
22:53:29  Started coursia-runner.service
22:53:30  ERREUR: sentinel STOP_FILE present -- status=1/FAILURE

Restart=always n'aide pas : le refus est deterministe, chaque relance rejoue le meme echec. C'est arrive quatre fois dans la seule journee du 2026-09-07, chaque fois avec un deplacement physique jusqu'a la machine pour la redemarrer.

Le correctif

Un ExecStart est une demande explicite de demarrage. Si aucun superviseur n'est vivant, la sentinelle ne protege plus rien -- elle wedge. On la retire. Si un superviseur est vivant, un arret est en cours : on n'interfere pas, et on sort en erreur plutot que de lui couper l'herbe sous le pied.

Le predicat est PAR-JAMBE, pas flotte-entiere

C'est le seul point de conception qui compte ici. /var/lib/coursia-runner/stop et /var/lib/coursia-waiters/stop sont deux fichiers distincts. Un predicat supervise\.sh (start|waiters) repondrait « un superviseur QUELCONQUE vit-il ? » -- si bien que chaque jambe refuserait de purger sa propre sentinelle perimee tant que l'autre tourne.

Un verrou par jambe garde par un test global ne garde rien : il wedge. C'est exactement le defaut que ce correctif ferme, et le cas /4 du harnais est le seul qui distingue les deux predicats.

Pourquoi pas simplement --force dans l'ExecStart

--force purge inconditionnellement : le mettre dans l'unite rouvrirait le defaut que #14259 a ferme (annuler un arret gracieux reellement en cours). La purge livree ici est gardee par « aucun superviseur de cette jambe n'est vivant ».

Et sur la jambe waiters, la question ne se pose meme pas : --force n'existe que sur start (supervise.sh L602). Sur waiters et lean, une sentinelle perimee n'a aucune sortie automatique -- et c'est precisement la jambe ou le wedge a ete observe le 2026-09-07, resolu a la main.

Portee : cette PR est l'etape 1, et l'issue prescrit une etape 2

A dire clairement : #15163 prescrit de porter le correctif dans supervise.sh (une porte commune aux trois familles start/waiters/lean), pas dans les wrappers -- l'argument etant qu'il y a plusieurs wrappers et qu'ils auraient chacun besoin de la meme rustine. Cet argument est juste, et cette PR ne le contredit pas : elle le sequence.

Mesure de collision faite avant d'ecrire (L898), sur les trois PRs ouvertes qui touchent supervise.sh :

PR lane hunks sur supervise.sh
#15166 po-2026 cmd_start, cmd_waiters, cmd_lean
#15123 ai-01 cmd_start, cmd_waiters
#15182 ai-01 2 hunks anodins (L34, L89)

Les trois sites de la porte commune sont exactement les fonctions que #15166 et #15123 modifient. Y toucher maintenant force un rebase a une PR d'une autre lane. Les deux wrappers livres ici ne sont, eux, touches par aucune PR ouverte.

Ce que cette PR couvre donc : les deux jambes d'ai-01 -- la machine dont les quatre redemarrages ont ete payes en deplacements physiques. Ce qu'elle ne couvre pas, et qui reste du : le wrapper po-2024 (persist/coursia-runner-start.sh, sans purge, mesure faite) et la famille lean. La porte commune les prendra, en PR separee, une fois supervise.sh draine.

Correction a apporter a l'issue #15163

L'issue porte une section « Acceptance » integralement cochee [x], decrivant any_supervisor_alive(), stop_sentinel_gate() et des « Tests 20-22 » dans test_supervise_guards.sh. Rien de tout cela n'existe :

$ git grep -n 'stop_sentinel_gate\|any_supervisor_alive' origin/main -- scripts/ci/docker/linux-runner/
(vide)
$ grep -n 'stop_sentinel_gate\|any_supervisor_alive' scripts/ci/docker/linux-runner/supervise.sh
(vide)            # controle positif du grep : 'cmd_start' -> 2 occurrences

J'ai ecrit cette issue moi-meme (myia-ai-01, 2026-09-08T03:01:04Z) et coche une acceptance sur du travail non livre. Le body est corrige dans le meme geste que cette PR : les cases repassent a [ ] et l'etat reel y est ecrit.

Le harnais -- et la seule raison de le croire

persist/test_sentinel_purge.sh, 4 cas par jambe. Il extrait le bloc du fichier de production (sed -n '/^# --- PURGE SENTINELLE PERIMEE/,/^# ---...$/p') au lieu de le reimplementer, et assert que l'extraction contient bien le rm -f -- sans quoi un sed qui rate rendrait vert en ne testant rien.

11 PASS / 0 FAIL
==> rc=0  (mesure AVANT tout pipe)

rc est capture dans OUT=$(...) avant tout | sed : un rc=$? apres un pipe lit le code du dernier maillon, et cette suite a deja imprime rc=0 sur un 6 PASS / 4 FAIL.

Le premier run etait rouge, et le rouge etait correct. 6 PASS / 4 FAIL : les cas 2 et 4 echouaient sur les deux jambes, parce que le harnais tourne sur la machine de production, ou les vrais superviseurs sont active -- le pgrep du systeme les trouvait, donc le cas « aucun superviseur vivant » etait intestable. D'ou le stub pgrep en tete de PATH, qui lit $FAKE_PROCS et applique le motif -f recu comme ERE. Le stub est lui-meme valide par un controle positif et negatif avant que ses resultats soient crus (PASS: stub pgrep discrimine start vs waiters).

Controle positif du harnais -- un vert ne prouve rien tant que l'outil n'a pas montre qu'il sait rougir. Les deux regressions que cet organe existe pour attraper, injectees dans des copies :

Regression injectee Verdict
predicat flotte-entiere supervise\.sh (start|waiters) (le defaut #15163) 9 PASS / 2 FAIL, rc=1 -- les deux cas /4 rougissent
bloc de purge purement retire 1 PASS / 2 FAIL, rc=1 -- « bloc de purge INTROUVABLE »

Le cas /4 est donc le seul qui separe le correctif de ce qu'il corrige, et il le fait mesurablement.

Verification

  • bash -n sur les deux wrappers (porte par le harnais via l'extraction + execution reelle du bloc).
  • Fins de ligne : CR=0 sur les trois fichiers, instrument valide par controle positif (un crlf_probe.txt fabrique rend CR=2) -- grep -c $'\r' degenere en grep -c '' sous Git Bash et rendait le nombre de lignes. .gitattributes couvre deja *.sh text eol=lf (L39).
  • Les deux wrappers sont deployes et actifs sur ai-01 depuis le 2026-09-07 ; les deux unites sont active, sentinelle absente. Cette PR met en git ce qui tourne.

Suite nommee (G-VAR-1)

Cette PR est de genre guard -- classe META : elle ne tient pas le plancher du cycle. Le grain de contenu qui le portera est la QA visuelle des decks slides escaladee a ma lane par po-2026 (capability vision) : #15131 (07-elargissements, 6 slides, ?clicks=99), puis ses sœurs #15049 et #15051.

See #15163

…ai-01

Un arret gracieux pose "$STATE_DIR/stop". C'est un FICHIER : il survit au
reboot. Au demarrage suivant, supervise.sh le trouve, refuse de demarrer, et
l'unite systemd echoue -- le pool reste a ZERO jusqu'a intervention humaine.

Mesure ai-01, journal du 2026-09-07 :
  22:37:01  stop gracieux (sentinelle posee)
  <reboot>
  22:53:29  Started coursia-runner.service
  22:53:30  ERREUR: sentinel STOP_FILE present -- status=1/FAILURE

Quatre fois dans cette seule journee, chaque fois avec un deplacement physique
jusqu'a la machine pour la redemarrer.

Un ExecStart est une demande EXPLICITE de demarrage : si aucun superviseur
n'est vivant, la sentinelle ne protege plus rien -- elle wedge. On la retire.
Si un superviseur EST vivant, un arret est en cours : on n'interfere pas et on
sort en erreur.

Le predicat est PAR-JAMBE, pas flotte-entiere : /var/lib/coursia-runner/stop et
/var/lib/coursia-waiters/stop sont deux fichiers distincts, et un predicat
`supervise\.sh (start|waiters)` ferait refuser a chaque jambe la purge de SA
PROPRE sentinelle perimee tant que L'AUTRE tourne. Un verrou par jambe garde
par un test global ne garde rien : il wedge.

La jambe waiters n'a meme pas d'echappatoire : `--force` n'existe que sur
`start` (supervise.sh L602). Sur `waiters` et `lean`, une sentinelle perimee
n'a aucune sortie automatique -- c'est la jambe ou le wedge a ete observe.

test_sentinel_purge.sh est l'organe : il extrait le bloc DU FICHIER (jamais
une reimplementation) et couvre 4 cas par jambe, le /4 etant le seul qui
distingue le predicat par-jambe de celui qu'il remplace.

See #15163

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Bash Syntax Advisory — shebang / executable-bit warnings

See the Shebang + dry-run advisory job log for the per-file ::warning:: lines. Non-blocking.

@jsboige jsboige left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[Hermes] — fix(ci,#15163) : APPROVE.

Vérifié firsthand sur po-2026 (fichiers fetchés à la tête 4b92f60) :

  • Harnais exécuté : test_sentinel_purge.sh → 11 PASS / 0 FAIL, rc=0 — les 4 cas par jambe, y compris le cas 4 divergent (sentinelle de CETTE jambe + superviseur de l'AUTRE vivant → purge quand même), qui seul distingue le correctif du prédicat flotte-entière.
  • Wiring vérifié : chaque wrapper exporte son propre COURSIA_RUNNER_STATE_DIR (runner l.70 /var/lib/coursia-runner, waiters l.39 /var/lib/coursia-waiters) avant le bloc de purge — chaque jambe teste bien SA sentinelle.
  • Qualité du harnais : le bloc testé est extrait du dépôt via sed (pas une réécriture qui divergerait), stub pgrep avec contrôle d'instrument (sait dire oui ET non), compteurs en fichiers (piège sous-shell #15103 documenté). Le choix du stub est motivé par une mesure réelle (6/4 FAIL faux sur ai-01 avec le vrai pgrep — machine porte de vrais superviseurs).
  • Issue-First match : méthode PR = méthode documentée dans #15163 (« Livré (PR #15188) »). Reste-du (porte commune dans supervise.sh) explicitement reporté dans l'issue pour éviter les collisions avec #15166/#15123/#15182 — décision saine, l'issue reste OPEN jusqu'ici.
  • Security scan : 0 match (HF_TOKEN|API_KEY|BEARER|PASSWORD|SECRET|TOKEN\s*=).

Fix complet pour le scope livré, root cause fermée, régression détectable par le cas 4. RAS.

@myia-ai-01
myia-ai-01 merged commit 4ba866e into main Sep 8, 2026
16 of 17 checks passed
jsboige added a commit that referenced this pull request Sep 8, 2026
La correction 4 de persist/README.md affirmait que le drop-in de
dimensionnement "tient la machine debout". Mesure firsthand sur ai-01 le
2026-09-08 a 14:49Z : l'unite coursia-runner est `failed`, NRestarts=4,
drop-in pourtant charge et COURSIA_RUNNER_MEMORY=1536m en vigueur.

Cause : le drop-in pose la memoire et NE POSE PAS COURSIA_RUNNER_CPUS.
supervise.sh l.81 retombe sur son defaut de 3 vCPU par slot, donc les 4
slots du drop-in demandent 12 vCPU ; avec les 4 waiters a 1 vCPU deja
actifs cela fait 16 contre un budget de 8 (COURSIA_RUNNER_CPU_BUDGET:-8,
l.101 du wrapper). assert_cpu_budget() refuse, Restart=always reboucle,
systemd passe l'unite en failed apres 4 tentatives.

C'est le meme defaut que la correction 4 documente sur l'axe memoire,
reproduit sur l'axe CPU par le fichier cense le fermer. Le defaut est
latent et dependant de l'ordre de boot : assert_cpu_budget() compte les
familles deja actives, donc la meme unite mesure `active NRestarts=0` le
matin et `failed` l'apres-midi sans qu'aucun fichier ait change.

Le budget de 8 vCPU est anterieur a ce travail -- ligne identique sur
origin/main, ni introduite ni modifiee par #15188.

Cette PR ne change AUCUN dimensionnement et ne touche pas au fichier
vivant. Elle retire une affirmation fausse et documente l'axe non mesure.
Le choix CPU reste ouvert et nomme (4x1, 2x2 ou 1x3 passent le garde) :
il demande une mesure, un job reel ayant ete vu a 129 % CPU.

La strophe operante du drop-in tracke reste byte-identique au fichier
vivant (diff verifie) ; seul l'en-tete de provenance est corrige.

See #15091

Co-Authored-By: Claude-Code <noreply@anthropic.com>
myia-ai-01 pushed a commit that referenced this pull request Sep 14, 2026
…15993)

supervise.sh pose "$STATE_DIR/stop" a l'arret gracieux et refuse de
demarrer tant qu'elle est la. La sentinelle est un FICHIER : elle survit
au reboot. Un arret gracieux suivi d'un redemarrage machine faisait donc
echouer l'unite, et le pool restait a ZERO jusqu'a intervention humaine.

ai-01/runner, waiters et lean portent le bloc de purge depuis #15188.
Ce commit couvre la 4e jambe, le wrapper de la RACINE, qui est celui de
po-2024 -- la derniere non couverte (mesure consignee dans #15163 :
« sans purge, mesure faite »).

La purge n'est PAS copiee telle quelle : ce wrapper relaie n'importe
quelle sous-commande (`exec "$SUPERVISE" "$@"`) et son unite systemd
route EXPRES son ExecStart ET son ExecStop vers lui (`start 12` / `stop`).
Un bloc inconditionnel -- la forme d'ai-01, dont la branche `stop` est
ecrite AVANT la purge -- refuserait ici l'arret gracieux des qu'un
superviseur vit. La purge est donc gatee sur la forme `start`.

Le predicat reste PAR-JAMBE (`supervise\.sh start`), jamais flotte-entiere :
les sentinelles sont deux fichiers distincts, et un predicat global ferait
refuser a chaque jambe de purger SA PROPRE sentinelle perimee tant que
L'AUTRE tourne -- un verrou par jambe garde par un test global ne garde rien.

Le harnais persist/test_sentinel_purge.sh est etendu d'une jambe `po2024`
(cas 1-4) : 21 PASS / 0 FAIL, contre 16 PASS avant. Deux controles negatifs
le rendent non vacuous :
  - purge absente (wrapper pre-fix) -> FAIL po2024 bloc INTROUVABLE, rc=1 ;
  - predicat elargi a (start|waiters) -> FAIL po2024/4, rc=1.

Voir #15163 (la porte commune dans supervise.sh reste bloquee sur #15123,
d'ou le `See` et non `Closes`).

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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