Repository navigation
feat(ci,#15574): co-residence du pool self-hosted -- hote, concurrence, slots - #17231
Conversation
… concurrence, slots Le depot sait deja mesurer la FAME (starvation, #13378) et la demande par runner, mais pas repondre aux deux questions qui tranchent une sur-souscription, explicitement laissees ouvertes par docs/ci/self-hosted-runners.md : combien de runners partagent un hote physique, et quel plafond de jobs concurrents cet hote porte. Deux blocs les rendent, dans measure_runner_demand.py : - `runners_inventory` (STATIQUE) : les slots enregistres groupes par hote, lus sur actions/runners. L'hote est presume du prefixe du nom (`<hote>-<n>`) ; un nom sans suffixe numerique n'est PAS attribue plutot que de fabriquer un hote d'un seul slot. Trois etats distingues -- measured / unavailable (droit de lecture manquant, raison incluse) / not_collected -- aucun ne rend un parc vide. - `co_residence` (DYNAMIQUE) : par job, le pic et la moyenne du nombre de jobs presents sur le meme hote, croises avec sa duree. Le pic est calcule sur les bornes des intervalles : le compte ne change qu'aux bornes, et un point median unique rate un job qui chevauche un autre sur la moitie de sa duree (constate en ecrivant le test). Honte des bornes : la concurrence observee est une borne INFERIEURE (les jobs hors fenetre sont invisibles) ; les caveats sont emis DANS la sortie, pas seulement en commentaire, et une correlation n'y est pas presentee comme une cause. Le workflow `runner-coresidence-advisory.yml` porte la mesure : la collecte coute ~1 appel API par run, et un poste epuise son quota REST avant de couvrir une fenetre utile (mesure 2026-09-21 : quota epuise en moins d'une heure, l'instrument rendant `BROKEN INSTRUMENT`). Le jeton du workflow a le sien. Advisory par construction (schedule + dispatch, jamais pull_request), sur ubuntu-latest -- l'observateur ne consomme pas ce qu'il observe. 27 tests passent (18 existants + 9 nouveaux). Gardes du depot : self-hosted policy OK (150 workflows), concurrency-conj 0 offenceur. See #15574 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…a mesure Ajoute a docs/ci/self-hosted-runners.md la section qui repond au paragraphe qui declarait le manque : les deux nombres sont mesurables cote API, ce que le document croyait hors d'atteinte (il les classait « console d'administration »). Le fait mesurant : `actions/runners` rend les slots enregistres. La section porte les deux blocs, la derivation d'hote et sa limite (un nom sans suffixe numerique n'est pas attribue), les deux statistiques de concurrence et pourquoi le pic se calcule sur les bornes, les caveats (bornes inferieures, correlation != cause) et le runner. ETAT DE LA MESURE : l'instrument et son runner sont livres, les CHIFFRES ne le sont pas encore -- quota REST epuise ce cycle (5000/h partage entre tous les agents de la machine). Aucun chiffre n'est donc ecrit ici : un instrument n'est pas une caracterisation, et l'issue reste ouverte jusqu'au premier run de l'organe. Meme discipline que po2024-topology-baseline.md (« pas de mesure, pas de chiffre »). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Path-collision (organ #13359/#13615)Cette PR #17231 (
|
|
[P0 lane Mesure firsthand, 2026-09-21T15:41:06Z — toute lecture REST des check-runs de ce head rend : Le quota REST de l'installation est épuisé, partagé entre toutes les lanes de cette machine. Il est invisible du poste : un Conséquence sur cette PR. La jambe Ce qui a pu être vérifié sans REST ( Ce que je ne fais pas, et pourquoi : je ne pousse pas pour « relancer » — un push ne répare pas un quota, et il déplace la tête. Le diagnostic de cette jambe est donc différé au retour du quota, pas clos : si elle reste rouge une fois le quota rendu, la cause sera ailleurs et je la reprendrai. Si une autre jambe de ce head est rouge (par ex. |
Imputation du rouge
|
Les deux rouges de
|
| Test en échec | Ce que le log porte verbatim | Cause mesurée |
|---|---|---|
scripts/audit/tests/test_scan_duplicate_test_pairs.py::test_retroactive_control_sees_third_pair_pre_consolidation |
CalledProcessError: Command '['git', ..., 'checkout-index', '-a', '--prefix=...']' returned non-zero exit status 128 |
Checkout partiel du workflow (fetch-depth: 0 + filter: blob:none) : les commits sont là, les blobs non — donc le skipif du test ne se déclenche pas et le fetch promisor sort en 128. Classe déjà tracée : #17253, correctif #17254. |
scripts/notebook_tools/tests/test_check_exec_ratchet.py::TestCli::test_exit_1_on_regression |
assert 2 == 1 puis, dans le même CompletedProcess, returncode = ... EAGAIN, p.ex. pytest-xdist -n 4) ou git absent du PATH ; relancer le job, ne pas lire ceci comme « 0 changements » |
InstrumentUnavailable : le ratchet n'a pas pu spawner git (OSError après les réessais bornés de run_with_fork_retry, #16217) → sys.exit(2). Contention de processus sous pytest-xdist -n 4. Mécanisme distinct de #17253, même check. |
Pourquoi ce n'est pas le diff de cette PR
Le diff de #17231 est : .github/workflows/runner-coresidence-advisory.yml, docs/ci/self-hosted-runners.md, docs/reference/scripts-reference.md, scripts/ci/measure_runner_demand.py, scripts/tests/test_measure_runner_demand.py. Il ne touche ni scripts/notebook_tools/tests/test_check_exec_ratchet.py, ni scripts/audit/tests/test_scan_duplicate_test_pairs.py, ni les scripts qu'ils exercent. Le second test échoue sur un OSError de spawn de git — un état du runner, pas du dépôt.
Le contrôle par la base le confirme : main porte le même check rouge deux fois aujourd'hui dans le même job Scripts Tests (CPU) — runs 35642940153 (19:08:34Z, sans étape en échec et sans log, job tué) et 35618165640 (15:19:33Z, étape Run tests en échec) — encadrant un run 35643227998 (19:11:17Z) vert sans aucun commit intermédiaire sur ces tests. Une jambe qui bascule entre deux têtes sans commit désigne l'environnement.
Geste
L'instrument prescrit lui-même la sortie : « relancer le job ». Rejeu lancé sur cette PR (attempt 3). Aucune modification de code n'est justifiée ici — réécrire un assert pour faire taire une contention de fork serait maquiller une non-mesure en vert, exactement ce que sys.exit(2) existe pour empêcher (#16164).
Si ce job redevient rouge après #17254, le résidu n'est plus la classe promisor : c'est le site InstrumentUnavailable, et il se lit au message EAGAIN du sous-process, pas à la couleur du check.
Imputation du rouge
|
| job | Scripts Tests (CPU) 106493719550, run 35612877192 |
| tentative | 4 (les 3 precedentes sont tombees pareil) |
| runner | myia-ai-01-wsl-3 (self-hosted, coursia-ephemeral, coursia-linux) |
| duree | 19:58:55Z -> 20:12:56Z, soit 14 min 01 s — le timeout-minutes du job est 30 : ce n'est pas un depassement |
etape 6 Run tests |
in_progress, conclusion: null — la jambe est morte en pleine execution |
| etapes 7-9 (planchers de collection 455 / 148 / 15) | pending — jamais atteintes |
Une etape qui reste in_progress avec conclusion: null alors que le job est completed n'est pas un echec de test : c'est un job tue.
Le verdict de l'organe, pas le mien
python scripts/ci/classify_job_deaths.py --run 35612877192
Jobs morts non-skips analyses : 1 | morts infrastructurelles : 1
(NO_RUNNER_ACQUIRED=0, RUNNER_LOST_COMM=1) | REAL_STEP_FAILURE=0
| TIMEOUT=0 | AUTRES=0
... Scripts Tests (CPU) | RUNNER_LOST_COMM | myia-ai-01-wsl-3 | 5/11 | 14m01s
REAL_STEP_FAILURE=0 sur les quatre tentatives. La jambe n'a jamais rendu de verdict sur les tests — ni vert, ni rouge. Le rouge affiche est la mort du runner, pas un resultat.
Preuve locale, au SHA de tete exact
c9637f27d4 dans un worktree detache :
python -m pytest scripts/tests/test_measure_runner_demand.py -q
27 passed in 0.20s
Le test de cette PR passe — et il n'est pas la seule chose qui passe : sur les 29 check-runs du head, 27 sont success, 2 sont skipped (Quarto/Pages, non pertinents), et les seuls rouges sont la jambe et le gate qui en cascade.
Une ironie qui vaut d'etre notee
Cette PR instrumente la sante du pool self-hosted (co-residence, concurrence, slots — #15574). Sa propre jambe meurt precisement de la sante de ce pool : quatre fois, sur le meme runner, en pleine execution. Ce n'est pas un argument pour merger ; c'est une donnee pour #16288 et pour la mesure que la PR apporte.
Ce que la lane ne peut pas reparer
Rien, dans le diff, n'est a corriger : le defaut est hors du depot. Un gh run rerun ne sert que si un runner sain prend le job — les quatre tentatives ont rendu RUNNER_LOST_COMM sur myia-ai-01-wsl-3.
Ce commentaire est la justification ecrite exigee avant --ignore-red (workflow /continue, P0). La lane poursuit sa file sans attendre.
Pour debloquer : une jambe Scripts Tests (CPU) qui va au bout, quand le pool est sain — c'est un geste de parc, pas de lane. Signalé au coordinateur.
|
[ADJOINT PREFLIGHT] |
|
[ADJOINT PREFLIGHT] |
Grain: DEEP/research-code — lane myia-po-2023:CoursIA — prev: LIGHT/notebook-python #17106
Ce que cette PR livre
Les deux nombres que
docs/ci/self-hosted-runners.mddéclarait hors d'atteinte du dépôt :Ils sont mesurables — côté API, pas côté machine (c'est ce que
po2024-topology-baseline.mdn'avait pas vu : il les classait « console d'administration GitHub »).actions/runnersrend les slots enregistrés ; les horodatages des jobs rendent le recouvrement.runners_inventoryactions/runners(--runners)co_residenceDécisions de mesure, et ce qu'elles refusent
runner_name(<hôte>-<n>). Un nom sans suffixe numérique n'est pas attribué : l'inventer fabriquerait un hôte d'un seul slot — exactement le chiffre qu'on cherche. Les non-attribuables sont comptés (jobs_unplaced,unplaced_runners).test_coresidence_separates_solo_from_shared_on_the_same_hosta rougi sur la première implémentation, qui comptait 2 jobs seuls au lieu d'1).measured/unavailableavec sa raison /not_collected) : aucun ne rend un parc vide, qui serait indiscernable d'un refus de lecture.Le runner
runner-coresidence-advisory.yml— advisory par construction (schedule+workflow_dispatchuniquement, jamaispull_request: un run rouge ne peut pas bloquer une PR), surubuntu-latest: l'observateur ne consomme pas ce qu'il observe.Pourquoi un runner et pas une commande locale : la collecte coûte ~1 appel API par run, et un poste épuise son quota REST avant de couvrir une fenêtre utile.
Validation
pytest scripts/tests/test_measure_runner_demand.pypython -m py_compile scripts/ci/measure_runner_demand.pycheck_self_hosted_runner_policy.pycheck_concurrency_conj.pyyaml.safe_loadOK, 4 étapes, triggersschedule+workflow_dispatchÉTAT DE LA MESURE — les chiffres ne sont PAS dans cette PR
Quota REST épuisé pendant ce cycle (5 000/h, partagé entre tous les agents de la machine), mesuré firsthand :
L'instrument a rendu exit 2 sur instrument cassé, jamais un zéro propre — c'est son contrat, et c'est exactement ce que produirait un « 0 % de co-résidence » fabriqué. Les deux alternatives ont été tentées et écartées sur mesure :
workflow_dispatchexige le fichier sur la branche par défaut (404 sur une branche de PR), et GraphQL n'expose pas les runs d'Actions (undefinedField WorkflowRun on Repository) — vérifié sur 1 995 points de quota GraphQL disponibles.Aucun chiffre n'est donc écrit dans
docs/ci/self-hosted-runners.md: la section porte la phrase « l'instrument et son runner sont livrés, les chiffres ne le sont pas encore », et l'issue reste ouverte. Un instrument n'est pas une caractérisation — même discipline quepo2024-topology-baseline.md: « pas de mesure, pas de chiffre dans la sortie JSON ».Ce qui produira les chiffres, sans geste humain : le cron du mardi 04:20 UTC après merge, ou un
gh workflow run runner-coresidence-advisory.yml -f hours=6immédiat après merge. Le verdict est publié en annotation de run (fenêtre, jobs placés/non attribués, p50 seul vs partagé et leur ratio, hôtes, inventaire).Portée
Un seul sujet : caractériser la co-résidence. Le garde d'éviction (acceptance #4 — « un seuil de variance au-delà duquel un runner sort du pool ») n'est pas dans cette PR : il exige les chiffres que cette PR rend mesurables, et le poser avant eux serait choisir un seuil sans mesure.
See #15574
🤖 Generated with Claude Code