Параллельные и устаревшие выпуски CI
Одиночное переключение current мы уже умеем описывать. Теперь представим два конвейера: первый получил изменение статьи, второй — последующее исправление. Второй завершился быстрее и опубликовал новый результат. Если первый затем безусловно выполнит свою команду, сайт вернётся к устаревшему содержимому, хотя оба задания покажут успех. Это ошибка порядка решений, а не повреждение файлов.
Для release-lab разберём две отдельные защиты. Сериализация не допускает одновременного переключения. Проверка свежести запрещает более старому кандидату заменить уже принятый новый. Их нельзя считать взаимозаменяемыми: последовательная очередь способна совершенно аккуратно публиковать устаревшие версии одну за другой.
Что именно конкурирует
Пусть учебные конвейеры имеют номера 101 и 102 и создают уникальные артефакты. Упаковка может выполняться параллельно: она не меняет публичный сайт. Критический участок начинается там, где издатель читает текущий выпуск, принимает решение и меняет current. Если проверку свежести выполнить до входа в общий блок, между проверкой и переключением другой издатель успеет опубликовать новый результат. Значит, сравнение и изменение должны находиться под одним механизмом исключения.
В GitLab resource_group позволяет ограничить одновременное выполнение заданий, использующих один ресурс. В примере ресурсом считаем production-публикацию этого сайта. Он не относится к сборке всех проектов компании. Правила очереди и режим обработки уточняются отдельно; наличие группы не означает автоматический выбор последнего исходного изменения. Первичная документация GitLab resource groups.
# Фрагмент будущего задания, не готовая автоматическая публикация.
deploy_production:
stage: deploy
resource_group: release-lab-production
environment:
name: production
url: https://example.com
when: manual
Для рабочего задания понадобятся зависимости от конкретного артефакта, разрешённые правила запуска и команда утверждённого издателя. Этот фрагмент показывает только сериализацию. В GitLab также есть защита от устаревших заданий развёртывания, связанная с окружениями. Перед включением проверяют актуальные условия и взаимодействие с повтором заданий или откатом, а не полагаются на одну универсальную настройку. Документация безопасности развёртываний GitLab.
У этой настройки GitLab возраст определяется временем старта deployment job, а не временем коммита. Поэтому её порядок может отличаться от порядка номеров конвейеров в нашем серверном счётчике. Например, поздно запущенное ручное задание старого конвейера меняет условия допуска другого задания. Не объединяйте две защиты в обещание, что сервис всегда выберет последний коммит: сначала согласуйте смысл возраста и порядок ручного запуска.
Сервер тоже имеет состояние
Ручной издатель или другой инструмент может обойти очередь GitLab. Поэтому на VPS все способы выбора выпуска должны пользоваться общим серверным механизмом. Для учебного варианта возьмём flock и запись максимального принятого номера конвейера. Номера сравниваются только внутри одного согласованного проекта и потока production. Нельзя переносить порядок между независимыми GitLab-проектами или считать номер доказательством качества статьи. Первичная справка flock из util-linux.
Паспорт артефакта содержит release и числовой pipeline_id; упаковщик сохраняет его из CI_PIPELINE_ID. Перед допуском проверяют соответствие аргументов паспорту и нахождение каталога внутри разрешённого releases. Новизна означает «не старее уже принятого состояния», а не «самое последнее ещё не завершившееся изменение ветки». Для запрета любого отставания от текущего решения понадобится дополнительная проверка утверждённого кандидата или состояния защищённой ветки.
Минимальный критический участок
Листинг ниже показывает ядро для уже подготовленного и проверенного кандидата. Его имя и номер заранее проверены по паспорту; начало сценария допускает только фиксированный серверный префикс и согласованный формат. Это не универсальный обработчик произвольного пути из запроса. Все авторизованные издатели должны обращаться к тому же lock-файлу, иначе блокировка не ограничит их действия.
До допуска заданий счётчик должен быть инициализирован отдельно под той же блокировкой, при остановленных издателях. Значение берут из доверенных паспортов и журнала принятых решений: при текущем выпуске 102 оно не может быть ниже 102, а незавершённое намерение могло уже принять более высокий номер. Ноль допустим только для подтверждённого состояния без прежних принятых генераций, а не по факту отсутствия файла. Если счётчик утрачен и историю нельзя восстановить, публикация останавливается до согласования состояния. Автоматически начинать с нуля нельзя.
# До этого фрагмента проверены паспорт, путь и числовой generation.
set -euo pipefail
base=/srv/release-lab
exec 9>"$base/deploy.lock"
flock -x 9
if ! test -f "$base/max-generation"; then
printf '%s\n' 'Generation state is absent; initialize or recover it first' >&2
exit 1
fi
last=$(cat "$base/max-generation")
[[ "$generation" =~ ^[1-9][0-9]*$ ]]
[[ "$last" =~ ^[0-9]+$ ]]
if test "$generation" -le "$last"; then
printf '%s\n' 'Candidate is not newer than accepted generation' >&2
exit 1
fi
test -L "$base/current"
next="$base/.current.$generation.next"
test ! -e "$next"
test ! -L "$next"
ln -s "$base/releases/$candidate" "$next"
printf '%s\n' "$generation" > "$base/.generation.next"
mv -Tf "$base/.generation.next" "$base/max-generation"
mv -Tf "$next" "$base/current"
Счётчик записан до выбора ссылки намеренно. Если процесс остановится между операциями, следующий более старый выпуск будет отклонён, хотя новый мог ещё не стать текущим. Такое состояние требует чтения журнала и согласования, а не автоматического продолжения по догадке. Две операции не образуют транзакцию и не обещают устойчивость к любому отказу питания. Для более строгой гарантии понадобится транзакционное хранилище или другой проверенный протокол публикации.
Для разбора такого обрыва нужен журнал намерения. Под блокировкой записывают ожидаемый прежний current, кандидата, номер и фазу операции. После переключения добавляют фактическую цель и результат внешней проверки. Если соединение прервалось, новый оператор сначала берёт ту же блокировку и сопоставляет журнал с реальной ссылкой и паспортом. Например, счётчик уже 102, а ссылка ещё соответствует 101: выбор не завершён, но старые задания всё равно не следует допускать автоматически. Оператор может закончить утверждённую публикацию либо оставить известный выпуск и записать отдельное решение. Сначала устанавливают состояние, затем выполняют действие.
Если фактическая ссылка не совпадает ни с прежней, ни с намеренной целью, значит, в истории присутствует другое изменение. Нельзя слепо возобновлять последнюю команду из файла. В таком случае согласуют журнал с тем, кто выбирал выпуск, и проверяют единство всех путей публикации. При ручном откате запись максимальной генерации остаётся прежней, а намерение возврата содержит отдельную причину и ожидаемый текущий номер. Так незавершённая публикация и осознанный возврат не выглядят одинаково и не открывают доступ забытому конвейеру.
Что произойдёт в нашем примере
Если 102 первым вошёл в критический участок, запись становится 102 и ссылка выбирает его выпуск. Позднее 101 сравнивает номер под той же блокировкой и получает отказ. Его файлы могут оставаться подготовленными, но публичный сайт не меняется. Отказ означает нормальную защиту от устаревшего решения, а не обязательную ошибку сборки. В журнале полезно различать эти результаты, чтобы оператор не повторял старое задание ради зелёного статуса.
Если 101 вошёл раньше, он может быть принят, а затем заменён 102. Это соответствует определённому правилу возраста, но не гарантирует, что посетители вообще не увидят промежуточную версию. Если проект запрещает такое поведение, допуск связывают с отдельно утверждённым актуальным кандидатом. Сначала формулируют требование, затем выбирают механизм. Удобная очередь не должна незаметно выдавать более сильное обещание.
Откат — отдельное намерение
Возврат к прежнему артефакту по определению выбирает старое содержание. Он не должен случайно отключать проверку свежести всем следующим заданиям. Для отката создают отдельную разрешённую операцию, которая проверяет ожидаемый текущий выпуск, берёт ту же блокировку и пишет причину в журнал. Максимальную принятую генерацию не уменьшают без особого согласованного решения: иначе забытый конвейер снова получит право заменить сайт.
Следующий обычный выпуск имеет новый номер, даже если исходники в нём возвращают прежний текст. Это отличается от повторного запуска старого publish-задания. Такая модель делает намерение видимым: новое решение восстановило содержание, а история не была переписана. Проверка основного домена после действия остаётся обязательной по смыслу, поскольку lock и счётчик подтверждают порядок выбора, а не правильность HTTP-ответов.
Теперь параллельные сборки и выбор production разделены. Мы получили правила для конкуренции, свежести и осознанного возврата. Следующий урок добавит внешний кеш, который может продолжать показывать прежний выпуск даже при совершенно правильном серверном порядке.