Lorsqu’un même commit est déclenché simultanément par une nouvelle tentative de webhook, une relance manuelle et une tâche planifiée, deux builds Xcode peuvent accéder au même espace de travail à quelques secondes d’intervalle. Ils n’échouent pas forcément tout de suite. Le plus souvent, ils modifient DerivedData en parallèle, écrasent le répertoire d’archives et finissent par provoquer une erreur intermittente difficile à reproduire. Sur un Mac cloud qui reste actif en permanence, ces déclenchements en double méritent donc d’être traités en amont, davantage que l’échec isolé d’une commande.
Déterminer d’abord la ressource qui doit réellement être verrouillée
Il ne faut pas commencer par appliquer un verrou global à l’ensemble du nœud. La portée du verrou doit dépendre de la destination d’écriture partagée, et non du nom du dépôt. Dès que deux tâches partagent l’une des ressources suivantes, elles doivent utiliser la même clé de verrouillage :
- des fichiers générés ou des répertoires de dépendances dans le même espace de travail ;
- le même chemin DerivedData ;
- le même répertoire d’archivage, d’exportation ou d’envoi ;
- un ensemble de simulateurs auquel l’accès doit rester séquentiel ;
- la même destination finale de publication.
À l’inverse, si les fragments de test disposent chacun de leur propre checkout, de leur propre DerivedData et de leur propre ensemble de simulateurs, ils peuvent s’exécuter en parallèle avec des clés différentes. Il est conseillé de composer la clé à partir du dépôt, du Scheme et de l’action, par exemple client-release-archive. Évitez d’y inclure directement le nom de la branche, car deux branches peuvent malgré tout se disputer la même destination d’archivage.
| Scénario | Granularité de verrouillage recommandée | Exécution parallèle autorisée |
|---|---|---|
| Archivage avec DerivedData partagé | Dépôt + Scheme + archive | Non |
| Fragments de test avec ensembles de simulateurs indépendants | Dépôt + numéro du fragment | Oui |
| Envoi d’artefacts de publication portant le même nom | Application + canal de publication | Non |
| Analyse statique en lecture seule | Aucun verrou nécessaire en général | Oui |
Le verrou protège les effets de bord partagés, pas le processeur. Contourner un verrou nécessaire pour augmenter la concurrence ne fait généralement que transformer le temps d’attente en temps de nettoyage.
Créer un point de compétition atomique avec mkdir
La commande mkdir fournie avec macOS constitue un point de compétition simple et fiable : lorsque plusieurs processus tentent de créer simultanément le même chemin, un seul réussit. Contrairement à l’approche consistant à vérifier d’abord l’existence d’un fichier avant d’y écrire un PID, elle ne crée aucune fenêtre de concurrence entre la vérification et l’écriture.
Le script suivant crée un jeton de propriétaire aléatoire pour le verrou et actualise un heartbeat toutes les 15 secondes. À l’arrêt, seul le processus dont le jeton correspond encore peut supprimer le verrou. Une ancienne tâche ne risque donc pas de supprimer par erreur le verrou d’une nouvelle tâche.
#!/bin/bash
set -euo pipefail
LOCK_ROOT="${HOME}/Library/Caches/ci-locks"
LOCK_KEY="client-release-archive"
LOCK_DIR="${LOCK_ROOT}/${LOCK_KEY}.lock"
TOKEN="$(uuidgen)"
mkdir -p "$LOCK_ROOT"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "build lock is already held"
test -f "$LOCK_DIR/owner" && cat "$LOCK_DIR/owner"
exit 75
fi
printf 'token=%s
pid=%s
started=%s
' \
"$TOKEN" "$$" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "$LOCK_DIR/owner"
touch "$LOCK_DIR/heartbeat"
heartbeat() {
while true; do
touch "$LOCK_DIR/heartbeat"
sleep 15
done
}
heartbeat &
HEARTBEAT_PID=$!
cleanup() {
kill "$HEARTBEAT_PID" 2>/dev/null || true
if grep -Fq "token=${TOKEN}" "$LOCK_DIR/owner" 2>/dev/null; then
rm -rf "$LOCK_DIR"
fi
}
trap cleanup EXIT
trap 'exit 130' INT TERM HUP
xcodebuild \
-workspace Client.xcworkspace \
-scheme Client \
-configuration Release \
-derivedDataPath "$PWD/.ci/DerivedData" \
archive
Le code de sortie 75 peut indiquer sans ambiguïté que la tâche est temporairement inexécutable. La couche de gestion de la file d’attente peut programmer une nouvelle tentative différée, mais elle ne doit pas relancer la tâche indéfiniment au cours de la même seconde. Tous les chemins doivent être placés entre guillemets. Le répertoire racine des verrous doit également se trouver à un emplacement accessible en écriture par le compte d’exécution actuel et ne pas risquer d’être supprimé par les commandes de nettoyage du dépôt.
Ne pas déterminer l’expiration à partir du seul PID
Les fichiers PID sont utiles au diagnostic, mais ne suffisent pas à prouver la propriété d’un verrou. macOS peut réutiliser un PID. Après le redémarrage d’un nœud, le numéro conservé dans un ancien fichier peut donc désigner un processus totalement différent. Un processus de nettoyage doit au minimum vérifier conjointement trois éléments :
- si la dernière modification du heartbeat remonte à plus longtemps que la durée d’exécution maximale du pipeline ;
- si la file d’attente a déjà marqué la tâche comme terminée ou arrivée à expiration ;
- si des processus de build associés écrivent encore dans le répertoire protégé.
Un heartbeat ancien indique seulement qu’une investigation est nécessaire, pas que le verrou peut être repris. Si un build est suspendu à cause d’un blocage du disque, le processus peut encore détenir des fichiers ouverts. Forcer le démarrage d’un second build risquerait alors d’aggraver les dégâts. Il est plus sûr de confier le nettoyage à une tâche de récupération indépendante plutôt que d’autoriser la tâche en attente à supprimer elle-même l’état laissé par la précédente.
Ajouter des champs observables au verrou
Le fichier owner peut également enregistrer le hash du commit, le numéro de tâche, le Scheme et le répertoire de travail. Il ne doit toutefois contenir ni jeton, ni donnée de signature, ni autre valeur sensible. En cas d’échec, les journaux doivent au minimum indiquer la clé du verrou, l’heure de démarrage et le numéro de tâche. Les ingénieurs pourront ainsi distinguer une attente normale de l’arrêt anormal de la tâche précédente.
Traiter séparément l’attente, l’expiration et l’annulation
L’exclusion mutuelle ne signifie pas que toutes les tâches arrivées ensuite doivent échouer. En pratique, les files d’attente appliquent généralement trois stratégies :
- archivage de publication : conserver la première tâche et mettre les suivantes en attente ;
- validation de branche : annuler l’ancienne tâche de la même branche et conserver le commit le plus récent ;
- build planifié : ignorer l’exécution en cours si une tâche utilisant la même clé est déjà active.
Quelle que soit la stratégie retenue, l’attente doit être limitée. L’expiration du délai d’attente ne doit arrêter que la tâche en attente, sans supprimer le verrou de la tâche qui le détient. Lors de l’annulation du détenteur, il faut d’abord lui envoyer un signal d’arrêt normal afin de permettre à trap d’effectuer le nettoyage. Une intervention manuelle ou une procédure de récupération indépendante ne doit être déclenchée que si le processus ne parvient pas à s’arrêter.
Il faut également éviter d’exécuter des opérations sans rapport avec la ressource partagée pendant que le verrou est détenu. Le téléchargement du code, les vérifications en lecture seule et la préparation des paramètres peuvent être effectués avant l’acquisition du verrou. Celui-ci ne doit être pris qu’au début de la phase qui modifie réellement les répertoires partagés. Cette organisation raccourcit la section critique et réduit l’encombrement de la file d’attente.
Tester les scénarios de panne avant la mise en production
Commencez par déclencher deux fois de suite le script dans une tâche sans publication et vérifiez qu’une seule instance atteint xcodebuild. Simulez ensuite une fin normale, l’échec d’une commande et la réception d’un signal d’arrêt, puis contrôlez dans chaque cas que le répertoire du verrou a été supprimé. Enfin, conservez manuellement un ancien verrou et vérifiez que la tâche en attente signale simplement son occupation sans tenter de le nettoyer de sa propre initiative.
La validation peut suivre cette liste de contrôle :
- la clé du verrou correspond précisément à la ressource partagée ;
- l’acquisition du verrou repose sur une seule opération atomique ;
- le jeton de propriétaire aléatoire est vérifié avant le déverrouillage ;
EXIT,INT,TERMetHUPdisposent tous d’un chemin de nettoyage ;- le heartbeat sert uniquement au diagnostic et ne déclenche jamais à lui seul la reprise du verrou ;
- les journaux permettent d’identifier la tâche détentrice sans contenir d’informations sensibles ;
- l’expiration du délai d’attente n’affecte pas le build en cours ;
- les tâches indépendantes utilisent des espaces de travail et des répertoires de sortie distincts.
Une fois ces vérifications terminées, les déclenchements en double deviennent des événements de mise en file d’attente explicables, plutôt que des conditions de concurrence cachées qui corrompent aléatoirement l’état du build. Pour augmenter ensuite la concurrence, il faut d’abord séparer les répertoires partagés et les destinations de publication, puis affiner les clés de verrouillage, au lieu de supprimer directement l’exclusion mutuelle.
Questions fréquentes
Pourquoi un simple fichier PID ne suffit-il pas pour verrouiller un build ?
Un PID peut être réutilisé et le fichier peut survivre à un arrêt anormal. La création atomique du répertoire désigne le propriétaire, puis un jeton aléatoire confirme son identité.
Tous les builds Xcode doivent-ils partager un verrou global ?
Non. Seules les tâches partageant le workspace, DerivedData, la destination d’archive ou la cible de publication utilisent la même clé. Les tâches isolées peuvent rester parallèles.
Peut-on supprimer un verrou dès que son heartbeat est ancien ?
Non. Il faut d’abord confirmer le dépassement du délai de la pipeline et l’absence de processus écrivant dans le chemin protégé, puis lancer une récupération séparée.
Choisissez des nœuds Mac physiques dédiés pour compiler, développer et expérimenter
Comparez deux configurations de Mac mini M4, quatre durées de location et cinq nœuds disponibles, puis choisissez selon votre flux de travail.