Облачный Mac, который ещё вчера успешно завершил сборку, сегодня может принять следующее задание и лишь затем завершиться с ошибкой из-за нехватки места на диске, изменённого пути к Xcode, остаточных данных симулятора или зависшего дочернего процесса. К этому моменту pipeline уже займёт слот выполнения, а реальная причина сбоя затеряется среди журналов установки зависимостей, компиляции и повторных попыток. Надёжнее установить входной контроль до того, как планировщик выдаст задание: runner принимает его только после успешной проверки, а при сбое покидает очередь и сохраняет снимок состояния.
Проверяйте состояние до получения задания
Входная проверка должна быть быстрой, детерминированной и не иметь побочных эффектов. Это не полная диагностика и не повод «заодно исправить» машину. Достаточно ответить на пять вопросов:
- Существует ли путь к инструментам разработки и запускается ли
xcodebuild. - Достаточно ли свободного места на рабочем томе.
- Не мешает ли давление на память запуску нового задания.
- Может ли служба симуляторов вернуть список устройств и runtime-сред.
- Не остались ли после предыдущего задания управляемые процессы или файл блокировки.
Runner на VMCache может управляться самостоятельно размещённым агентом, планировщиком или собственной системой очередей команды. Независимо от выбранной схемы входной контроль должен выполняться до «получения задания», а не первым шагом сценария сборки. Во втором случае задание уже будет считаться неудачным и исказит статистику успешности и повторных запусков.
Правильная реакция на сбой проверки — временно прекратить приём заданий и сохранить диагностические данные, а не немедленно удалять кэш, завершать все процессы или перезагружать всю машину.
Создайте проверяемый сценарий контроля состояния
Приведённый ниже сценарий только считывает состояние и возвращает ненулевой код, если runner не должен принимать задание. Пороговые значения следует подбирать с учётом размера проекта, а не использовать как единую константу для всех 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 -rf, killall и автоматическое переключение Xcode. На этапе входного контроля нужно оценивать состояние, а не изменять его. Тогда даже ошибка конфигурации самой проверки не увеличит масштаб последствий.
Закрепите семантику кодов завершения
Рекомендуется определить 0 как готовность принимать задания, 20 — как несоответствие среды требованиям, а 30 — как невозможность выполнить саму проверку. Получив 20, планировщик переводит Runner в состояние паузы; получив 30, сообщает о сбое инфраструктуры проверки. Не возвращайте 1 для всех исключительных ситуаций, иначе будет невозможно отличить неисправность узла от ошибки сценария.
Проверяйте диск и память, но не очищайте их во время проверки
Порог свободного места должен учитывать пиковый объём исходного кода, зависимостей, DerivedData, архивов и временных файлов одного задания. Его можно определить по пиковым значениям успешно выполненных заданий, добавив резерв для отката. Не ограничивайтесь проверкой корневого тома: если рабочая область, кэш или данные симуляторов находятся на других томах APFS, выполните df -Pk для каждого из них.
При проверке памяти не следует ориентироваться только на объём «свободной памяти». macOS активно использует память для файлового кэша, поэтому важнее отслеживать устойчивое давление на память и swap-активность. Лёгкая входная проверка может сохранять вывод memory_pressure и vm_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
Не отклоняйте задание на основании единичного значения счётчика страниц памяти. Надёжнее сначала записать состояние и блокировать приём только при наличии условия, которое уже подтверждённо приводит к сбою текущей нагрузки. Для MLX-инференса или крупных задач линковки требования к unified memory следует задавать в отдельной политике соответствующей очереди, а не применять один порог ко всем заданиям.
Выявляйте остаточные задания, не завершая системные процессы по ошибке
При поиске остаточных процессов необходимо учитывать их принадлежность. Глобальное завершение процессов только по имени xcodebuild, swift или Simulator может прервать интерактивный сеанс или задание другого исполнителя. Безопаснее, чтобы каждое задание создавало файл блокировки с PID, номером задания и временем запуска, а при завершении удаляло его через trap.
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 как «проверяемый», выполнить сценарий, а затем атомарно переключить его в состояние «готов принимать задания» или «изолирован». Если агент не поддерживает атомарную смену состояния, сначала приостановите получение заданий, затем выполните проверку и только после этого возобновите приём. Так новое задание не попадёт на Runner во время проверки.
Действия по восстановлению следует разделить по уровню риска:
- Старый файл блокировки, PID не существует: заархивировать файл и снять блокировку.
- Размер восстанавливаемого каталога превышает установленный лимит: убедиться в отсутствии активных заданий и очистить его с учётом принадлежности каталога.
- Служба симуляторов не отвечает: сохранить диагностические данные, затем выполнить утверждённую командой процедуру восстановления службы.
- Неверный путь к Xcode или аномальное состояние диска: сохранить изоляцию до ручной проверки.
- Одна и та же неисправность возникает подряд: прекратить автоматическое восстановление и сохранить несколько последних снимков для сравнения.
В завершение проведите учебную проверку отказа самого входного контроля: временно повысьте минимальный порог свободного места и убедитесь, что Runner покидает очередь; верните прежнее значение и проверьте, что он снова начинает принимать задания. Затем создайте старый файл блокировки и убедитесь, что система только изолирует узел, не завершая процессы по ошибке. Ценность проверки состояния не в том, чтобы охватить все возможные сбои, а в том, чтобы обнаружить их до выдачи задания и оставить достаточно ясные данные для следующего шага.
Часто задаваемые вопросы
Нужно ли запускать полную диагностику перед каждой сборкой?
Перед каждым заданием достаточно быстрых проверок. Долгие тесты лучше выполнять периодически или после сбоя, чтобы не задерживать очередь.
Можно ли удалить весь DerivedData при нехватке места?
Нет. Сначала остановите приём заданий, затем удаляйте только воспроизводимые каталоги, которые не принадлежат активным сборкам.
Следует ли автоматически перезапускать runner после ошибки?
Только если причина известна, а восстановление идемпотентно. Повторяющиеся ошибки диска и неверный путь Xcode требуют сохранения диагностики и ручной проверки.
Выберите облачный Mac для сборки, тестирования и инференса MLX
Выберите M4, объём памяти, хранилище, узел и срок аренды под свои задачи. Каждая аренда включает выделенный физический компьютер, а не виртуальную машину.