diff --git a/scripts/ci/check_companion_coverage.py b/scripts/ci/check_companion_coverage.py index 2795146cf93..570fd323924 100644 --- a/scripts/ci/check_companion_coverage.py +++ b/scripts/ci/check_companion_coverage.py @@ -71,12 +71,22 @@ # le lanceur a plafond memoire Natif (Job Object Windows, `run_guarded.py`) # dont le plafond n'a pas d'equivalent POSIX : un compagnon .sh serait une # imitation sans lecture derriere. +# +# `persist/po-2027/install-keepalive-task.ps1` releve de la meme classe, et +# c'est mesure : il installe une tache planifiee Windows qui re-invoque le +# pont WSL toutes les N minutes, parce que le porteur du runner vit +# exactement aussi longtemps que la distro. Le compagnon .sh qui viendrait a +# cote est celui qui tourne DANS la distro (`coursia-runner-start.sh`) -- +# c'est-a-dire precisement le processus que le `wsl --shutdown` tue, donc +# celui qui ne peut pas se re-armer lui-meme. Un `.sh` compagnon ici serait +# la copie d'un geste que seul l'hote Windows peut executer. EXEMPT_FILES = { "docker-configurations/scripts/setup-win-acme-renewal.ps1", "docker-configurations/services/tts-multi/setup-iis-reverse-proxy.ps1", "scripts/genai-stack/Configure-IISAuthentication.ps1", "scripts/ci/docker/linux-runner/persist/hold-runner.ps1", "scripts/ci/docker/linux-runner/persist/host-distress-probe.ps1", + "scripts/ci/docker/linux-runner/persist/po-2027/install-keepalive-task.ps1", "MyIA.AI.Notebooks/QuantConnect/ML-Training-Pipeline/scripts/launchers/launch_capped.ps1", } diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/README.md b/scripts/ci/docker/linux-runner/persist/po-2027/README.md new file mode 100644 index 00000000000..75b29d5d171 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/README.md @@ -0,0 +1,91 @@ +# Leg coursia-runner de po-2027 (Linux, ephemere) + +Slots Docker Linux pour le pool `coursia-linux`, en mode **ephemere** : un job par +enregistrement, puis le conteneur se retire et le superviseur en demarre un neuf. +La machine reste un poste de travail — les caps sont dimensionnes pour que l'hote +prime. + +## Manifeste + +| Fichier | Role | +|---|---| +| `coursia-runner-start.sh` | Enveloppe du legs : budget CPU/memoire, jeton, `DOCKER_HOST`, gardes, puis `exec supervise.sh` | +| `coursia-ci.slice` | Slice cgroup qui borne l'ensemble (memoire, swap, cpu) | +| `coursia-runner.service` | Unite systemd du superviseur, rattachee a la slice | +| `coursia-socket.conf` | Drop-in de `docker.service` (vide les references Requires/After a docker.socket, epingle l'ExecStart sur /var/run/docker-ce.sock) | +| `docker.service` | Override complet de l'unite Docker (necessaire sur ce systemd) | +| `install-keepalive-task.ps1` | Tache Windows qui re-arme le porteur toutes les 5 min (self-heal) | + +Le pont Windows `persist/launch-runner.sh` et le porteur `hold-runner.ps1` sont +communs a toutes les machines ; seul `install-keepalive-task.ps1` est propre a ce +poste. + +## Pourquoi un porteur, et pourquoi il ne suffit pas + +WSL reape une distro des que le dernier client sort, **meme** avec systemd en +PID 1, le service actif et les slots online (mesure fondatrice du 2026-09-02, +consignee dans `persist/hold-runner.ps1`). Un reveil one-shot ne tient donc rien : +le porteur est un `wsl.exe` detache qui demarre le service puis dort. + +Mesure du 2026-10-24 (cette machine) : ce porteur vit exactement aussi longtemps +que la distro. Un `wsl --shutdown` — ce que fait Docker Desktop a chaque +redemarrage — les tue ensemble. + +| Evenement | Heure (CEST) | Effet mesure | +|---|---|---| +| Porteur spawn (pid 38780) | 23:32:22 | slots online | +| `wsl --shutdown` (Docker Desktop) | 23:54:02 | distro et porteur morts ; 4 slots disparus de GitHub | +| Boot suivant, autostart systemd | 23:54:12 | `docker.service` + `coursia-runner.service` actifs | +| Slots de nouveau online | ~23:54:2x | 4/4, sans intervention | + +Le redemarrage se rétablit donc seul **cote runner** ; ce qui manquait est le +porteur. Sans lui, la distro est a nouveau reapee des que le dernier client +sort, et les slots disparaissent en silence — le seul symptome est leur absence. + +## Installation + +1. Fichiers systeme (sous WSL root) : `coursia-ci.slice`, `coursia-runner.service`, + `coursia-socket.conf`, `docker.service` aux emplacements systemd. +2. Pont et porteur Windows : `persist/launch-runner.sh` + + `%USERPROFILE%\.coursia-runner\hold-myia-po-2027.ps1`. +3. Tache logon (creee en eleve, une seule fois) : action + `C:\Program Files\Git\bin\bash.exe -lc "/persist/launch-runner.sh 4"`. +4. Tache de rattrapage, **non elevee** : + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File install-keepalive-task.ps1 # dry-run +powershell -NoProfile -ExecutionPolicy Bypass -File install-keepalive-task.ps1 -Apply # ecrit +``` + +L'appel est idempotent : porteur vivant, `hold-myia-po-2027.ps1` se contente d'un +`systemctl start` one-shot. + +## Retrait + +```powershell +schtasks /Delete /TN "Coursia-LinuxRunners-Keep" /F # tache de rattrapage +schtasks /Delete /TN "Coursia-LinuxRunners" /F # tache logon (en eleve) +``` + +Puis `supervise.sh stop` **avant** toute extinction : l'arret gracieux laisse le +job en vol se terminer, une extinction brutale le tue. + +## Limite connue, a ne pas maquiller + +Un `wsl --shutdown` tue les jobs en vol, et GitHub laisse alors le job +`in_progress` jusqu'a detection de la perte du runner. Cas mesure le +2026-10-24 : run `40129334303`, job `120977484412` sur +`myia-po-2027-linux-docker-4`, demarre 21:53:45Z, fige a l'etape 5 a 21:53:53Z, +soit 9 s avant le shutdown de 21:54:02Z (journal evenementiel Windows du +2026-10-24 : NIC de la VM demonte a 21:54:03Z, recree au boot suivant a +21:54:12Z ; Docker Desktop redemarre a 21:54:19Z). Le leg ne peut pas s'en proteger : la +cause est externe (redemarrage de Docker Desktop). Ce qu'il peut faire, et que +la tache de rattrapage fait, est de revenir en ligne sans intervention. + +## Deregistration propre + +Le mode ephemere se desinscrit seul : apres chaque job, le conteneur ecrit +`Removed .credentials` / `Removed .runner` et rend son jeton d'enregistrement. +Un `supervise.sh stop` arrete les slots sans laisser d'enregistrement orphelin ; +le registre GitHub oscille donc en fonctionnement normal (une entree par slot +present a l'instant de la lecture). diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/coursia-ci.slice b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-ci.slice new file mode 100644 index 00000000000..b68a74ac631 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-ci.slice @@ -0,0 +1,49 @@ +# Budget AGREGE des conteneurs runner CI -- myia-po-2027 (VM WSL de 32 Go, 20 vCPU). +# Adapte le 2026-10-24 depuis persist/po-2028/coursia-ci.slice (meme distro WSL, +# meme moteur docker-ce epingle sur /var/run/docker-ce.sock). Doctrine inchangee : +# budget userspace = somme des caps ; mur kernel au-dessus du budget, sous la VM ; +# AUCUN SWAP pour la CI (MemorySwapMax=0 : un job qui deborde meurt localement, +# exit 137 retryable, au lieu de swapper la VM et de faire tomber DriveFS puis la +# coordination de flotte -- chaine causale ai-01 07/09 et po-2024 08/10, 104 GiB +# de swap ecrits en 11 h). +# +# ARITHMETIQUE (2026-10-24, depart a N=4) : +# 4 slots docker x 4096 Mo = 16 384 Mo de caps (classe Scripts Tests, pic +# mesure 3,5 Gio + agent + marge ~15 %, #16643 -- cap 4g, pids 512) +# budget userspace COURSIA_RUNNER_BUDGET_GB = 16 (= somme des caps) +# MemoryHigh = 16G (pression/reclaim AVANT la somme des caps) +# MemoryMax = 18G (filet, 2 Go au-dessus du budget, 14 Go sous la VM) +# -> 13 Go de la VM restent aux lanes CoursIA interactives et a Docker +# Desktop ; l'hote Windows (63,6 Go) garde >= 45 Go, la VM etant plafonnee +# a 32 Go par .wslconfig. +# +# I/O : valeurs REPRISES de la mesure ai-01 (2026-09-07, 3 conteneurs : +# 221 Mo/s cumules sous plafond contre 16,9 Go/s sans, facteur 76). Role : +# interdire qu'un thrash PASSE L'ECHELLE (~2 % d'un NVMe), pas reguler un +# debit normal. +# +# CPU : pas de CPUQuota ici (doctrine po-2024/po-2028) -- l'axe CPU est garde +# par COURSIA_RUNNER_CPU_BUDGET=10 dans le wrapper (borne = moitie des 20 vCPU, +# garde 1 de l'addendum ai-01 du 2026-10-24 ; caps effectifs 4 x 2 = 8 vCPU, +# soit 40 %), refus au demarrage au depassement. +# +# ORDRE : cette slice se deploye AVANT les conteneurs ; assert_slice +# (supervise.sh) REFUSE le demarrage si elle est absente ou sans io.max +# (COURSIA_RUNNER_REQUIRE_CGROUP_BUDGET=1 dans le wrapper po-2027). +[Unit] +Description=CoursIA CI runner containers -- aggregate budget (all families, po-2027) +Documentation=https://github.com/jsboige/CoursIA/blob/main/docs/ci/self-hosted-runners.md +Before=slices.target + +[Slice] +CPUAccounting=yes + +IOAccounting=yes +IOWeight=50 +IOWriteBandwidthMax=/var/lib/docker 209715200 +IOReadBandwidthMax=/var/lib/docker 419430400 + +MemoryAccounting=yes +MemoryHigh=16G +MemoryMax=18G +MemorySwapMax=0 diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner-start.sh b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner-start.sh new file mode 100644 index 00000000000..cf6078bdc12 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner-start.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Wrapper de demarrage du superviseur de runners Linux CoursIA sous systemd. +# Deploiement myia-po-2027 (2026-10-24) : copie de reference -- l'original vit +# dans la distro Ubuntu de l'hote, sous /usr/local/bin/coursia-runner-start.sh. +# Adapte de persist/coursia-runner-start.sh (po-2024) et de +# persist/po-2028/coursia-runner-start.sh, dont la structure de jetons est +# reprise : le jeton n'est ni stocke ni passe en argv, il est relu a CHAQUE +# invocation. +# +# DELTAS po-2027, mesures le 2026-10-24 : +# - distro Ubuntu 26.04 « resolute », systemd PID 1, 20 vCPU / 31 Go vus ; +# - `GH_RUNNERS_ADMIN_TOKEN` est ABSENT du master.env de cette machine : la +# voie de repli de po-2028 (gh.exe + APPDATA Windows) est donc la voie +# nominale ici, le master.env restant prioritaire s'il est renseigne un +# jour (une seule lecture, deux sources ordonnees) ; +# - `gh` natif installe dans la distro (2.103.2) parce que supervise.sh +# l'appelle directement (`command -v gh`) et que le +# conteneur ne peut pas executer un .exe Windows ; +# - le daemon est docker-ce 29.9.2 epingle sur /var/run/docker-ce.sock ; le +# drop-in de flotte ne suffisait pas sur ce systemd (Requires/After vers +# docker.socket non vidables), d'ou l'override d'unite +# persist/po-2027/docker.service, pose avec persist/po-2027/coursia-socket.conf. +set -euo pipefail + +# Racine du depot. Le defaut suit le chemin REEL de cette machine : la migration +# du 2026-09-17 a deplace `C:\dev\CoursIA` -> `D:\Dev\CoursIA` (le `dev` est en +# minuscules ici, et /mnt/d est insensible a la casse). +REPO_DIR="${COURSIA_REPO_DIR:-/mnt/d/dev/CoursIA}" + +# --- JETON (jamais en argv, jamais stocke dans la distro) ------------------- +# Ordre : master.env d'abord (doctrine de flotte, CRLF interdit -> tr -d '\r'), +# puis le CLI Windows, dont le jeton vit dans le trousseau Windows. Le `gh.exe` +# lance depuis la distro n'herite pas de APPDATA : sans cette variable il rend +# une chaine vide (mesure du 2026-10-24), ce qui produisait un FATAL opaque. +TOKEN_FILE="${COURSIA_MASTER_ENV:-$REPO_DIR/.secrets/master.env}" +TOKEN="" +if [ -r "$TOKEN_FILE" ]; then + TOKEN="$(sed -n 's/^GH_RUNNERS_ADMIN_TOKEN=//p' "$TOKEN_FILE" | tr -d '\r')" +fi +if [ -z "$TOKEN" ]; then + GH_WIN="/mnt/c/Program Files/GitHub CLI/gh.exe" + [ -x "$GH_WIN" ] || GH_WIN="$(command -v gh.exe || true)" + if [ -n "${GH_WIN:-}" ] && [ -x "$GH_WIN" ]; then + TOKEN="$(APPDATA='C:\Users\Jesse\AppData\Roaming' "$GH_WIN" auth token 2>/dev/null | tr -d '\r\n')" + fi +fi +if [ -z "$TOKEN" ]; then + echo "FATAL: aucun jeton GitHub -- GH_RUNNERS_ADMIN_TOKEN absent de $TOKEN_FILE ET gh.exe muet (auth Windows). Reparer l'un des deux avant de relancer le service." >&2 + exit 1 +fi + +export DOCKER_HOST="unix:///var/run/docker-ce.sock" +export GH_TOKEN="$TOKEN" +export COURSIA_RUNNER_REPO="jsboige/CoursIA" +export COURSIA_RUNNER_NAME_PREFIX="myia-po-2027-linux-docker" +export COURSIA_RUNNER_STATE_DIR="/var/lib/coursia-runner" +export COURSIA_RUNNER_TOOLCACHE_VOLUME="coursia-runner-toolcache" + +# --- DIMENSIONNEMENT (l'hote prime : c'est aussi un poste GPU interactif) ---- +# N = 4 slots, caps 2 vCPU / 4 Go / 512 pids. Pourquoi ces caps et pas les +# 1536 Mo du defaut du superviseur : la classe « Scripts Tests » culmine a +# 3,5 Gio (mesure po-2028 du 2026-10-20, #16643) ; a 1536 Mo elle meurt sur +# KeyError xdist, c'est-a-dire sur des workers tues par le cap, pas sur un test +# rouge. On reduit donc le NOMBRE de slots, jamais le cap unitaire. +# +# ARITHMETIQUE DECLAREE (regle #19802 : le budget est la somme des caps) : +# 4 slots x 4 Go = 16 384 Mo -> COURSIA_RUNNER_BUDGET_GB=16 +# 4 slots x 2 vCPU = 8 vCPU sur 20 (40 %) -> COURSIA_RUNNER_CPU_BUDGET=10 +# slice : MemoryHigh=16G, MemoryMax=18G, MemorySwapMax=0 (mur kernel) +# La VM WSL est plafonnee a 32 Go (.wslconfig) et l'hote a 63,6 Go : 18 Go de +# CI laissent 13 Go dans la VM aux lanes interactives, et >= 45 Go a Windows. +# +# Pourquoi 2 vCPU et un budget de 10 : la garde d'addendum ai-01 du 2026-10-24 +# (c1843, garde 1) definit COURSIA_RUNNER_CPU_BUDGET comme la MOITIE des coeurs +# logiques de la VM -- 20 nproc -> 10 -- et « l'hote doit primer ». Un premier +# demarrage a 4 x 3 = 12 vCPU (60 %) depassait cette borne : le cap unitaire a +# ete ramene a 2, en gardant les 4 slots (plus de jobs simultanes, moins de +# coeurs chacun ; le pool voisin n'a pas de mal a absorber la latence). +# Aucune jambe coursia-waiter n'est installee sur cette machine (dispatch +# ai-01 du 2026-10-24) : la famine se traite par des slots d'execution, pas +# par des places dans la file. +export COURSIA_RUNNER_BUDGET_GB=16 +export COURSIA_RUNNER_CPU_BUDGET=10 +export COURSIA_RUNNER_CPUS=2 +export COURSIA_RUNNER_MEMORY=4g +export COURSIA_RUNNER_PIDS=512 + +# Le mur kernel des conteneurs : la meme slice que daemon.json declare en +# cgroup-parent. assert_slice REFUSE le demarrage si elle est absente ou sans +# io.max -- d'ou REQUIRE_CGROUP_BUDGET=1 (fail-closed, jamais un pool non +# borne en silence). +export COURSIA_RUNNER_CGROUP_PARENT="coursia-ci.slice" +export COURSIA_RUNNER_REQUIRE_CGROUP_BUDGET=1 + +# I/O : valeurs reprises de la mesure ai-01 (2026-09-07, 221 Mo/s cumules sous +# plafond contre 16,9 Go/s sans). Role : empecher qu'un thrash passe l'echelle, +# pas reguler un debit normal. +export COURSIA_RUNNER_DEVICE_WRITE_BPS=41943040 +export COURSIA_RUNNER_DEVICE_READ_BPS=83886080 + +# PATH explicite : l'unite systemd ne charge pas de profil de shell, et le CLI +# GitHub installe par apt vit sous /usr/bin. +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +# #15095 : echec immediat si le demon du socket epingle ne repond pas -- +# AVANT tout demarrage de slot et tout fetch de registration token (gh). +# Sans cette garde, un daemon arrete + Restart=always = le superviseur +# martelait docker/gh indefiniment (incident 07/09 : 2 gels machine ai-01, +# ecriture ext4.vhdx 94-96 Mo/s). Le superviseur porte la meme garde en +# profondeur (assert_docker_daemon) -- celle-ci rend l'echec visible au +# niveau systemd des l'ExecStart. +if ! docker info >/dev/null 2>&1; then + echo "FATAL: demon Docker indisponible sur DOCKER_HOST=$DOCKER_HOST (docker info echoue, #15095) -- reparer le daemon avant de relancer le service." >&2 + exit 1 +fi + +SUPERVISE="$REPO_DIR/scripts/ci/docker/linux-runner/supervise.sh" +mkdir -p "$COURSIA_RUNNER_STATE_DIR" + +# --- PURGE SENTINELLE PERIMEE (#15163) ----------------------------- +# 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 laissait donc +# l'unite echouer, et le pool restait a ZERO jusqu'a intervention humaine. +# +# Le predicat est PAR-JAMBE : un `supervise.sh (start|waiters)` repondrait +# « un superviseur QUELCONQUE vit-il ? » et chaque jambe refuserait de purger +# SA sentinelle perimee tant que l'autre tourne. +if [ "${1:-start}" = "start" ] && [ -e "$COURSIA_RUNNER_STATE_DIR/stop" ]; then + if pgrep -f 'supervise\.sh start' >/dev/null 2>&1; then + echo "sentinelle presente ET superviseur vivant -- arret en cours, on n'interfere pas" >&2 + exit 1 + fi + echo "sentinelle perimee (aucun superviseur vivant) -- purge avant demarrage" >&2 + rm -f "$COURSIA_RUNNER_STATE_DIR/stop" +fi +# --------------------------------------------------------------------------- + +exec "$SUPERVISE" "$@" diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner.service b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner.service new file mode 100644 index 00000000000..d07893c60d5 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-runner.service @@ -0,0 +1,37 @@ +# Unite systemd de persistance du superviseur de runners Linux CoursIA. +# Deploiement myia-po-2027 (2026-10-24) -- copie de reference ; l'original vit +# dans la distro Ubuntu sous /etc/systemd/system/coursia-runner.service. +# Adaptee de persist/po-2028/coursia-runner.service (structure et gardes +# conformes #15095 fail-closed, #15091 IOWeight, #15163 sentinel) ; seul le +# dimensionnement change -- start 4 (composition et historique dans +# coursia-runner-start.sh, deltas po-2027, et dans coursia-ci.slice). +[Unit] +Description=CoursIA Linux runners supervisor (docker slots, po-2027) +# #15095 : Requires+BindsTo fail-closed -- docker tombe ou echoue au boot => +# le superviseur est ARRETE (propagation), pas laisse a marteler docker/gh. +# Retour du daemon = geste operateur explicite (systemctl start coursia-runner) +# -- l'hote prime sur la CI. +Requires=docker.service +BindsTo=docker.service +After=docker.service +# Borne de rafale : 5 tentatives / 300 s, puis failed et SORTIE de boucle. +StartLimitIntervalSec=300 +StartLimitBurst=5 + +[Service] +Type=simple +ExecStart=/usr/local/bin/coursia-runner-start.sh start 4 +ExecStop=/usr/local/bin/coursia-runner-start.sh stop +Restart=always +RestartSec=30 +Nice=10 +# ExecStop est gracieux (sentinel supervise.sh) : un job en vol va a son terme. +TimeoutStopSec=900 +# #15091 : IOWeight fait ceder le parc sous contention. Le plafond DUR vit +# dans la slice (les conteneurs sont les neveux de ce service) : +# coursia-ci.slice po-2027 porte IOWrite/ReadBandwidthMax + murs memoire. +IOAccounting=yes +IOWeight=50 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/coursia-socket.conf b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-socket.conf new file mode 100644 index 00000000000..79b286fafb5 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/coursia-socket.conf @@ -0,0 +1,19 @@ +# Po-2027 -- epingle le daemon docker-ce sur /var/run/docker-ce.sock. +# Recette de flotte : docs/ci/self-hosted-runners.md, "Persistance du superviseur". +# Le socket PAR DEFAUT de cette machine revient au proxy Docker Desktop (engine qui +# porte claudish-proxy) : le conteneur de CI ne doit jamais partager l'engine +# interactif. +# +# PIEGE mesure sur po-2028 (2026-10-20) puis ici (2026-10-24) : l'unite stock +# docker.service porte Requires=docker.socket ET After=...docker.socket. Vider +# Requires= ne suffit pas a dockerd-ce 29.9.2 : docker.socket masque, systemctl +# refuse le demarrage sur la reference d'ordre. Les deux sont donc vidEES. +[Unit] +Requires= +Requires=sysinit.target system.slice +After= +After=network-online.target firewalld.service containerd.service time-set.target basic.target nss-lookup.target system.slice systemd-journald.socket sysinit.target + +[Service] +ExecStart= +ExecStart=/usr/bin/dockerd -H unix:///var/run/docker-ce.sock --containerd=/run/containerd/containerd.sock diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/docker.service b/scripts/ci/docker/linux-runner/persist/po-2027/docker.service new file mode 100644 index 00000000000..e099a5ec84a --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/docker.service @@ -0,0 +1,60 @@ +# OVERRIDE D'UNITE -- myia-po-2027, 2026-10-24. +# Copie fidele de /usr/lib/systemd/system/docker.service livre par docker-ce +# 29.9.2, avec DEUX lignes retouchees seulement : +# - `Requires=docker.socket` -> `Requires=sysinit.target system.slice` +# - la ligne `After=...` perd le seul token `docker.socket` +# A poser sous /etc/systemd/system/docker.service (prioritaire sur /usr/lib). +# +# POURQUOI PAS LE SEUL DROP-IN (mesure du 2026-10-24 sur cette machine) : le +# drop-in de flotte persist/po-2028/coursia-socket.conf vide `Requires=` puis +# re-declare la liste. Ici, apres daemon-reload, `systemctl show docker.service +# -p Requires` rendait TOUJOURS `... docker.socket`, et tout demarrage echouait +# sur « Unit docker.socket is masked » (la reference d'ordre d'`After=` suffit +# a produire cette erreur). Ce systemd n'honore donc pas le vidage ; l'override +# d'unite est deterministe. Le drop-in reste necessaire pour l'ExecStart. +# +# POURQUOI MASQUER docker.socket : l'unite stock detient /run/docker.sock, que +# le proxy Docker Desktop occupe sur cette machine (integration de la distro). +# Le daemon de CI doit ecouter /var/run/docker-ce.sock et RIEN d'autre +# (doctrine docs/ci/self-hosted-runners.md, "Persistance du superviseur"). +# +# ORDRE DE POSE : docker.service (ce fichier) + coursia-socket.conf (drop-in) +# + `systemctl mask docker.socket` + daemon-reload, PUIS restart docker. +[Unit] +Description=Docker Application Container Engine +Documentation=https://docs.docker.com +After=network-online.target nss-lookup.target firewalld.service containerd.service time-set.target +Wants=network-online.target containerd.service +Requires=sysinit.target system.slice +StartLimitBurst=3 +StartLimitIntervalSec=60 + +[Service] +Type=notify +# the default is not to use systemd for cgroups because the delegate issues still +# exists and systemd currently does not support the cgroup feature set required +# for containers run by docker +ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock +ExecReload=/bin/kill -s HUP $MAINPID +TimeoutStartSec=0 +RestartSec=2 +Restart=always + +# Having non-zero Limit*s causes performance problems due to accounting overhead +# in the kernel. We recommend using cgroups to do container-local accounting. +LimitNPROC=infinity +LimitCORE=infinity + +# Comment TasksMax if your systemd version does not support it. +# Only systemd 226 and above support this option. +TasksMax=infinity + +# set delegate yes so that systemd does not reset the cgroups of docker containers +Delegate=yes + +# kill only the docker process, not all processes in the cgroup +KillMode=process +OOMScoreAdjust=-500 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/ci/docker/linux-runner/persist/po-2027/install-keepalive-task.ps1 b/scripts/ci/docker/linux-runner/persist/po-2027/install-keepalive-task.ps1 new file mode 100644 index 00000000000..43c46e0f679 --- /dev/null +++ b/scripts/ci/docker/linux-runner/persist/po-2027/install-keepalive-task.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS +Re-arme le porteur WSL du leg coursia-runner de po-2027 apres un arret de la distro. + +.DESCRIPTION +Mesure du 2026-10-24 sur cette machine : le porteur spawn par +hold-myia-po-2027.ps1 vit exactement aussi longtemps que la distro. Un +`wsl --shutdown` -- ce que fait Docker Desktop a chaque redemarrage -- tue le +porteur et la distro ensemble. La tache logon `Coursia-LinuxRunners` ne tire +qu'a l'ouverture de session : apres un shutdown, plus rien ne re-arme le +porteur, et WSL reape la distro des que le dernier client sort (mesure +fondatrice du 2026-09-02, cf persist/hold-runner.ps1). + +Sequence mesuree le 2026-10-24 (boot -1 : 22:50:37 -> 23:54:02 CEST) : +Docker Desktop redemarre a 23:54:19, la distro meurt a 23:54:02, le porteur +pid 38780 meurt avec elle. docker.service et coursia-runner.service se +relevent seuls au boot suivant (4 slots de nouveau online en ~25 s), mais le +porteur reste absent : la distro est donc a nouveau reapee une fois le dernier +client sorti, sans autre signal qu'une disparition des slots sur GitHub. + +Cette tache est le rattrapage : elle re-invoque le pont launch-runner.sh toutes +les N minutes. Le garde anti-doublon de hold-myia-po-2027.ps1 rend l'appel +idempotent (porteur vivant -> simple `systemctl start`). + +Creation non elevee (tache utilisateur, Interactive/Limited) : aucune fenetre +UAC. La tache logon d'origine, elle, a ete creee en eleve et n'est pas modifiee +ici -- `Set-ScheduledTask` y rend 0x80070005, mesure le 2026-10-24. + +.PARAMETER Apply +Ecrit la tache. Sans ce commutateur : dry-run, aucune ecriture. + +.PARAMETER Minutes +Intervalle de repetition, en minutes (defaut 5). + +.PARAMETER Bridge +Pont a invoquer (defaut : launch-runner.sh du depot, slots = -Slots). + +.PARAMETER Slots +Nombre de slots passe au pont (defaut 4). + +.EXAMPLE +powershell -NoProfile -ExecutionPolicy Bypass -File install-keepalive-task.ps1 + +.EXAMPLE +powershell -NoProfile -ExecutionPolicy Bypass -File install-keepalive-task.ps1 -Apply +#> +param( + [switch]$Apply, + [int]$Minutes = 5, + [string]$Bridge = 'D:\dev\CoursIA\scripts\ci\docker\linux-runner\persist\launch-runner.sh', + [int]$Slots = 4 +) +$ErrorActionPreference = 'Stop' + +$name = 'Coursia-LinuxRunners-Keep' +$bash = 'C:\Program Files\Git\bin\bash.exe' +# launch-runner.sh attend un chemin POSIX (il tourne sous bash.exe de Git). +$bridgePosix = $Bridge -replace '\\', '/' -replace '^([A-Za-z]):', '/$1' +$arguments = "-lc `"$bridgePosix $Slots`"" + +$action = New-ScheduledTaskAction -Execute $bash -Argument $arguments +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date.AddMinutes(1) ` + -RepetitionInterval (New-TimeSpan -Minutes $Minutes) ` + -RepetitionDuration ([TimeSpan]::FromDays(3650)) +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries -StartWhenAvailable ` + -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10) +$description = "po-2027 coursia-runner : re-arme le porteur WSL toutes les $Minutes min (self-heal apres wsl --shutdown)" + +function Write-KeepaliveTask { + param($Action, $Trigger) + if (Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue) { + Set-ScheduledTask -TaskName $name -Action $Action -Trigger $Trigger -Settings $settings | Out-Null + "maj : tache existante mise a jour (idempotent)" + } else { + Register-ScheduledTask -TaskName $name -Action $Action -Trigger $Trigger ` + -Settings $settings -Description $description | Out-Null + "creation : tache enregistree (non eleve)" + } +} + +"tache : $name" +"action : $bash $arguments" +"repet : interval=$($trigger.Repetition.Interval) duration=$($trigger.Repetition.Duration)" + +if (-not $Apply) { + "DRY-RUN : aucune ecriture. Relancer avec -Apply pour installer." + exit 0 +} + +try { + Write-KeepaliveTask -Action $action -Trigger $trigger +} catch { + # Certains hotes refusent un -RepetitionDuration de 3650 j sur un declencheur + # Once : le rejet tombe au Register/Set-ScheduledTask, pas a la construction + # du trigger. Repli : FromDays(30). [TimeSpan]::MaxValue a ete le premier + # repli envisage, puis mesure refus sur ce poste lui-meme le 2026-10-25 : + # le planificateur borne la duree a la validation XML (« valeur hors + # limites », Duration:10675199.02:48:05.4775807), le Register n'est meme + # pas atteint -- le repli serait mort sur la machine qu'il vise. 30 j se + # situe sous la borne documentee (~31 j) et se lit P30D au retour. + # Residu assume : un hote qui refuse 3650 j ET reste up plus de 30 j perd + # la repetition au bout de 30 j ; un reboot ou un nouveau -Apply la restaure. + "repli : duration 3650 j refusee -- $($_.Exception.Message)" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date.AddMinutes(1) ` + -RepetitionInterval (New-TimeSpan -Minutes $Minutes) ` + -RepetitionDuration ([TimeSpan]::FromDays(30)) + Write-KeepaliveTask -Action $action -Trigger $trigger +} + +$task = Get-ScheduledTask -TaskName $name +"verifie : state=$($task.State) interval='$($task.Triggers[0].Repetition.Interval)' duration='$($task.Triggers[0].Repetition.Duration)' enabled=$($task.Triggers[0].Enabled)" +"next : " + (Get-ScheduledTaskInfo -TaskName $name).NextRunTime +exit 0