Зачем нужен контроль выполнения шагов
По умолчанию шаг конвейера выполняется столько, сколько потребуется. Если процесс под капотом завис, jenkins pipeline будет терпеливо ждать - минуты, часы, до перезапуска контроллера. Агент при этом занят, слот исполнителя не освобождается. Поэтому первое правило надёжного конвейера: любой потенциально долгий или сетевой шаг должен иметь верхнюю границу по времени и, если операция нестабильна, политику повтора.
Все четыре героя урока - это step-функции, которые принимают замыкание (блок кода) и оборачивают его своим поведением. Их можно вкладывать друг в друга, и именно из комбинаций рождается надёжность.

jenkins timeout: прервать зависший шаг
Шаг timeout задаёт лимит времени на выполнение вложенного блока. Если лимит превышен, Jenkins прерывает блок и сборка получает статус ABORTED. Параметры: time (число), unit (SECONDS, MINUTES, HOURS - по умолчанию минуты) и activity (если true, таймер считает не общее время, а время без вывода в лог - удобно для зависших, но молчащих процессов).
Код: Выделить всё
pipeline {
agent any
stages {
stage('Integration tests') {
steps {
timeout(time: 10, unit: 'MINUTES') {
sh './gradlew integrationTest'
}
}
}
}
}
Код: Выделить всё
stage('Deploy') {
options {
timeout(time: 15, unit: 'MINUTES')
}
steps {
sh './deploy.sh production'
}
}
Код: Выделить всё
timeout(time: 5, unit: 'MINUTES', activity: true) {
sh './run-noisy-task.sh'
}
jenkins retry: повторить флапающий шаг
Шаг retry повторяет вложенный блок до N попыток, если внутри вылетело исключение. Первая попытка плюс повторы: retry(3) - это до трёх запусков всего. Между попытками есть небольшая стартовая пауза (по умолчанию около 250 мс). Это лекарство от моргающей сети, медленно поднимающегося контейнера, временно недоступного registry - всего, что падает не из-за бага, а из-за внешней нестабильности.
Код: Выделить всё
stage('Push image') {
steps {
retry(3) {
sh 'docker push registry.example.com/app:$BUILD_NUMBER'
}
}
}
Связка timeout + retry для надёжного деплоя
Самое ценное в этом jenkins pipeline - комбинация. Нам нужно: повторить деплой при сбое, но чтобы каждая отдельная попытка не висла вечно. Порядок вложения решает всё.
Код: Выделить всё
stage('Deploy with safety net') {
steps {
retry(3) {
timeout(time: 5, unit: 'MINUTES') {
sh './deploy.sh staging'
}
}
}
}
А вот если поменять местами - timeout снаружи, retry внутри - смысл другой и почти всегда нежелательный: общий лимит времени накрывает все попытки разом. Сработал внешний timeout - и retry уже не получит шанса, потому что прерывание таймаута гасит весь блок. Запомни как правило: для надёжного деплоя timeout кладём внутрь retry, а не наоборот.
sleep и jenkins waitUntil: ждать готовности
Шаг sleep - тупая пауза на заданное время: sleep(time: 10, unit: 'SECONDS') или коротко sleep 10 (секунды по умолчанию). Использовать sleep как способ "дождаться, пока сервис поднимется" - антипаттерн: либо ждёшь слишком мало и ловишь гонку, либо слишком много и тормозишь конвейер. Для ожидания условия есть jenkins waitUntil.
Шаг waitUntil выполняет блок повторно, пока тот не вернёт true. Вернул false - подождал и попробовал снова. Пауза между проверками растёт от стартовой (initialRecurrencePeriod, по умолчанию 250 мс) до максимума в 15 секунд. Параметр quiet: true глушит лог на каждой итерации, чтобы не засорять вывод. Сам по себе waitUntil ждёт бесконечно - поэтому его ВСЕГДА оборачивают в timeout.
Код: Выделить всё
stage('Wait for service health') {
steps {
timeout(time: 3, unit: 'MINUTES') {
waitUntil(quiet: true) {
script {
def code = sh(
script: 'curl -sf -o /dev/null -w "%{http_code}" http://staging:8080/health',
returnStdout: true
).trim()
return code == '200'
}
}
}
}
}
Код: Выделить всё
timeout(time: 60, unit: 'SECONDS') {
waitUntil {
return fileExists('/shared/build.lock') == false
}
}
Мягкая обработка: catchError и warnError
Иногда сбой шага не должен ронять всю сборку. catchError выполняет блок и, если внутри ошибка, ставит результат сборки в указанный статус, но даёт конвейеру жить дальше. warnError - частный удобный случай: ловит ошибку и помечает сборку и стейдж как UNSTABLE, выводя твоё сообщение.
Код: Выделить всё
stage('Optional smoke test') {
steps {
warnError('Smoke-тест не прошёл, помечаю как нестабильно') {
sh './smoke.sh'
}
}
stage('Publish report') {
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
sh './upload-report.sh'
}
}
}
}
Типичные грабли
- timeout снаружи retry вместо внутри - повторов фактически нет, внешний таймаут гасит всё.
- retry вокруг не идемпотентного шага - три попытки = три побочных эффекта.
- waitUntil без timeout - конвейер может ждать вечно, исполнитель занят навсегда.
- sleep вместо waitUntil для ожидания сервиса - гонки или потерянное время.
- Тело waitUntil бросает исключение вместо возврата false - блок падает на первой же неудаче.
- Слишком жёсткий timeout на легитимно долгом шаге - ложные аборты; используй activity: true там, где работа долгая, но не молчаливая.
Мини-лаба
- Создай pipeline-джобу. Оберни шаг sh 'sleep 120' в timeout(time: 5, unit: 'SECONDS') и убедись, что сборка завершилась как ABORTED через 5 секунд.
- Сделай флапающий шаг: sh 'if [ $((RANDOM % 2)) -eq 0 ]; then exit 1; fi' и оберни в retry(5). Запусти несколько раз, посмотри в логе повторы.
- Собери надёжный деплой: retry(3) { timeout(time: 30, unit: 'SECONDS') { ... } }. Поменяй местами вложение и сравни поведение при зависании.
- Подними локально любой сервис с /health и дождись его через waitUntil под timeout. Затем погаси сервис и проверь, что таймаут срабатывает корректно.
- Почему для надёжного деплоя timeout кладут внутрь retry, а не наоборот?
- Какой статус получает сборка при срабатывании timeout и как этот статус смягчить?
- Чем waitUntil лучше связки sleep + проверка, и почему его всегда оборачивают в timeout?
- В чём разница между catchError и warnError по влиянию на результат сборки и стейджа?
timeout, retry, sleep и waitUntil - это базовый набор для управления потоком в конвейере. timeout защищает от зависаний, retry глушит флап нестабильных операций, waitUntil ждёт реальной готовности вместо слепых пауз, а catchError и warnError позволяют не ронять всю сборку из-за второстепенного сбоя. Ключевая идея - правильное вложение: timeout внутри retry для деплоя, waitUntil всегда под timeout. Освой эти комбинации - и ночные алерты про зависшие билды уйдут из твоей жизни.