Давайте автоматизировать автоматизацию.
Сегодня поговорим о некоторых сложностях и их решениях в работе с Jenkins API.

Jenkins запускает задачи (Jobs) и следит за их пошаговым исполнением. Мы хотим запустить задачу с параметрами (Build) не из Jenkins GUI а из кода, и следить за ее исполнением.
* Оговорка- нижеприведенная задача это упрощение. Смысл в том, чтобы показать несколько проблем и их решение а не конкретную имплементацию у нас.
Деплойнуть среду (или очень много таких сред).
Для этого сначала мы поднимаем инфраструктуру.
Потом заливаем в неё новый код.
И вконце запускаем системные тесты и оповещения.
Параметры среды находятся в Single_Source_Of_Truth.json файле.
(Предполагается, что это знакомо. Просто для уточнения общего языка):
Job — задача, имеет переменные, которые нужно заполнить, чтобы ее запустить.
Executor- Процесс выполняющий задачу (Job а точнее Build).
Agent- Исполнитель задач. Server/Docker Container/AWS Instance и т.д. Управляет Executor-ами.
Jenkins_Node — Дженкинсовый мозг. Управляет задачами и агентами.
Queue_Item — Задача с заполненными параметрами, готовая к запуску. Находится в очереди ожидания.
Queue_Item_Id — Queue_Item идентификатор. Временный номер, может повторяться многократно у разных Queue_Items
Build_Item — Задача направлена к Agent & Executor и начала исполнятся.
Build_Item_Id — Глобальный уникальный идентификатор Build_Item-а.
(1) Клиент (наш код) триггерит задачу в Jenkins_Node.
(2) Jenkins_Node создаёт из неё Queue_Item и возвращает нам Queue_Item_Id.
(3) Мы периодически посылаем запросы по Queue_Item_Id, чтобы получить Build_Item_Id. Пока
наша задача не получила Agent & Executor она не имеет Build_Item_Id. Ждём.
(4) Jenkins_Node назначает Agent, создаёт Build_Item и возвращает нам Build_Item_Id.
(5) Мы делаем запросы по Build_Item_Id ожидая окончания выполнения задачи.
Мы столкнулись со следующими “задачами”.
(Отсортированы не по значимости а по ходу процесса приведённого выше.)
jenkins.build_job(server_team_data)- Обычный HTTP Request. У нас занимал до 1 минуты
При десятках последовательных задач (у нас количество приближается к 100) это было критично.
Решение:
Триггерить параллельно. Каждый запуск- свой thread. Только запуск. После этого они (threads) отмирают. Главный процесс ждет завершения всех пусков и отслеживает статус всех секвентально.
Известная и старая проблема.
Случается, что в какой то момент Jenkins_Node перестаёт узнавать Queue_Item_Id (ещё не вернув нам Build_Item_Id). Тупик. Все дальнейшие запросы бессмысленны. На каждый запрос queueitem.get_build вы получаете ошибку “ID не найден”.
Решение:
Создаём свой глобальный уникальный идентификатор- аргумент задачи (Job argument) Global_Random_Id. Он известен нам заранее. Теперь мы можем найти по нему наш Build_Item не зная его Build_Item_Id. Это превращает Queue_Item_Id в рудимент и он отмирает (только если вы не хотите следить за Jenkins Queue. Лично мы не хотим).
Мы используем AWS Instances как Agents. Дабы не тратить деньги за простой, Jenkins поднимает их по мере надобности. При 100 запущенных задачах и 5 Executors per Agent мы долго ждём пока Jenkins запустит все Agents по мере запуска задач.
Решение:
Перед запуском самих задач мы запускаем нужное количество Agents.
Из за того, что это функционал плагина Jenkins а не самого Jenkins API пришлось в коде симулировать POST Request, который посылает Jenkins GUI, после клика мыши на Provision.
Напоминалка- в (3) мы изменили индивидуальные запросы статуса по Queue_Item_Id на запросы истории билдов по Job_name и в них ищем наш по Global_Random_Id.
В некоторых клиентах каждый запрос по Job_Name посылает всю историю билдов. Когда это доходит до сотен тысяч (у нас 300,000) начинаются аномалии в виде таймаутов и ошибок на прокси по дороге. Да и вообще, зачем?
Решение:
Ограничение количества возвращаемых задач в ответе от Jenkins_Node. Нам для этого пришлось сменить python client.
Чтобы получить статус исполняемой задачи нужно раз в какое то время посылать get_build_info запрос на каждый бегущий Build_Item.
При timeout = 10 и length(list_builds) = 100
Каждые 10 секунд мы делаем 100 запросов.
Так как задачи бегут разное количество времени мы тратим кучу ресурсов в пустую. Если мы решаем изменить timeout=60 возможна ситуация, когда все задачи выполнились но мы спим минуту до того как сообщить об этом. Умножим на 60 программистов в день ожидающих ответ. Получим 1 человеко-час впустую при выполнении одного деплоя в день. А их как минимум три на человека (Testing/Pre-Prod/Prod). Да и зачем Jenkins мурыжить впустую?
Решение:
Динамичный timeout.
Здесь я прошелся только по архитектурным решениям не касаясь мелких ошибок, шероховатостей и оптимизаций. Как результат- нам удалось существенно сократить время затрачиваемое на инфраструктуру. С 40 минут в среднем до 24 минут без предзапуска AWS Agents. После введения последнего, удалось сократить до 19 минут, иногда встречаются и 16- редко правда, но это даёт стимул дальше работать в этом направлении )
Сегодня поговорим о некоторых сложностях и их решениях в работе с Jenkins API.

Пример
Общая идея
Jenkins запускает задачи (Jobs) и следит за их пошаговым исполнением. Мы хотим запустить задачу с параметрами (Build) не из Jenkins GUI а из кода, и следить за ее исполнением.
* Оговорка- нижеприведенная задача это упрощение. Смысл в том, чтобы показать несколько проблем и их решение а не конкретную имплементацию у нас.
Цель
Деплойнуть среду (или очень много таких сред).
Для этого сначала мы поднимаем инфраструктуру.
Потом заливаем в неё новый код.
И вконце запускаем системные тесты и оповещения.
Способ
Параметры среды находятся в Single_Source_Of_Truth.json файле.
Вот как выглядит наш код
Single_Source_Of_Truth.json:
{
backend_server: {count: 4, code_path: Https:github.com/my_backend_code},
mobile_server: {count: 10, code_path: Https:github.com/my_mobile_code},
frontend_server: {count: 5, code_path: Https:github.com/my_frontend_code},
}
list_queue_items = []
for server_type_data in load(Single_Source_Of_Truth.json):
list_queue_items.extend(jenkins.build_job(server_type_data))
list_builds = []
for q_item in list_queue_items:
list_builds.extend(jenkins.get_build(q_item))
time_to_live = time_now + 10 * 60 # 10 min
while time_now < time_to_live and list_builds != []:
for build in list_builds:
if build.finished:
list_builds.pop(build)
sleep(10)
run_system_checks()
send_notification()
Flow со стороны Jenkins
Терминология
(Предполагается, что это знакомо. Просто для уточнения общего языка):
Job — задача, имеет переменные, которые нужно заполнить, чтобы ее запустить.
Executor- Процесс выполняющий задачу (Job а точнее Build).
Agent- Исполнитель задач. Server/Docker Container/AWS Instance и т.д. Управляет Executor-ами.
Jenkins_Node — Дженкинсовый мозг. Управляет задачами и агентами.
Queue_Item — Задача с заполненными параметрами, готовая к запуску. Находится в очереди ожидания.
Queue_Item_Id — Queue_Item идентификатор. Временный номер, может повторяться многократно у разных Queue_Items
Build_Item — Задача направлена к Agent & Executor и начала исполнятся.
Build_Item_Id — Глобальный уникальный идентификатор Build_Item-а.
Flow
(1) Клиент (наш код) триггерит задачу в Jenkins_Node.
(2) Jenkins_Node создаёт из неё Queue_Item и возвращает нам Queue_Item_Id.
(3) Мы периодически посылаем запросы по Queue_Item_Id, чтобы получить Build_Item_Id. Пока
наша задача не получила Agent & Executor она не имеет Build_Item_Id. Ждём.
(4) Jenkins_Node назначает Agent, создаёт Build_Item и возвращает нам Build_Item_Id.
(5) Мы делаем запросы по Build_Item_Id ожидая окончания выполнения задачи.
Сложности и их решения
Мы столкнулись со следующими “задачами”.
(Отсортированы не по значимости а по ходу процесса приведённого выше.)
Задача
(1):jenkins.build_job(server_team_data)- Обычный HTTP Request. У нас занимал до 1 минуты
При десятках последовательных задач (у нас количество приближается к 100) это было критично.
Решение:
Триггерить параллельно. Каждый запуск- свой thread. Только запуск. После этого они (threads) отмирают. Главный процесс ждет завершения всех пусков и отслеживает статус всех секвентально.
Задача
(3):Известная и старая проблема.
Случается, что в какой то момент Jenkins_Node перестаёт узнавать Queue_Item_Id (ещё не вернув нам Build_Item_Id). Тупик. Все дальнейшие запросы бессмысленны. На каждый запрос queueitem.get_build вы получаете ошибку “ID не найден”.
Решение:
Создаём свой глобальный уникальный идентификатор- аргумент задачи (Job argument) Global_Random_Id. Он известен нам заранее. Теперь мы можем найти по нему наш Build_Item не зная его Build_Item_Id. Это превращает Queue_Item_Id в рудимент и он отмирает (только если вы не хотите следить за Jenkins Queue. Лично мы не хотим).
Так выглядит наш новый код:
triggered_builds = []
for server_type_data in load(Single_Source_Of_Truth.json):
server_type_data.global_random_id = random()
jenkins.build_job(server_type_data)
Triggered_builds.extend(server_type_data)
started_builds = []
time_to_live = time_now + 10 * 60 # 10 min
while time_now < time_to_live and triggered_builds != []:
for build in triggered_builds:
builds_history = jenkins.get_job_buids_history(build.job_name)
if build in builds_history:
triggered_builds.pop(build)
started_builds.extend(build)
sleep(10)
time_to_live = time_now + 10 * 60 # 10 min
while time_now < time_to_live and started_builds != []:
for build in started_builds:
if jenkins.build.finished:
started_builds.pop(build)
sleep(10)
run_system_checks()
send_notification()
Задача
(4):Мы используем AWS Instances как Agents. Дабы не тратить деньги за простой, Jenkins поднимает их по мере надобности. При 100 запущенных задачах и 5 Executors per Agent мы долго ждём пока Jenkins запустит все Agents по мере запуска задач.
Решение:
Перед запуском самих задач мы запускаем нужное количество Agents.
Из за того, что это функционал плагина Jenkins а не самого Jenkins API пришлось в коде симулировать POST Request, который посылает Jenkins GUI, после клика мыши на Provision.
AWS EC2 Provision URL
https://your-jenkins.com/cloud/ec2-Main%20AWS%20account/provision
Задача
(5):Напоминалка- в (3) мы изменили индивидуальные запросы статуса по Queue_Item_Id на запросы истории билдов по Job_name и в них ищем наш по Global_Random_Id.
В некоторых клиентах каждый запрос по Job_Name посылает всю историю билдов. Когда это доходит до сотен тысяч (у нас 300,000) начинаются аномалии в виде таймаутов и ошибок на прокси по дороге. Да и вообще, зачем?
Решение:
Ограничение количества возвращаемых задач в ответе от Jenkins_Node. Нам для этого пришлось сменить python client.
Задача
(5):Чтобы получить статус исполняемой задачи нужно раз в какое то время посылать get_build_info запрос на каждый бегущий Build_Item.
Из нашего примера сверху:
time_to_live = time_now + 10 * 60 # 10 min
while time_now < time_to_live and started_builds != []:
for build in started_builds:
if jenkins.finished(build):
started_builds.pop(build)
sleep(timeout)
При timeout = 10 и length(list_builds) = 100
Каждые 10 секунд мы делаем 100 запросов.
Так как задачи бегут разное количество времени мы тратим кучу ресурсов в пустую. Если мы решаем изменить timeout=60 возможна ситуация, когда все задачи выполнились но мы спим минуту до того как сообщить об этом. Умножим на 60 программистов в день ожидающих ответ. Получим 1 человеко-час впустую при выполнении одного деплоя в день. А их как минимум три на человека (Testing/Pre-Prod/Prod). Да и зачем Jenkins мурыжить впустую?
Решение:
Динамичный timeout.
Пример реализации
Первая итерация спит 100*3 = 300 sec.
Если по истечении 5 минут у нас стало length(list_builds) == 50
(100+50)/2 — среднее количество запросов за итерацию.
(300/timeout) — Количество сэкономленных итераций.
Timeout = 10
(100+50)/2 * (300/timeout)= 75*30 = 2250 Requests экономим за первую итерацию нового кода.
time_seed = 3
while time_now < time_to_live and started_builds != []:
for build in started_builds:
if jenkins.finished(build):
started_builds.pop(build)
sleep(time_seed*length(started_builds))
Считаем плюшки
Первая итерация спит 100*3 = 300 sec.
Если по истечении 5 минут у нас стало length(list_builds) == 50
(100+50)/2 — среднее количество запросов за итерацию.
(300/timeout) — Количество сэкономленных итераций.
Timeout = 10
(100+50)/2 * (300/timeout)= 75*30 = 2250 Requests экономим за первую итерацию нового кода.
Здесь я прошелся только по архитектурным решениям не касаясь мелких ошибок, шероховатостей и оптимизаций. Как результат- нам удалось существенно сократить время затрачиваемое на инфраструктуру. С 40 минут в среднем до 24 минут без предзапуска AWS Agents. После введения последнего, удалось сократить до 19 минут, иногда встречаются и 16- редко правда, но это даёт стимул дальше работать в этом направлении )