fix(ci): прод-деплои в одну группу concurrency — прун одного убивал pull другого (#2950) #2952

Merged
bot-backend merged 1 commit from fix/2950-deploy-docker-lock into main 2026-08-20 07:47:49 +00:00
Collaborator

Закрывает #2950.

Что случилось

20.08 деплой ПТИЦЫ (run 8083, коммит 6bca4f7e) упал за 5 секунд:

unable to lease content: lease does not exist: not found

Причина — по секундам

время (UTC) run что происходит
07:05:05.2 8083 (ПТИЦА) docker compose pull — 7 образов Pulling
07:05:06.0 8083 (ПТИЦА) все 7 Interrupted, unable to lease content
07:05:06.4 8084 (Trade-In) Deleted Images: — отработал docker image prune -af

Оба деплоя ходят по SSH в один докер-демон. Проверено docker ps на VM: стеки gendesign-*, tradein-* и сам forgejo-runner живут на одном хосте.

Группы concurrency были разные — deploy-prod и deploy-tradein-prod — Forgejo их параллелил. cancel-in-progress: false не помогал: он сериализует раны внутри группы, а гонка была между группами.

Почему отказ был тихим

Голова main показывала success. Но зелёным был Trade-In'овый деплой этой головы: последний коммит трогал только tradein-mvp/**, поэтому у него запустился только Deploy Trade-In. Деплой ПТИЦЫ висел на своём, более раннем коммите — с исходом failure. Прод остался на старом коде, #2946 и #2947 не доехали (выкачены отдельным workflow_dispatch).

Почему общая группа, а не flock на хосте

Сначала сделал host-lock: он сериализует только докер-секцию, не заставляя деплой ждать чужих билдов (~6 мин). Потом проверил его на самой VM и отказался:

B1: не смог за 1с, пока A держит       ← взаимное исключение работает
B2: взял лок через 2с после отпускания ← не залипает
B:  НЕ СМОГ после kill -9 держателя    ← а вот тут залипает

Дочерний процесс наследует fd лока; при аварийной смерти job'а (ENOSPC, OOM, отмена) лок остаётся занятым и блокирует прод-деплои на весь таймаут. Планировщик Forgejo сериализует без лок-файла — залипать нечему. Плачу ожиданием ~6 мин, покупаю отсутствие целого класса отказов.

Гейт и его проверка

backend/tests/ops/test_2950_deploy_concurrency_group.py — 5 тестов. Мутационно проверен, что умеет краснеть:

мутация результат
развести группы обратно 1 failed
cancel-in-progress: true 1 failed
убрать секцию concurrency целиком 3 failed
контроль (как в PR) 5 passed, rc=0

Отдельно — фильтр changes

Гейт стережёт .forgejo/workflows/**, а фильтр backend их не включал. Правка, разводящая группы обратно, не трогает backend/**backend-tests пропускается → гейт не исполняется ровно на той правке, от которой стережёт. Добавил оба deploy-workflow'а в фильтр. Тот же класс, что #2587 (там auth/roles.yaml лежал вне backend/**, и правка ролей уехала в main без прогона).

Закрывает #2950. ## Что случилось 20.08 деплой ПТИЦЫ (run 8083, коммит `6bca4f7e`) упал за 5 секунд: ``` unable to lease content: lease does not exist: not found ``` ## Причина — по секундам | время (UTC) | run | что происходит | |---|---|---| | 07:05:05.2 | 8083 (ПТИЦА) | `docker compose pull` — 7 образов `Pulling` | | 07:05:06.0 | 8083 (ПТИЦА) | все 7 `Interrupted`, `unable to lease content` | | 07:05:06.4 | 8084 (Trade-In) | `Deleted Images:` — отработал `docker image prune -af` | Оба деплоя ходят по SSH в **один** докер-демон. Проверено `docker ps` на VM: стеки `gendesign-*`, `tradein-*` и сам `forgejo-runner` живут на одном хосте. Группы concurrency были разные — `deploy-prod` и `deploy-tradein-prod` — Forgejo их параллелил. `cancel-in-progress: false` не помогал: он сериализует раны **внутри** группы, а гонка была **между** группами. ## Почему отказ был тихим Голова main показывала `success`. Но зелёным был **Trade-In'овый** деплой этой головы: последний коммит трогал только `tradein-mvp/**`, поэтому у него запустился только `Deploy Trade-In`. Деплой ПТИЦЫ висел на своём, более раннем коммите — с исходом `failure`. Прод остался на старом коде, #2946 и #2947 не доехали (выкачены отдельным `workflow_dispatch`). ## Почему общая группа, а не flock на хосте Сначала сделал host-lock: он сериализует только докер-секцию, не заставляя деплой ждать чужих билдов (~6 мин). Потом проверил его на самой VM и **отказался**: ``` B1: не смог за 1с, пока A держит ← взаимное исключение работает B2: взял лок через 2с после отпускания ← не залипает B: НЕ СМОГ после kill -9 держателя ← а вот тут залипает ``` Дочерний процесс наследует fd лока; при аварийной смерти job'а (ENOSPC, OOM, отмена) лок остаётся занятым и блокирует прод-деплои на весь таймаут. Планировщик Forgejo сериализует без лок-файла — залипать нечему. Плачу ожиданием ~6 мин, покупаю отсутствие целого класса отказов. ## Гейт и его проверка `backend/tests/ops/test_2950_deploy_concurrency_group.py` — 5 тестов. Мутационно проверен, что умеет краснеть: | мутация | результат | |---|---| | развести группы обратно | 1 failed | | `cancel-in-progress: true` | 1 failed | | убрать секцию `concurrency` целиком | 3 failed | | контроль (как в PR) | 5 passed, rc=0 | ## Отдельно — фильтр changes Гейт стережёт `.forgejo/workflows/**`, а фильтр `backend` их не включал. Правка, разводящая группы обратно, не трогает `backend/**` → `backend-tests` пропускается → гейт **не исполняется ровно на той правке, от которой стережёт**. Добавил оба deploy-workflow'а в фильтр. Тот же класс, что #2587 (там `auth/roles.yaml` лежал вне `backend/**`, и правка ролей уехала в main без прогона).
bot-backend added 1 commit 2026-08-20 07:26:29 +00:00
fix(ci): прод-деплои сведены в одну группу concurrency — прун одного убивал pull другого (#2950)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m13s
CI / openapi-codegen-check (pull_request) Successful in 2m5s
CI / backend-tests (pull_request) Successful in 17m19s
c930b12d5f
20.08 деплой ПТИЦЫ (run 8083) упал за 5 секунд:

    unable to lease content: lease does not exist: not found

Все 7 образов ушли в Interrupted через 0.8с после старта пула. Причина — не
в ПТИЦЕ: через 0.4с после обрыва соседний деплой Trade-In напечатал
«Deleted Images:» своего `docker image prune -af`.

Оба деплоя ходят по SSH в ОДИН докер-демон: стеки gendesign-* и tradein-*
плюс сам forgejo-runner живут на одной VM (проверено `docker ps` на хосте).
Группы были разные — deploy-prod и deploy-tradein-prod — поэтому Forgejo
запускал их параллельно. `cancel-in-progress: false` не спасал: он про раны
ВНУТРИ группы, а гонка была МЕЖДУ группами.

Последствие было тихим: голова main показывала success, но зелёным был
Trade-In'овый деплой этой головы, а деплой ПТИЦЫ висел на своём, более раннем
коммите с исходом failure. Прод остался на старом коде — #2946 и #2947 не
доехали и были выкачены отдельным workflow_dispatch.

Почему общая группа, а не flock на хосте. Host-lock сериализовал бы только
докер-секцию, не заставляя деплои ждать чужих билдов (~6 мин). Но у него своя
отказная мода: дочерний процесс наследует fd лока, и при аварийной смерти
job'а лок залипает. Проверено на самой VM: после kill -9 держателя следующий
претендент лок НЕ получил. Залипший лок блокирует прод-деплои на весь таймаут.
Планировщик Forgejo сериализует без лок-файла, залипать там нечему —
платим ожиданием, покупаем отсутствие целого класса отказов.

Гейт backend/tests/ops/test_2950_deploy_concurrency_group.py фиксирует
инвариант. Мутационно проверен: разводка групп обратно, cancel-in-progress:
true и удаление секции concurrency краснят его (1, 1 и 3 упавших теста),
контроль зелёный.

В фильтр changes добавлены сами deploy-workflow'ы — иначе правка, разводящая
группы, не трогает 'backend/**', backend-tests пропускается, и гейт не
исполняется ровно на той правке, от которой стережёт. Тот же класс, что #2587.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 2a01dea103 into main 2026-08-20 07:47:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2952
No description provided.