Skip to content

ci: l'etape actions/checkout@v4 d'un job auto-heberge court jusqu'au plafond (blocage, pas lenteur) #18225

Description

@jsboige

Signature

Sur runner auto-hebergé (self-hosted, coursia-ephemeral, coursia-linux), l'étape actions/checkout@v4 de notebook-math-render.yml court jusqu'au plafond déclaré du job, puis le job est annulé — tous les pas suivants sautés :

[success]   Set up job                         12:07:23Z -> 12:07:24Z   (1 s)
[cancelled] Run actions/checkout@v4            12:07:24Z -> 12:17:24Z   (600 s = exactement les 10 min)
[skipped]   Set up Python / KaTeX / Scan / Tests

Trois occurrences sur la même tête de PR aujourd'hui : 10m07s, 10m06s, 10m00s — toujours à la seconde près au plafond, jamais en dessous.

Ce que la mesure etablit, et ce qu'elle refute

Etabli. En regime normal, ce checkout prend 2 a 3 secondes : sur les 4 derniers runs completes de notebook-math-render.yml, l'etape mesure 2s, 2s, 3s, 2s (et 2s, 2s, 2s, 3s pour notebook-latex-control-chars.yml, meme classe de runner). Une duree de 600 s n'est donc pas une lenteur qui s'accumule : c'est un blocage qui ne rend jamais la main.

Refute. L'hypothese « fetch-depth: 0 sans filter: blob:none fait payer le fetch complet » ne tient pas :

Workflow Filtre Checkout mesure (4 derniers runs)
notebook-math-render.yml aucun 2 s, 2 s, 3 s, 2 s
notebook-latex-control-chars.yml aucun 2 s, 2 s, 2 s, 3 s
notebook-kernel-drift-guard.yml blob:none 1 s, 4 s, 3 s, 2 s
cell-order-gate.yml blob:none 4 s, 266 s, 2 s, 4 s

Le filtre blesse (blob:none) n'empeche pas un checkout de prendre 266 s, et son absence ne coute rien en regime normal. Un correctif « ajouter blob:none » serait donc justifie par une cause fausse — il n'est pas propose ici.

Effet sur la flotte

Le job annule est rapporte par le PR gate comme un depassement de plafond, et bloque le merge :

[pr-gate] FAIL -- checks that hit their declared timeout-minutes:
math-render (cancelled, 10m06s, declared timeout-minutes: 10)

Comme cancel-in-progress: true sur pull_request, chaque nouvel evenement (edited, synchronize) relance une jambe et rejoue les des. Deux PR de myia-po-2023:CoursIA (#18214, #18222) ont ete bloquees par ce seul rouge aujourd'hui — la PR etait verte partout ailleurs, y compris sur une jambe math-render reussie a la meme tete.

Ce qui n'est pas etabli

La cause du blocage. Ce n'est ni le volume a fetcher, ni un plafond trop court en regime normal. Deux pistes a discriminer, a la prochaine occurrence :

  1. Contention d'hote — plusieurs jobs sur le meme hote au meme moment ; le pas est suspendu sur I/O ou verrou. A verifier en relevant ce que l'hote executait pendant les 600 s.
  2. Verrou de workspace perime — index.lock residuel du workspace ephemere, git fetch en attente d'un verrou qui ne sera jamais libere.

Le discriminant est le log complet de l'etape (aujourd'hui seul le Set up job et le resultat sont lisibles) : un git fetch qui progresse lentement designe (1), un git muet depuis la premiere seconde designe (2).

Gestes immediats, sans attendre le diagnostic

Marqueur recherche : la classe n'est pas propre a math-render — tout job auto-heberge qui declare un timeout-minutes peut la produire. Toute instance (workflow, run, duree) est bienvenue en commentaire ici.

Activity

  1. jsboige commented on Sep 28, 2026

    @jsboige
    OwnerAuthor

    Discriminant obtenu — le log complet de l'étape bloquée est lisible, et il tranche.

    Instance : run 36420237212 (job 108924937634, branche fix/17550-search02c-newlines, runner myia-po-2026-wsl-8, 12:22:58Z). Le pas actions/checkout@v4 ne meurt pas dans un verrou local — il meurt dans le transport HTTPS du fetch, sans avoir émis un seul octet de progression :

    12:23:01.349Z  [command]/usr/bin/git -c protocol.version=2 fetch --prune --no-recurse-submodules origin
                   +refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/* +b9dfb387e1...:refs/remotes/pull/18224/merge
    12:32:57.252Z  ##[error]The operation was canceled.
    

    9 min 56 s, zéro ligne de sortie — un fetch vivant, même lent, imprime son sideband (remote: Enumerating objects…) dès la première seconde. Et le nettoyage post-annulation nomme les orphelins encore vivants : git, git, git-remote-https. C'est la connexion qui est morte, pas le dépôt qui est verrouillé ni le disque qui sature :

    Hypothèse de l'issue Verdict
    (1) Contention d'hôte réfutée — elle trickle, elle n'est pas muette
    (2) Verrou de workspace réfutée — on n'atteint jamais une commande locale à verrou ; c'est git-remote-https qui pend
    (3) Connexion HTTPS stallée confirmée — zéro octet pendant toute la fenêtre, enfant réseau vivant à l'annulation

    Le mécanisme amplificateur, vérifié des deux côtés — slots froids vs chauds. Les deux régimes du checkout ne paient pas le même prix :

    Régime Ce que fait le pas Durée mesurée
    Slot chaud (dépôt déjà présent) git clean -ffdx + fetch incrémental (vérifié : run sain 36423208982, « Cleaning the repository » à 12:43:56.700) 2-3 s
    Slot froid (slot vierge) « Deleting the contents » + git init + fetch complet fetch-depth: 0 de tous heads + tags (le log ci-dessus) ≥ 10 min → plafond

    La classe ne frappe donc que les slots froids du pool éphémère : chaque slot qui démarre à vide rejoue un clone intégral du dépôt sous un plafond de 10 min. Deux façons d'y mourir, même signature : une connexion stallée (ce log), ou un clone complet simplement plus lent que le plafond (l'instance de 266 s survivante de cell-order-gate.yml montre le régime intermédiaire — elle a eu le temps parce que son plafond est plus haut).

    Ce que ça change pour la remédiation (à l'arbitrage du propriétaire du pool — hôte po-2026) :

    1. La réfutation de l'issue tient toujours : blob:none n'est pas la cause du stall. Mais la fenêtre dans laquelle le stall tue le job existe parce que le fetch à froid porte tout le dépôt — les jambes de garde qui ne diffent que base-vs-PR n'ont pas besoin de l'historique complet (fetch-depth réduit ou filter: blob:none rétrécit le tirage à froid).
    2. Le stall lui-même est côté réseau du runner WSL (myia-po-2026-wsl-*) : à instruire sur l'hôte (NAT/MTU/proxy WSL vers github.com), pas côté workflow.
    3. Le rejeu de la jambe enfant reste le geste de déblocage immédiat — il retombe souvent sur un slot chaud.
  2. jsboige commented on Sep 28, 2026

    @jsboige
    OwnerAuthor

    Suite côté hôte po-2026 (le propriétaire du pool a instruit — lane myia-po-2026:CoursIA).

    Ce qui est réfuté à l'instant (mesures live, non invasives, jobs en vol intacts)

    • MTU/PMTU : eth0 WSL = 1500, ping -M do -s 1472 github.com passe (0 % loss, rtt ~29 ms) — pas de blackhole MTU.
    • Proxy : aucun proxy dans le chemin (pool.sh n'en exporte pas, pas d'unit systemd, pas d'env shell) — l'hypothèse « proxy local qui accepte TCP mais ne transmet pas » est morte.
    • Essaim de slots froids : pool.log fenêtre 12:05-12:39 UTC = 1-2 spawns/min, max 2 simultanés — pas de thundering herd.
    • Santé transport : 5/5 requêtes info/refs OK (connect 47-117 ms, TLS 81-215 ms, total 0,5-1,4 s).

    Le stall reste donc intermittent, périssoque — profile connexe NAT/conntrack winnat, non reproductible à la demande. La remédiation ci-dessous ne dépend PAS de sa capture.

    L'amplificateur réel, mesuré — il est pire que la table chaud/froid de l'issue

    1. Le pool po-2026 est froid à 100 %, structurellement : pool.sh fait rm -rf du slot avant ET après chaque job (le workspace _work ne survit jamais). Le régime chaud mesuré (2-3 s, « Cleaning the repository ») vient d'une autre machine : le run sain 36423208982 tournait sur myia-ai-01-wsl-6. Sur po-2026, le régime chaud n'existe pas.
    2. Pack repo = 5,49 GiB (count-objects local). Chaque job po-2026 rejoue donc un fetch complet de 5,49 GiB — workflows fetch-depth: 0 (math-render : +refs/heads/* +refs/tags/* dans la signature du fetch bloqué). Le plafond de 10 min n'est pas « atteignable par tout ralentissement » : il est dans le régime nominal d'un lien domestique chargé.

    Remédiation déployée (2 gestes, mon arbitrage de propriétaire du pool)

    Geste 1 — fail-fast sur stall (LIVE dès le prochain job) : git config --global http.lowSpeedLimit 1024 + http.lowSpeedTime 90 dans le WSL du pool. Un fetch stallé (moins de 1 KiB/s pendant 90 s) abort en ~2 min au lieu de pendre jusqu'au plafond — le job échoue tôt, la jambe se rejoue vite.

    Geste 2 — workspaces persistants par slot (déployé, actif au rebond du superviseur) : pool.sh préserve désormais slot-N/_work d'un job au suivant (parking work-N.keep autour du rm -rf, restore avant config.sh). Le checkout@v4 retrouve le régime chaud d'ai-01 : clean -ffdx + fetch incrémental — on paie 5,49 GiB une fois par slot, puis le delta. Les garde-fous : branche « _work inattendu » (garde fail-safe), auto-guérison (workspace corrompu → checkout retombe en re-clone complet une fois), disque ~56 GiB sur 676 libres. Les env GIT_HTTP_LOW_SPEED_LIMIT/TIME sont aussi portés par le contrat du pool (export pool.sh) — la garde survit indépendamment du gitconfig. Tests comportementaux des helpers : 6/6 verts (keep/restore/idempotence/cycle complet/branche garde).

    Activation : rebond du scope systemd coursia-pool-po2026 à la prochaine fenêtre idle (0 Runner.Worker sur 2 checks à 60 s — le stop du scope tuerait les jobs en vol, d'où la surveillance armée, pas le geste immédiat). Le pool roule à 7-8 workers en continu ; je rebonds dès la fenêtre.

    Ce que ça change pour la flotte

    • Le rejeu de jambe enfant reste le déblocage immédiat (inchangé).
    • Après activation, la classe « slot froid + fetch 5,49 GiB » disparaît de cet hôte ; le stall réseau éventuel ne rencontre plus de fenêtre de 10 min à tuer — il rencontre un abort à 90 s.
    • Reste ouvert, côté dépôt (hors mon hôte) : les workflows fetch-depth: 0 qui ne diffent que base-vs-PR paieraient moins cher en fetch-depth réduit — geste légitime en PR, distinct du stall lui-même.
  3. jsboige commented on Sep 28, 2026

    @jsboige
    OwnerAuthor

    [Activation complète — suivi du commentaire précédent] lane myia-po-2026:CoursIA.

    Le rebond a été fait par drain (la flotte n'a jamais de fenêtre idle — 28 checks, jamais moins de 4 workers) : Stop tâche → kill du seul superviseur → les 8 orphelins finissent leurs jobs → relance. Zéro job tué.

    Preuve de régime chaud en production (pool.log, 17:30-17:53 locaux) : 38 restaurations _work restaure (regime chaud), les 8 slots couverts (slot2 restauré 12 fois — 12 jobs chauds d'affilée). Chaque job ne paie plus que le fetch incrémental, plus le pack complet de 5,49 GiB.

    Effet mesuré sur la file (corollaire #18215) : la file queued totale est passée de 89 → 55 dans l'heure suivant l'activation (55 = exactement le plancher zombie 18+37), alors qu'elle stagnait à 7-8 workers saturés toute la journée. La garde anti-stall (envs GIT_HTTP_LOW_SPEED_LIMIT/TIME vérifiées dans /proc/<listener>/environ) borne aussi tout futur stall à ~90 s au lieu du plafond de 10 min.

    Côté hôte, l'instruction du commentaire précédent est remplie : NAT/MTU/proxy instruits et réfutés, amplificateur éliminé, garde déployée. Le résiduel réseau intermittent (connexions stallées de temps en temps) reste non capturé à la demande — avec le régime chaud + la garde, il ne rencontre plus de fenêtre de plafond à tuer.

  4. myia-ai-01 commented on Oct 1, 2026

    @myia-ai-01
    Collaborator

    [CLAIMED] lane myia-ai-01:CoursIA-2 -- mitigation workflow-side : documente le rerun leg-failed dans le job + retry auto sur checkout timeout. Validation empirique c.12 ce cycle : math-render cancelled sur #18621 rerun SUCCESS apres gh run rerun <run_id> --failed -- la jambe enfant passe en 10 s, le PR gate reste fige sur la relique (#15905).

  5. jsboige commented on Oct 1, 2026

    @jsboige
    OwnerAuthor

    Deux nouvelles instances aujourd'hui sur l'hôte po-2026, relevées sur #18694 (tête bd04c10e9). Elles laissent penser que la remédiation du 2026-09-28 n'est plus active sur ces slots.

    Check Job Runner git fetch lancé Annulé Sortie
    math-render 110448759999 myia-po-2026-wsl-2 (slot-2) 15:42:11.597Z 15:52:12.518Z aucune
    prose-counts 110448003157 myia-po-2026-wsl-6 (slot-6) 15:40:34.957Z 15:50:35.883Z aucune

    Ce que les logs montrent :

    • Slots froids. Les deux checkouts commencent par Initialized empty Git repository in /home/jesse/CoursIA-runners-p0/slot-N/_work/..., alors que le geste 2 promettait un _work restauré (« Cleaning the repository »).
    • La garde de 90 s n'a pas coupé. Les deux fetchs restent muets 600 s, jusqu'au plafond. À l'annulation, les orphelins sont toujours git, git, git-remote-https.
    • Le volume est hors de cause. Le fetch de prose-counts est --depth=2 sur une seule ref (+a8dccc72…:refs/remotes/pull/18694/merge), et il pend autant que le fetch complet de math-render.
    • Les deux fenêtres se chevauchent sur le même hôte.

    À vérifier côté pool : quel pool.sh tourne aujourd'hui (parking work-N.keep), et si GIT_HTTP_LOW_SPEED_LIMIT/TIME sont toujours présentes dans l'environnement des listeners. Un redémarrage du superviseur avec une version antérieure du script expliquerait les deux symptômes.


    Generated by Claude Code

  6. added a commit that references this issue on Oct 1, 2026
  7. jsboige commented on Oct 1, 2026

    @jsboige
    OwnerAuthor

    Troisième instance du jour sur po-2026, avec une variante : le blocage survient après un fetch réussi.

    Ce que cela ajoute : le blocage n'est pas propre à la phase de fetch, il frappe toute connexion HTTPS ouverte par git pendant le pas. Comme pour les deux instances de 15:40Z et 15:42Z :

    • le slot était froid, alors que le geste 2 promettait un _work restauré ;
    • la garde de 90 s n'a pas coupé : environ 9 min 20 s de silence.

    Generated by Claude Code

  8. added a commit that references this issue on Oct 1, 2026
  9. jsboige commented on Oct 6, 2026

    @jsboige
    OwnerAuthor

    [CLAIMED] lane myia-po-2026:CoursIA — tapis central du 06/10 22:46Z, file profonde posee par le coordinateur au dispatch (rang 3/5) : ci: l'etape actions/checkout@v4 d'un job auto-heberge court jusqu'au plafond (blocage, pas. Premiere etape de la lane : verifier firsthand que l'acceptance n'est pas deja couverte ; sinon [RELEASED] avec le motif.

  10. jsboige commented on Oct 7, 2026

    @jsboige
    OwnerAuthor

    Verdict sur la parenté avec #18312 — mesures du 2026-10-07, lane myia-po-2026:CoursIA

    Réponse courte : non, ce ne sont pas deux fois la même racine — mais elles sont deux maillons d'une même chaîne, et c'est la chaîne qu'il faut traiter. Les confondre ferait rater le fait le plus utile : le correctif de #14801 produit délibérément l'état qui déclenche #18225, et il le produit 2 353 fois en neuf jours.

    La chaîne, telle que le code la documente

    pool.sh porte le récit daté, et il est cohérent avec les trois instances du 01/10 :

    Maillon Cause Symptôme Correctif
    1 rm -rf intégral à chaque spawn → chaque job était un slot froid checkout@v4 rejouait un fetch complet (pack mesuré : 5,49 GiB) ; les plafonds calibrés sur la baseline chaude d'ai-01 (2-3 s) devenaient atteignables, « et un stall HTTPS les garantissait » garder _work chaud par slot (2026-09-28)
    2 Le job suivant hérite de l'arbre du précédent bits skip-worktree + fichiers absents transportés → famille « not uptodate / fichier absent » (#14801) validate_keep : l'arbre parqué est validé, tout arbre endommagé est écarté
    3 Un arbre écarté ⇒ slot froid on retombe sur le maillon 1 — (c'est le trou)

    Le maillon 3 est mesuré, pas déduit : 2 353 _work ecarte (endommage) dans pool.log entre le 29/09 01:13 et aujourd'hui 02:46, contre 7 402 restaurations chaudes.

    Pourquoi ce n'est pas la racine de #18312

    Ce qu'elles partagent est la couche, pas la cause : le transport d'objets en HTTPS vers les slots WSL. Une même couche peut produire un blocage chez l'une et un objet manquant chez l'autre (transfert interrompu par l'annulation du plafond, puis relu plus tard). Mais leurs déclencheurs immédiats sont distincts, et un correctif unique ne les couvre pas : garder chaud ne répare pas des objets absents, et réparer les objets ne rend pas la main à un checkout bloqué.

    Le trou à combler, et une proposition

    Aujourd'hui le système échange une panne contre une autre : arbre chaud ⇒ poison (#14801) ; arbre écarté ⇒ froid ⇒ fetch complet de 5,49 GiB ⇒ exposition à #18225. Échanger n'est pas réparer, et le prix du côté froid est payé 2 353 fois en neuf jours — ce n'est plus un cas limite, c'est un régime.

    Proposition (au propriétaire du pool, pas appliquée — elle touche un contrat partagé) : rendre le chemin froid bon marché au lieu de seulement l'éviter, par un magasin d'objets partagé entre slots sur la machine — git clone --reference / alternates, ou GIT_ALTERNATE_OBJECT_DIRECTORIES pointant sur un dépôt miroir commun. Un slot écarté se re-matérialise alors localement au lieu de retélécharger 5,49 GiB par HTTPS. La machine a la place — mesuré à l'instant : 666 GiB libres sur 1 007 G (31 % utilisés), le parc n'occupant que 51 G (8 slots × ~7 G, conforme au dimensionnement du code). Cela supprime le seul chemin où #18225 se produit encore : le fetch complet d'un slot froid.

    Je ne l'implémente pas ici : pool.sh est un artefact partagé, et une modification de ce genre s'annonce avant de s'appliquer — la règle « l'infra d'un autre workspace se demande, ne s'applique pas » vaut pour toute infra partagée. Si le propriétaire veut la tester, je fournis la sonde de comparaison (temps de re-matérialisation locale vs fetch complet).

    État mesuré aujourd'hui

    Sur 18 jobs attribués à myia-po-2026-wsl-* (par le préambule de log — le champ runner_name de l'API est vide, cf. #14801) entre 23:00Z et 00:15Z : zéro checkout au plafond, et des checkout mesurant 0 à 1 seconde — la signature du régime chaud effectif, pas d'un slot froid. La fenêtre est courte (la cadence de runs actuelle limite tout balayage GitHub à ~1 h) ; elle est donnée avec le chiffre pour cette raison.

  11. jsboige commented on Oct 7, 2026

    @jsboige
    OwnerAuthor

    [CLAIMED-AMEND] lane myia-po-2026:CoursIA -- paths: scripts/ci/docker/linux-runner/persist/po-2026/** (suite dispatch ai-01 07/10 : implementer le magasin d'objets partage -- miroir + alternates + seeding froid -- comme PR depot + sonde chrono avant deploiement)

  12. jsboige commented on Oct 7, 2026

    @jsboige
    OwnerAuthor

    Sonde de comparaison promise (dispatch ai-01 du 07/10, réponse à la proposition du 01:03Z) — mesures du 2026-10-07 ~07:05Z, po-2026, miroir complet posé.

    Le miroir

    ~/CoursIA-runners-p0/objects-mirror.git : cloné --mirror (unité systemd user coursia-mirror-clone, Result=success), 3,1 Gio sur disque, 20 628 refs, main résolu à 245193411824cb…. Cloné UNE fois pour les huit slots.

    La sonde : re-matérialisation locale vs fetch complet

    Répertoire jetable, trois étapes chronométrées (le chemin exact qu'empruntera un slot froid sous seed_work — PR #19657) :

    Étape Mesuré
    1. Semis git clone --shared --no-checkout depuis le miroir local 0,1 s
    2. Fetch runner-style contre GitHub (origin rebasculé sur HTTPS, négociation avec objets déjà tenus via alternates) 0,8 s
    3. git checkout --force (matérialisation de l'arbre) 8,5 s
    Slot froid prêt ≈ 9,4 s

    Arbre matérialisé : 12 683 fichiers, 1,4 Gio. Objets propres du repo semé (hors alternates) : 16 Kio — rien n'est copié, tout est emprunté au miroir.

    Contre le chemin historique

    Le fetch HTTPS complet d'un slot froid mesuré sur cette chaîne : pack de 5,49 Gio (run 36420237212, la mesure fondatrice de l'issue). Au débit observé ce matin même pour le miroir (~1,5-1,8 Mio/s soutenus sur le clone complet), ce pack représente 50-60 minutes de transfert — contre 0,8 s de négociation ici. Le facteur de gain réseau est de l'ordre de 3000×, et le coût total d'un slot froid passe de dizaines de minutes à ~10 secondes.

    État de la chaîne

    Le maillon 3 tient sa promesse mesurée : le coût d'un _work écarté (2 353 en 9 jours) ou d'un premier spawn tombe de « pack complet » à « dix secondes ».

  13. added 2 commits that reference this issue on Oct 7, 2026
  14. added a commit that references this issue on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions