クラウドMac CI Runnerのジョブ受信前ヘルスチェック

クラウドMac CI Runnerのジョブ受信前ヘルスチェック

あるクラウドMacが昨夜は問題なくビルドを完了していても、今日はディスク空き容量の不足、Xcode選択パスの変更、シミュレータの残留状態、ゾンビ化した子プロセスなどが原因で、次のジョブを受信した後に失敗する可能性があります。その時点ではすでにパイプラインの実行スロットが占有されており、本当のエラーも依存関係のインストール、コンパイルログ、再試行メッセージに埋もれてしまいます。より確実なのは、スケジューラがジョブを割り当てる前に受け入れゲートを設けることです。チェックに合格した場合だけジョブを受信し、失敗した場合はキューから外れて状態のスナップショットを残します。

ジョブ受信前にヘルスチェックを配置する

受け入れチェックは短時間で完了し、結果が確定的で、副作用がないものでなければなりません。完全なシステム診断ではなく、その場でマシンを「ついでに修復」するためのものでもありません。確認するのは、次の5点に絞ることを推奨します。

  1. 開発ツールのパスが存在し、xcodebuildを起動できるか。
  2. 作業ボリュームに十分な空き容量があるか。
  3. メモリプレッシャーが新しいジョブに影響する状態になっていないか。
  4. シミュレータサービスがデバイスとランタイムの一覧を返せるか。
  5. 前のジョブが管理対象プロセスやジョブロックを残していないか。

VMCache上のRunnerは、セルフホスト型エージェント、定期スケジューラ、またはチーム独自のキューマネージャーから制御できます。どの方式を採用する場合でも、ゲートはビルドスクリプトの最初のステップではなく、「ジョブ受信」の前に配置してください。ビルドスクリプト内で失敗すると、すでに1件の失敗ジョブとして記録され、成功率や再試行の統計を汚してしまいます。

ヘルスチェックに失敗した場合の正しい対応は、ジョブ受信を一時停止して証拠を保存することです。キャッシュを即座に削除したり、すべてのプロセスを強制終了したり、マシン全体を再起動したりすることではありません。

まず監査可能なチェックスクリプトを用意する

以下のスクリプトは状態を読み取るだけで、ジョブ受信を拒否する場合はゼロ以外の終了コードを返します。しきい値はプロジェクトの規模に応じて調整し、すべてのRunnerに共通する固定値としてそのまま適用しないでください。

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

スクリプト内でsudo rm -rfkillallを実行したり、Xcodeを自動で切り替えたりしないでください。受け入れ段階の役割は判定であり、現場の状態を変更することではありません。こうしておけば、ゲート自体の設定に誤りがあった場合でも影響の拡大を防げます。

終了コードの意味を固定する

0はジョブ受信可能、20は環境が要件を満たしていない状態、30はチェッカー自体が処理を完了できない状態として定義することを推奨します。スケジューラは20を受け取ったらRunnerを一時停止し、30を受け取ったらチェック基盤の障害として報告します。すべての異常で1を返す設計にすると、ノード障害とスクリプトエラーを区別できません。

ディスクとメモリを確認しても、その場ではクリーンアップしない

ディスク容量のしきい値には、1回のジョブで必要になるソースコード、依存関係、DerivedData、アーカイブ、一時ファイルのピーク使用量を含める必要があります。過去に成功したジョブのピーク値から逆算し、さらにロールバック用の余裕を確保できます。また、確認対象をルートボリュームだけに限定してはいけません。ワークスペース、キャッシュ、シミュレータデータが別のAPFSボリュームにある場合は、それぞれに対してdf -Pkを実行してください。

メモリチェックでは、単に「空きメモリ」だけを読み取るべきではありません。macOSはファイルキャッシュにメモリを積極的に利用するため、継続的なメモリプレッシャーとスワップ動作の方が重要です。軽量なゲートではmemory_pressurevm_statの出力を保存し、監視側で傾向を比較できます。

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

1回分のページ数だけを基準にジョブを拒否しないでください。より確実なルールは、まずゲートで状態を記録し、現在のワークロードを確実に失敗させると確認済みの条件だけでジョブ受信を停止することです。MLX推論や大規模なリンク処理については、ユニファイドメモリ要件を対応するキューの個別ポリシーに記載し、すべてのジョブで1つのしきい値を共有しないようにしてください。

システムプロセスを誤って終了せず、残留ジョブを識別する

残留状態の検出では、プロセスの所有範囲を限定する必要があります。xcodebuildswiftSimulatorといった名前だけを基準にプロセスを一括終了すると、対話セッションや別のExecutorが実行しているジョブまで停止させる可能性があります。より安全なのは、各ジョブがPID、ジョブ番号、開始時刻を含むロックファイルを作成し、終了時のトラップで削除する方法です。

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

古いロックを検出したら、まず記録されているPIDが現在も存在するか確認し、その後でプロセスの開始時刻とコマンドラインを照合します。PIDは再利用されるため、番号だけを根拠に終了を判断してはいけません。プロセスがすでに存在しない場合は、ロックファイルをチェック時刻とともに診断ディレクトリへ移し、管理された復旧手順によってブロックを解除できます。

シミュレータチェックの範囲を明確にする

simctl listが成功しても、サービスと通信できることを示すだけで、特定のテストターゲットが利用可能だとは限りません。ゲートが検証するのはサービス層までとし、プロジェクト層ではジョブ内で明示的なdestinationを使って対象デバイスを検索する必要があります。受け入れスクリプト内でデバイスを自動作成、削除、消去しないでください。これらの操作には時間がかかり、後続テストのベースラインも変化します。

スケジューラに組み込み、復旧経路を設計する

スケジューラは、まずRunnerを「チェック中」に設定してスクリプトを実行し、その後で状態を「ジョブ受信可能」または「隔離」にアトミックに切り替える必要があります。エージェントがアトミックな状態変更に対応していない場合は、先にジョブ受信を一時停止してからチェックを行い、最後に受信を再開します。これにより、チェック中に新しいジョブが割り込むのを防げます。

復旧処理はリスクに応じて段階分けします。

  1. 古いロックがあり、PIDが存在しない:ロックファイルをアーカイブしてから解除する。
  2. 再生成可能なディレクトリが容量予算を超えている:実行中のジョブがないことを確認し、ディレクトリの所有範囲に従ってクリーンアップする。
  3. シミュレータサービスが応答しない:診断情報を保存してから、チームが承認したサービス復旧手順を実行する。
  4. Xcodeのパスが正しくない、またはディスク状態に異常がある:隔離を維持し、人手で確認する。
  5. 同じ障害が連続して発生する:自動復旧を停止し、直近数回のスナップショットを保存して差分を比較する。

最後に、ゲート自体の障害訓練も実施します。一時的に最小ディスク容量のしきい値を引き上げ、Runnerがキューから外れることを確認してください。しきい値を元に戻したら、Runnerが再びジョブ受信可能な状態になることを確認します。次に古いロックファイルを作成し、システムがノードを隔離するだけで、プロセスを誤って終了しないことを検証します。ヘルスチェックの価値は、あらゆる障害を網羅することではありません。ジョブが割り当てられる前に障害を顕在化させ、次に取るべき対応を判断できるだけの明確な情報を残すことにあります。

よくある質問

すべてのビルド前に完全な診断を実行すべきですか?

ジョブ受信前は低コストな検査だけを実行し、時間のかかる診断は定期実行またはゲート失敗後に回すのが適切です。

ディスク不足時にDerivedDataをすべて削除してよいですか?

推奨しません。まずRunnerをキューから外し、実行中のビルドが使っていない再生成可能なディレクトリだけを所有者と最終利用時刻に基づいて削除します。

チェック失敗後にRunnerを自動再起動すべきですか?

原因が既知で復旧処理が冪等な場合に限ります。ディスク異常の再発やXcode選択ミスでは証拠を保存して調査すべきです。

専用物理ノード

構築・テスト・MLX推論にクラウドMacを選ぶ

実際のタスクに合わせて、M4、メモリ、ストレージ、ノード、契約期間を選択できます。各レンタルには専用物理マシンが割り当てられ、仮想マシンではありません。

プランを選ぶ