SureVM Engineering-Notizen

Atomare Job-Sperren gegen doppelte Builds in Cloud-Mac-CI

Atomare Job-Sperren gegen doppelte Builds in Cloud-Mac-CI

Wenn derselbe Commit gleichzeitig durch Webhook-Wiederholungen, manuelle Neustarts und geplante Jobs ausgelöst wird, können innerhalb weniger Sekunden zwei Xcode-Builds auf denselben Arbeitsbereich zugreifen. Sie schlagen nicht unbedingt sofort fehl. Häufiger überschreiben beide gleichzeitig DerivedData und Archivverzeichnisse, bis schließlich ein sporadischer, kaum reproduzierbarer Fehler auftritt. Bei dauerhaft laufenden Cloud-Macs sollten solche Mehrfachauslösungen deshalb früher berücksichtigt werden als einzelne fehlgeschlagene Befehle.

Zuerst die tatsächlich zu sperrende Ressource bestimmen

Sperren Sie nicht vorschnell den gesamten Knoten mit einer globalen Sperre. Der Geltungsbereich einer Sperre sollte sich nach dem gemeinsam beschriebenen Ziel richten, nicht nach dem Namen des Repositorys. Sobald zwei Jobs eine der folgenden Ressourcen gemeinsam verwenden, sollten sie denselben Sperrschlüssel nutzen:

Verfügen Test-Shards dagegen über jeweils eigene Checkouts, DerivedData-Verzeichnisse und Simulatorgerätegruppen, können sie mit unterschiedlichen Sperrschlüsseln parallel ausgeführt werden. Empfehlenswert ist eine Kombination aus Repository, Scheme und Aktion, beispielsweise client-release-archive. Der Branchname sollte nicht direkt Bestandteil des Schlüssels sein, da zwei Branches dennoch um dasselbe Archivziel konkurrieren können.

Szenario Empfohlene Sperrgranularität Parallele Ausführung zulässig
Archivierung mit gemeinsamem DerivedData Repository + Scheme + archive Nein
Test-Shards mit getrennten Gerätegruppen Repository + Shard-Nummer Ja
Upload gleichnamiger Release-Artefakte Anwendung + Veröffentlichungskanal Nein
Schreibgeschützte statische Prüfung Normalerweise keine Sperre erforderlich Ja

Die Sperre schützt gemeinsam verursachte Seiteneffekte, nicht die CPU. Eine notwendige Sperre zugunsten höherer Parallelität zu umgehen, verwandelt Wartezeit in der Regel lediglich in Bereinigungszeit.

Mit mkdir einen atomaren Konkurrenzpunkt schaffen

Das in macOS enthaltene mkdir eignet sich als einfacher und zuverlässiger Konkurrenzpunkt: Versuchen mehrere Prozesse gleichzeitig, denselben Pfad anzulegen, ist nur einer erfolgreich. Anders als beim Ansatz „zuerst prüfen, ob eine Datei vorhanden ist, dann die PID schreiben“ entsteht dabei kein Race-Condition-Zeitfenster zwischen Prüfung und Schreibvorgang.

Das folgende Skript erstellt für die Sperre ein zufälliges Besitzer-Token und aktualisiert alle 15 Sekunden einen Heartbeat. Beim Beenden darf nur der Prozess die Sperre löschen, dessen Token weiterhin übereinstimmt. Dadurch kann ein alter Job nicht versehentlich die Sperre eines neuen Jobs entfernen.

#!/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

Der Exit-Code 75 kann eindeutig signalisieren, dass der Job vorübergehend nicht ausführbar ist. Die Warteschlangenebene kann einen verzögerten Wiederholungsversuch einplanen, sollte den Job aber nicht unbegrenzt innerhalb derselben Sekunde erneut starten. Sämtliche Pfade müssen in Anführungszeichen stehen. Auch das Stammverzeichnis der Sperren sollte an einem Ort liegen, den das aktuelle Ausführungskonto beschreiben kann und den Bereinigungsbefehle des Repositorys nicht löschen.

Ablauf nicht allein anhand der PID erkennen

PID-Dateien sind für die Diagnose geeignet, nicht jedoch als alleiniger Eigentumsnachweis. macOS kann PIDs wiederverwenden. Nach einem Neustart des Knotens kann die Zahl in einer alten Datei daher zu einem völlig anderen Prozess gehören. Ein Bereinigungsprozess sollte mindestens drei Punkte gemeinsam prüfen:

  1. ob die letzte Heartbeat-Änderung länger zurückliegt als die maximale Laufzeit der Pipeline;
  2. ob die Warteschlange den Job bereits als beendet oder abgelaufen markiert hat;
  3. ob zugehörige Build-Prozesse weiterhin in das geschützte Verzeichnis schreiben.

Ein veralteter Heartbeat bedeutet lediglich, dass eine Untersuchung erforderlich ist, nicht dass die Sperre übernommen werden darf. Wird ein Build aufgrund blockierter Festplattenzugriffe angehalten, kann der Prozess weiterhin Dateien geöffnet halten. Ein erzwungener zweiter Build würde den möglichen Schaden dann vergrößern. Sicherer ist es, die Bereinigung durch einen separaten Reclaim-Job ausführen zu lassen, statt wartenden Jobs zu erlauben, den Zustand ihres Vorgängers selbst zu löschen.

Beobachtbare Felder für die Sperre erfassen

In der Datei owner können zusätzlich Commit-Hash, Jobnummer, Scheme und Arbeitsverzeichnis festgehalten werden. Token, Signaturdaten oder andere sensible Werte gehören jedoch nicht hinein. Fehlerprotokolle sollten mindestens Sperrschlüssel, Startzeit und Jobnummer ausgeben, damit erkennbar ist, ob es sich um reguläres Warten oder einen abnormal beendeten vorherigen Job handelt.

Warten, Zeitüberschreitung und Abbruch getrennt behandeln

Gegenseitiger Ausschluss bedeutet nicht, dass alle später eintreffenden Jobs fehlschlagen müssen. In der Praxis verwenden Warteschlangen üblicherweise drei Strategien:

Unabhängig von der Strategie sollte die Wartezeit begrenzt sein. Eine Zeitüberschreitung beendet nur den wartenden Job und darf die Sperre des Besitzers nicht löschen. Wird der Besitzer abgebrochen, sollte zunächst ein reguläres Beendigungssignal gesendet werden, damit trap die Bereinigung abschließen kann. Nur wenn der Prozess nicht beendet werden kann, sollte eine manuelle oder separate Reclaim-Prozedur folgen.

Nicht zusammenhängende Arbeiten sollten ebenfalls nicht innerhalb der Sperre ausgeführt werden. Das Herunterladen des Codes, schreibgeschützte Prüfungen und die Vorbereitung von Parametern können vor dem Sperren erfolgen. Die Sperre wird erst für die Phase benötigt, die gemeinsam genutzte Verzeichnisse tatsächlich verändert. So bleibt der kritische Abschnitt kurz und die Warteschlange wird weniger belastet.

Fehlerszenarien vor der Einführung testen

Lösen Sie das Skript zunächst zweimal unmittelbar hintereinander in einem Job ohne Veröffentlichung aus und prüfen Sie, dass nur eine Instanz xcodebuild erreicht. Simulieren Sie anschließend einen normalen Abschluss, einen fehlgeschlagenen Befehl und den Empfang eines Beendigungssignals. Kontrollieren Sie dabei jeweils, ob das Sperrverzeichnis gelöscht wurde. Lassen Sie zum Schluss manuell eine alte Sperre bestehen und verifizieren Sie, dass ein wartender Job die Belegung lediglich meldet, ohne sie eigenmächtig zu bereinigen.

Für die Abnahme kann folgende Checkliste verwendet werden:

Nach diesen Prüfungen werden Mehrfachauslösungen zu nachvollziehbaren Warteschlangenereignissen statt zu versteckten Race Conditions, die den Build-Zustand zufällig beschädigen. Soll die Parallelität später erhöht werden, sollten zuerst gemeinsam genutzte Verzeichnisse und Veröffentlichungsziele aufgeteilt und anschließend die Sperrschlüssel verfeinert werden. Der gegenseitige Ausschluss sollte nicht einfach entfernt werden.

Häufig gestellte Fragen

Warum reicht eine PID-Datei als Build-Sperre nicht aus?

Eine PID kann erneut vergeben werden, und die Datei kann nach einem Absturz bestehen bleiben. Das atomare Verzeichnis bestimmt den Besitzer, ein zufälliges Token bestätigt ihn.

Sollten alle Xcode-Builds dieselbe globale Sperre verwenden?

Nein. Nur Jobs mit gemeinsamem Workspace, DerivedData-Verzeichnis, Archivziel oder Veröffentlichungsziel benötigen denselben Schlüssel. Isolierte Tests dürfen parallel laufen.

Darf eine Sperre bei altem Heartbeat sofort gelöscht werden?

Nein. Prüfen Sie zuerst das Zeitlimit der Pipeline und ob noch ein Prozess in den geschützten Pfad schreibt. Erst danach darf ein separater Wiederherstellungsschritt aufräumen.

SureVM Cloud-Mac

Wählen Sie exklusive physische Mac-Knoten für Builds, Entwicklung und Experimente.

Vergleichen Sie zwei Mac mini M4-Konfigurationen, vier Mietlaufzeiten und fünf buchbare Knoten, und treffen Sie Ihre Wahl passend zu Ihrem Workflow.

Mietoption auswählen