Contrôler un runner CI Mac cloud avant chaque prise de tâche

Contrôler un runner CI Mac cloud avant chaque prise de tâche

Un Mac cloud peut avoir terminé une compilation hier soir, puis échouer aujourd’hui seulement après avoir accepté la tâche suivante, faute d’espace disque, à cause d’un changement du chemin Xcode sélectionné, de résidus de simulateur ou d’un sous-processus bloqué. Le pipeline occupe alors déjà un créneau d’exécution, tandis que l’erreur réelle se retrouve noyée dans l’installation des dépendances, les journaux de compilation et les tentatives de relance. Une approche plus fiable consiste à placer un contrôle d’admission avant l’attribution par l’ordonnanceur : le runner n’accepte la tâche que si les vérifications réussissent ; sinon, il quitte la file d’attente et enregistre un instantané de son état.

Placer le contrôle de santé avant la prise de tâche

Le contrôle d’admission doit être rapide, déterministe et sans effet de bord. Il ne s’agit ni d’un audit complet ni d’un mécanisme chargé de « réparer au passage » la machine. Il devrait se limiter à cinq questions :

  1. Le chemin des outils de développement existe-t-il et xcodebuild peut-il démarrer ?
  2. Le volume de travail dispose-t-il de suffisamment d’espace libre ?
  3. La pression mémoire affecte-t-elle déjà les nouvelles tâches ?
  4. Le service des simulateurs peut-il renvoyer la liste des appareils et des runtimes ?
  5. La tâche précédente a-t-elle laissé un processus géré ou un verrou de tâche ?

Sur VMCache, les runners peuvent être pilotés par un agent auto-hébergé, un ordonnanceur planifié ou le gestionnaire de file d’attente de l’équipe. Quelle que soit la solution retenue, le contrôle doit intervenir avant la « prise de tâche », et non comme première étape du script de compilation. Dans ce dernier cas, l’exécution est déjà comptabilisée comme un échec, ce qui fausse les taux de réussite et les statistiques de relance.

Lorsqu’un contrôle de santé échoue, la bonne réaction consiste à suspendre temporairement la prise de tâches et à conserver les éléments de diagnostic, et non à supprimer immédiatement les caches, arrêter tous les processus ou redémarrer toute la machine.

Construire d’abord un script de contrôle auditable

Le script ci-dessous se contente de lire l’état du système et utilise un code de sortie non nul pour refuser une nouvelle tâche. Les seuils doivent être adaptés à la taille du projet, et non repris tels quels comme constantes communes à tous les runners.

#!/bin/zsh
set -u

WORK_VOLUME="${WORK_VOLUME:-/}"
MIN_FREE_GB="${MIN_FREE_GB:-40}"
LOCK_FILE="${RUNNER_LOCK_FILE:-/tmp/vmcache-ci-job.lock}"
failures=0

fail() {
  print -u2 "FAIL: $1"
  failures=$((failures + 1))
}

developer_dir="$(xcode-select -p 2>/dev/null || true)"
[[ -n "$developer_dir" && -d "$developer_dir" ]] \
  || fail "developer directory is unavailable"

if ! xcodebuild -version >/dev/null 2>&1; then
  fail "xcodebuild cannot start"
fi

free_kb="$(df -Pk "$WORK_VOLUME" | awk 'NR==2 {print $4}')"
if [[ -z "$free_kb" ]]; then
  fail "free disk space cannot be read"
else
  free_gb=$((free_kb / 1024 / 1024))
  (( free_gb >= MIN_FREE_GB )) \
    || fail "free disk space is below ${MIN_FREE_GB} GB"
fi

if [[ -e "$LOCK_FILE" ]]; then
  fail "a previous job lock still exists"
fi

if ! xcrun simctl list devices available >/dev/null 2>&1; then
  fail "simulator service is not responding"
fi

exit $((failures > 0 ? 20 : 0))

N’utilisez pas sudo rm -rf, killall ni de changement automatique de version de Xcode dans ce script. Le rôle de la phase d’admission est de prendre une décision, pas de modifier l’état de la machine. Ainsi, même si le contrôle est lui-même mal configuré, son impact reste limité.

Définir une sémantique fixe pour les codes de sortie

Il est recommandé de définir 0 comme « prêt à accepter une tâche », 20 comme « environnement non conforme aux exigences » et 30 comme « contrôle impossible à exécuter ». Lorsqu’il reçoit 20, l’ordonnanceur marque le runner comme suspendu ; avec 30, il signale une défaillance de l’infrastructure de contrôle. Évitez de renvoyer 1 pour toutes les anomalies, faute de quoi il sera impossible de distinguer une défaillance du nœud d’une erreur du script.

Contrôler le disque et la mémoire sans les nettoyer en parallèle

Le seuil d’espace disque doit couvrir le pic cumulé du code source, des dépendances, de DerivedData, des archives et des fichiers temporaires pour une tâche. Il peut être établi à partir des pics observés sur les exécutions réussies, avec une marge supplémentaire pour un retour arrière. Le contrôle ne doit pas non plus se limiter au volume racine : si l’espace de travail, les caches ou les données des simulateurs résident sur d’autres volumes APFS, exécutez df -Pk pour chacun d’eux.

Pour la mémoire, il ne suffit pas de lire la quantité de « mémoire libre ». macOS utilise activement la mémoire comme cache de fichiers ; la pression mémoire durable et l’activité de swap sont donc plus pertinentes. Un contrôle léger peut enregistrer les sorties de memory_pressure et de vm_stat, puis laisser le système de supervision comparer leur évolution :

snapshot_dir="${RUNNER_STATE_DIR:-$HOME/ci-state}"
mkdir -p "$snapshot_dir"

{
  date -u "+%Y-%m-%dT%H:%M:%SZ"
  xcode-select -p
  xcodebuild -version
  df -h /
  memory_pressure
  vm_stat
} > "$snapshot_dir/preflight-latest.txt" 2>&1

Ne refusez pas une tâche sur la seule base d’un comptage ponctuel des pages mémoire. Une règle plus fiable consiste à enregistrer d’abord l’état, puis à bloquer la prise de tâche uniquement lorsqu’une condition connue pour faire échouer la charge de travail concernée est détectée. Pour l’inférence MLX ou les opérations d’édition de liens volumineuses, les besoins en mémoire unifiée doivent en outre être définis dans une stratégie propre à la file d’attente correspondante, plutôt que d’imposer un seuil unique à toutes les tâches.

Détecter les tâches résiduelles sans arrêter des processus système par erreur

La détection des résidus doit impérativement tenir compte du propriétaire des processus. Arrêter globalement tous les processus nommés xcodebuild, swift ou Simulator risque d’interrompre une session interactive ou les tâches d’un autre exécuteur. Une méthode plus sûre consiste à demander à chaque tâche de créer un fichier de verrouillage contenant son PID, son identifiant et son heure de début, puis à supprimer ce fichier au moyen d’un piège de sortie.

job_lock="${RUNNER_LOCK_FILE:-/tmp/vmcache-ci-job.lock}"

cleanup() {
  rm -f "$job_lock"
}

if ! ( set -o noclobber; print "$$ ${CI_JOB_ID:-unknown}" > "$job_lock" ) 2>/dev/null; then
  print -u2 "another managed job owns the runner"
  exit 20
fi

trap cleanup EXIT INT TERM

Lorsqu’un ancien verrou est détecté, vérifiez d’abord si le PID qu’il contient existe encore, puis contrôlez l’heure de démarrage et la ligne de commande du processus. Les PID étant réutilisés, leur seule valeur numérique ne suffit pas pour décider d’arrêter un processus. Si celui-ci n’existe plus, le fichier de verrouillage peut être déplacé avec l’heure du contrôle dans le répertoire de diagnostic, avant qu’une procédure de récupération contrôlée ne lève le blocage.

Délimiter le contrôle des simulateurs

La réussite de simctl list indique seulement que le service est joignable, pas qu’une cible de test particulière est disponible. Le contrôle d’admission valide la couche de service ; au niveau du projet, la tâche doit encore rechercher l’appareil cible à l’aide d’une destination explicite. Ne créez, ne supprimez et n’effacez pas automatiquement des appareils dans le script d’admission : ces opérations sont longues et modifient l’état de référence des tests suivants.

Intégrer le contrôle à l’ordonnanceur et prévoir la récupération

L’ordonnanceur doit d’abord marquer le runner comme « en cours de contrôle », exécuter le script, puis basculer atomiquement son état vers « prêt à accepter des tâches » ou « isolé ». Si l’agent ne permet pas de changement d’état atomique, suspendez d’abord la prise de tâches, effectuez le contrôle, puis réactivez-la afin d’éviter qu’une nouvelle tâche ne soit attribuée pendant la vérification.

Les actions de récupération doivent être classées par niveau de risque :

  1. Ancien verrou et PID inexistant : archiver le fichier de verrouillage, puis lever le blocage.
  2. Répertoire reconstructible dépassant le budget de stockage : après confirmation qu’aucune tâche n’est active, nettoyer selon le propriétaire du répertoire.
  3. Service des simulateurs sans réponse : enregistrer les diagnostics, puis appliquer la procédure de rétablissement du service approuvée par l’équipe.
  4. Chemin Xcode incorrect ou état du disque anormal : maintenir l’isolement et demander une vérification humaine.
  5. Répétition consécutive de la même panne : arrêter la récupération automatique et conserver les derniers instantanés pour comparer les écarts.

Enfin, testez le contrôle lui-même en situation de panne. Augmentez temporairement le seuil minimal d’espace disque et vérifiez que le runner quitte la file d’attente ; rétablissez ensuite le seuil et confirmez qu’il redevient disponible. Créez également un ancien fichier de verrouillage pour vérifier que le système isole uniquement le nœud sans arrêter de processus par erreur. L’intérêt d’un contrôle de santé n’est pas de couvrir toutes les défaillances possibles, mais de les faire apparaître avant l’attribution d’une tâche et de laisser une indication suffisamment claire sur la suite à donner.

Questions fréquentes

Faut-il exécuter tous les contrôles avant chaque compilation ?

Exécutez les contrôles rapides avant chaque prise de tâche et réservez les diagnostics coûteux à une vérification périodique ou à l’analyse d’un échec.

Peut-on supprimer tout DerivedData lorsque le disque est presque plein ?

Non. Retirez d’abord le runner de la file, identifiez les répertoires reproductibles et supprimez-les selon leur propriétaire et leur ancienneté, sans toucher aux builds actifs.

Un échec doit-il provoquer un redémarrage automatique ?

Uniquement pour une panne connue avec une procédure idempotente. Une sélection Xcode incorrecte ou une anomalie disque répétée doit conserver ses preuves et être examinée.

Nœuds physiques dédiés

Choisissez un Mac cloud pour compiler, tester et exécuter des inférences MLX

Choisissez votre M4, votre mémoire, votre stockage, votre nœud et votre durée selon vos besoins. Chaque location correspond à une machine physique dédiée, et non à une machine virtuelle.

Choisir une formule de location