Cloud-Mac-CI-Jobs sauber abbrechen und Prozesse aufräumen

Cloud-Mac-CI-Jobs sauber abbrechen und Prozesse aufräumen

In einer entfernten CI-Warteschlange sind nicht fehlgeschlagene, sondern abgebrochene Jobs am schwierigsten zu handhaben. Stoppt ein Entwickler einen Job über die Benutzeroberfläche, sendet der Runner in der Regel zunächst TERM an das Skript. Verarbeitet das Skript dieses Signal nicht, können laufende Instanzen von xcodebuild, Testprozesse oder Log-Sammler den Workspace weiter belegen. Der nächste Job trifft dann auf Sperrdateien, belegte Simulatoren oder unvollständige Ergebnis-Bundles. Das wirkt wie ein zufälliger Fehler, geht tatsächlich aber auf die unvollständige Beendigung des vorherigen Jobs zurück.

Kriterien für einen vollständig abgebrochenen Job definieren

Dass ein Job aus der Warteschlange verschwunden ist, bedeutet nicht, dass seine Ressourcen bereits freigegeben wurden. Eine praxistaugliche Definition des Abschlusses sollte mindestens vier Kriterien umfassen:

  1. Der primäre Buildprozess hat das Beendigungssignal empfangen und wurde beendet.
  2. Das Skript protokolliert den tatsächlichen Beendigungsgrund, statt jeden Fall pauschal als Buildfehler zu erfassen.
  3. Bereits erzeugte Logs und xcresult bleiben erhalten.
  4. Im Workspace verbleiben weder Hintergrundprozesse noch temporäre Mounts, die zu diesem Job gehören.

Auch der Abbruchpfad gehört zu den regulären Pfaden einer Pipeline. Wird die Bereinigungslogik nur bei unerwarteten Beendigungen getestet, stellt sich meist ausgerechnet unter hoher Last heraus, dass sie nicht funktioniert.

Prüfen Sie zunächst, welches Signal der Runner tatsächlich sendet. Dazu kann ein Testjob einen einfachen Signalfühler ausführen, der ausschließlich Signale und keine vertraulichen Umgebungsvariablen protokolliert. Verlassen Sie sich nicht auf vermutete Standardwerte. Auch ein sofortiges KILL ist keine geeignete Abbruchstrategie, weil Prozesse dann keine Gelegenheit erhalten, Datenbanken zu schließen, Logs zu schreiben oder Ergebnis-Bundles zu ordnen.

TERM mit einem Trap behandeln und den Hauptprozess verfolgen

Das Buildskript sollte die PID von xcodebuild ausdrücklich speichern. Nach dem Empfang von TERM oder INT dürfen nur Prozesse beendet werden, die der aktuelle Job erstellt hat. Ein uneingeschränktes killall ist zu vermeiden. Das folgende Bash-Grundgerüst schreibt Logs und Ergebnisse in ein separates Verzeichnis für den jeweiligen Lauf:

#!/bin/bash
set -u

run_id="${CI_RUN_ID:-manual-$(date +%s)}"
run_dir="$PWD/.ci-runs/$run_id"
result_path="$run_dir/TestResults.xcresult"
log_path="$run_dir/xcodebuild.log"
status_path="$run_dir/status.txt"
child_pid=""
cancelled=0

mkdir -p "$run_dir"

on_cancel() {
  cancelled=1
  if [[ -n "$child_pid" ]] && kill -0 "$child_pid" 2>/dev/null; then
    kill -TERM "$child_pid" 2>/dev/null || true
  fi
}

trap on_cancel TERM INT

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath "$result_path" \
  test >"$log_path" 2>&1 &

child_pid=$!
wait "$child_pid"
build_status=$?

if [[ "$cancelled" -eq 1 ]]; then
  printf 'cancelled\n' >"$status_path"
  exit 130
fi

printf 'finished:%s\n' "$build_status" >"$status_path"
exit "$build_status"

Das Verzeichnis wird hier nicht direkt im Trap gelöscht. Der Trap ist lediglich für die Weiterleitung des Signals zuständig, während der Hauptablauf weiterhin wait ausführt. Dadurch lässt sich ein regulärer Fehler von einem manuell ausgelösten Abbruch unterscheiden. Der Exitcode 130 wird häufig für unterbrochene Prozesse verwendet. Prüfen Sie jedoch vorher, wie Ihr CI-System diesen Code einem Abbruchstatus zuordnet.

Eine begrenzte Beendigungsfrist gewähren

Manche Tests reagieren nicht auf TERM. Ein Produktionsskript kann nach dem Senden des Signals beispielsweise 20 Sekunden lang einmal pro Sekunde die PID prüfen und nach Ablauf dieser Frist KILL senden. Die Nachfrist muss begrenzt sein, da ein Abbruch den Runner sonst dauerhaft blockieren kann. Sie sollte umgekehrt aber auch nicht auf ein oder zwei Sekunden verkürzt werden, weil xcodebuild möglicherweise gerade das Ergebnis-Bundle schreibt.

Diagnosematerial und Cachebereinigung trennen

Ein häufiger Fehler bei der Abschlussbereinigung besteht darin, innerhalb von trap cleanup EXIT direkt rm -rf "$run_dir" auszuführen. Das hinterlässt zwar ein sauberes Verzeichnis, löscht aber zugleich die für die Ursachenanalyse benötigten Logs. Robuster ist eine Aufteilung in zwei Kategorien:

Inhalt Behandlung nach einem Abbruch
xcodebuild.log, xcresult, Statusdatei Aufbewahren und archivieren
Vom aktuellen Job erstellte temporäre Verzeichnisse Nach Prüfung des Pfads löschen
Gemeinsam genutzter Abhängigkeitscache Nicht im Abbruch-Trap löschen
Simulatoren und abgeleitete Daten Gemäß der Isolationsstrategie des Jobs behandeln

Auch die Logarchivierung muss damit umgehen können, dass eine Datei noch nicht erzeugt wurde. Prüfen Sie dies mit [[ -e "$result_path" ]], statt ein fehlendes Ergebnis-Bundle zu einem zweiten Fehler werden zu lassen. Kann ein Upload länger dauern, sollte der Uploadschritt ein eigenes Zeitlimit erhalten. Außerdem darf ein fehlgeschlagener Upload den ursprünglichen Exitcode des Builds nicht überschreiben.

Temporäre Verzeichnisse sollten unter einem festen übergeordneten Verzeichnis liegen und eine Laufnummer enthalten. Prüfen Sie vor dem Löschen sowohl das Präfix als auch den tatsächlichen Pfad. So verhindern Sie, dass eine leere Variable den Löschbereich unbeabsichtigt erweitert:

safe_remove_run_dir() {
  local target="$1"
  local root="$PWD/.ci-tmp"

  [[ -n "$target" ]] || return 1
  [[ "$target" == "$root/"* ]] || return 1
  [[ -d "$target" ]] || return 0

  rm -rf -- "$target"
}

Verbliebene Prozesse prüfen statt global aufzuräumen

Nach dem Ende eines Jobs kann zunächst ein Prozess-Snapshot protokolliert werden:

ps -axo pid,ppid,state,etime,command >"$run_dir/processes-after.txt"
pgrep -P "$$" >"$run_dir/direct-children.txt" 2>/dev/null || true

pgrep -P zeigt nur direkte Kindprozesse und erfasst daher nicht alle Nachfahren. Zuverlässiger ist es, jeden Hilfsprozess durch das Skript zu starten und dessen PID zu registrieren. So können etwa Log-Forwarder, Test-Agenten und Portweiterleitungen jeweils in einem Array erfasst und bei der Bereinigung einzeln geprüft werden. Beenden Sie nicht anhand eines Prozessnamens sämtliche gleichnamigen Prozesse auf dem System. Selbst auf einem dedizierten physischen Mac mini können gleichzeitig langlebige Prozesse laufen, die ein Entwickler bewusst gestartet hat.

Erlaubt die Pipeline parallele Ausführungen, müssen Workspace, Ergebnisverzeichnis, Simulator-Gerätesatz und temporäres Verzeichnis jeweils die Jobnummer enthalten. Ein sauberer Abbruch regelt lediglich den Lebenszyklus der Prozesse. Er kann nicht verhindern, dass sich mehrere Jobs gegenseitig Daten überschreiben, wenn sie dasselbe veränderliche Verzeichnis verwenden.

Den Abbruchpfad mit drei Arten von Probeläufen prüfen

Führen Sie vor dem Produktivbetrieb mindestens drei Probeläufe durch. Brechen Sie den ersten während der Auflösung von Abhängigkeiten ab und prüfen Sie, ob Paketmanager und Logprozesse beendet werden. Brechen Sie den zweiten während der Kompilierung ab und stellen Sie sicher, dass die Builddatenbank nicht fälschlich vom nächsten Job verwendet wird. Brechen Sie den dritten während eines Testlaufs ab und prüfen Sie, ob das Ergebnis-Bundle lesbar bleibt und keine simulatorbezogenen Prozesse den Gerätesatz dieses Jobs weiter belegen.

Kontrollieren Sie bei jedem Probelauf die folgenden Punkte:

Wenn Sie langfristige CI-Workloads auf Cloud-Macs von RentMini ausführen, sollten die Abbruch-Probeläufe außerdem in die Abnahme-Checkliste nach Runner-Updates aufgenommen werden. Prüfen Sie die aktuell verfügbaren Knoten und Konfigurationen ausschließlich in der Konsole. Das Skript selbst sollte weder von einem Standort oder Rechnernamen noch von einem fest vorgegebenen Workspace-Pfad abhängen. Ziel ist nicht ein möglichst leer wirkendes Verzeichnis, sondern eine klar begrenzte und nachvollziehbare Beendigung, nach der der nächste Build in einem eindeutig definierten Zustand starten kann.

Häufig gestellte Fragen

Soll ein abgebrochener CI-Job sofort kill -9 verwenden?

Nein. Zuerst sollte TERM an den Hauptprozess gehen. Eine begrenzte Frist lässt ihm Zeit, Dateien zu schließen und Ergebnisse zu schreiben. KILL ist nur nach Ablauf dieser Frist sinnvoll.

Welche Daten sollten nach einem abgebrochenen Xcode-Build bleiben?

Bewahren Sie das xcodebuild-Protokoll, vorhandene xcresult-Pakete, den Abschlussstatus und einen Prozess-Snapshot auf. Die Exit-Bereinigung darf diese Diagnosequellen nicht pauschal löschen.

Cloud-Mac nach Bedarf nutzen

Exklusiven physischen Mac mini für den nächsten Build vorbereiten

Wählen Sie Konfiguration, Mietdauer und Standort. Jede Bestellung umfasst ein exklusives physisches Gerät. Der tatsächlich verfügbare Status wird in Echtzeit über die Konsole angezeigt.

Konfiguration auswählen und mieten