Инженерный блог SureVM

Атомарная блокировка задач и защита от повторных сборок в Cloud Mac CI

Атомарная блокировка задач и защита от повторных сборок в Cloud Mac CI

Когда один и тот же коммит одновременно запускается повторной попыткой Webhook, ручным перезапуском и заданием по расписанию, две сборки Xcode могут с разницей в несколько секунд попасть в одно рабочее пространство. Они не обязательно завершатся с ошибкой сразу: гораздо чаще оба процесса одновременно изменяют DerivedData, перезаписывают каталог архивов и в итоге вызывают нерегулярную ошибку, которую трудно воспроизвести. Для постоянно работающего облачного Mac такие повторные запуски стоит предотвращать заранее — зачастую это важнее, чем обработка единичного сбоя команды.

Сначала определите ресурс, которому действительно нужна взаимная блокировка

Не следует сразу устанавливать глобальную блокировку на весь узел. Область блокировки должна определяться общим объектом записи, а не названием репозитория. Если две задачи совместно используют хотя бы один из следующих ресурсов, они должны использовать один ключ блокировки:

И наоборот, если тестовые шарды используют отдельные checkout, DerivedData и наборы устройств симулятора, их можно выполнять параллельно с разными ключами блокировки. Рекомендуется составлять ключ из репозитория, Scheme и действия, например client-release-archive. Не включайте в него только имя ветки: две ветки всё равно могут конкурировать за один каталог архивов.

Сценарий Рекомендуемая гранулярность блокировки Допускается параллельное выполнение
Архивирование с общим DerivedData Репозиторий + Scheme + archive Нет
Тестовые шарды с отдельными наборами устройств Репозиторий + номер шарда Да
Загрузка одноимённых артефактов релиза Приложение + канал публикации Нет
Статический анализ без записи Обычно блокировка не нужна Да

Блокировка защищает общие побочные эффекты, а не CPU. Попытка обойти необходимую блокировку ради повышения параллелизма обычно лишь превращает время ожидания во время, потраченное на очистку.

Создайте атомарную точку конкуренции с помощью mkdir

Встроенную в macOS команду mkdir можно использовать как простую и надёжную точку конкуренции: когда несколько процессов одновременно пытаются создать один и тот же путь, успешным будет только один. В отличие от подхода «сначала проверить наличие файла, затем записать PID», здесь нет окна гонки между проверкой и записью.

Следующий скрипт создаёт для блокировки случайный токен владельца и обновляет heartbeat каждые 15 секунд. При завершении удалить блокировку сможет только процесс, токен которого по-прежнему совпадает. Это не позволяет старой задаче случайно удалить блокировку новой.

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

Код выхода 75 однозначно указывает, что задача временно не может быть выполнена. На уровне очереди можно настроить отложенный повторный запуск, но нельзя бесконечно перезапускать задачу в течение одной секунды. Все пути должны быть заключены в кавычки, а корневой каталог блокировок следует разместить там, где текущая учётная запись имеет права на запись и где команды очистки репозитория его не удалят.

Не определяйте устаревшую блокировку только по PID

PID-файл полезен для диагностики, но сам по себе не подтверждает владение блокировкой. macOS может повторно использовать PID, а после перезапуска узла число из старого файла может соответствовать совершенно другому процессу. Поэтому процедура очистки должна проверять как минимум три условия:

  1. Превысило ли время с последнего изменения heartbeat максимальную продолжительность выполнения конвейера;
  2. Помечена ли задача в очереди как завершённая или превысившая лимит времени;
  3. Продолжают ли связанные процессы сборки записывать данные в защищаемый каталог.

Устаревший heartbeat означает лишь, что ситуацию нужно проверить, а не то, что блокировку уже можно перехватить. Если сборка приостановилась из-за блокировки дискового ввода-вывода, процесс всё ещё может удерживать файлы. Принудительный запуск второй сборки в этот момент только увеличит масштаб повреждений. Надёжнее поручить очистку отдельной задаче восстановления, а не позволять ожидающей блокировку задаче самостоятельно удалять состояние предшественника.

Добавьте в блокировку наблюдаемые поля

В файле owner можно дополнительно хранить хеш коммита, номер задачи, Scheme и рабочий каталог, но нельзя записывать токены доступа, данные для подписи и другие конфиденциальные значения. При ошибке журнал должен как минимум содержать ключ блокировки, время запуска и номер задачи. Это позволит инженерам отличить обычное ожидание в очереди от аварийного завершения предыдущего запуска.

Разделяйте ожидание, тайм-аут и отмену

Взаимная блокировка не означает, что все последующие задачи должны завершаться с ошибкой. На практике обычно применяют три стратегии очереди:

Независимо от выбранной стратегии ожидание должно иметь верхний предел. Тайм-аут ожидания завершает только ожидающую задачу и не должен удалять блокировку владельца. При отмене владельца сначала следует отправить обычный сигнал завершения, чтобы trap выполнил очистку. Только если процесс не удаётся завершить, нужно переходить к ручному или отдельному процессу восстановления.

Кроме того, не выполняйте внутри блокировки несвязанную работу. Загрузку кода, проверки без записи и подготовку параметров можно выполнить до получения блокировки. Блокировку следует захватывать только перед этапом, который действительно изменяет общие каталоги. Это сокращает критическую секцию и уменьшает перегрузку очереди.

Проведите проверку отказоустойчивости перед вводом в эксплуатацию

Сначала дважды подряд запустите скрипт в задаче, не связанной с публикацией, и убедитесь, что только один процесс переходит к xcodebuild. Затем по отдельности смоделируйте нормальное завершение, ошибку команды и получение сигнала завершения, каждый раз проверяя, удалён ли каталог блокировки. Наконец, вручную оставьте старую блокировку и убедитесь, что ожидающая задача лишь сообщает о занятости и не пытается самостоятельно её удалить.

При приёмке используйте следующий контрольный список:

После этих проверок повторные запуски станут понятными событиями ожидания в очереди, а не скрытыми гонками, случайно повреждающими состояние сборки. Если впоследствии потребуется увеличить параллелизм, сначала разделите общие каталоги и цели публикации, а затем уточните ключи блокировок — не удаляйте взаимную блокировку напрямую.

Часто задаваемые вопросы

Почему недостаточно хранить только PID процесса сборки?

PID может быть повторно выдан другому процессу, а файл может остаться после сбоя. Атомарное создание каталога определяет победителя, а случайный токен подтверждает владельца.

Нужно ли блокировать все Xcode-сборки одним глобальным ключом?

Нет. Один ключ нужен только задачам с общими рабочими каталогами, DerivedData, архивами или местом публикации. Полностью изолированные задачи можно выполнять параллельно.

Можно ли удалить блокировку сразу после остановки heartbeat?

Нет. Сначала подтвердите превышение лимита времени и отсутствие процессов, записывающих данные в защищаемый каталог, затем выполните удаление отдельным этапом очистки.

SureVM облачный Mac

Выбирайте выделенные физические узлы Mac для сборки, разработки и экспериментов

Сравните две конфигурации Mac mini M4, четыре срока аренды и пять доступных узлов, а затем выберите вариант под свой рабочий процесс.

Выбрать тариф