5.6 KiB
adr-0004: Поллинг с ретраем в quality-gate check_ci_green (фикс CI-race)
- Статус: accepted
- Дата: 2026-06-05
- Задача: ORCH-045
Контекст
Quality-gate check_ci_green(repo, branch) (src/qg/checks.py) проверяет combined commit-status ветки через Gitea API сразу после того, как developer-агент запушил код. Реализация была single-shot: один GET /repos/{owner}/{repo}/commits/{branch}/status, чтение data["state"] — success → пропуск, иначе → сразу False.
Это создавало race condition. Gitea-CI после пуша 1–3 секунды держит combined state pending, пока не отработают чек-раннеры. Если гейт опрашивал статус в этом окне, он получал pending и возвращал False ровно один раз — повторного опроса не было. Combined state затем дозеленевал до success, но гейт уже промахнулся, и задача застревала насмерть без видимой причины.
Реальный инцидент ORCH-017: гейт опросил статус в 17:58:54 → pending; CI дозеленел в 17:58:55. Задача встала в тупик (см. docs/history / lessons ORCH-017).
Решение
check_ci_green превращён из single-shot в polling с ретраем:
state == "success"→(True, "CI green")немедленно.state in ("failure", "error")→(False, "CI state: <state>")немедленно — CI красный, ретрай бессмыслен (терминальное состояние).state == "pending"(илиunknown/ иное не-терминальное) →time.sleep(interval)и опрос снова, доNпопыток.- После исчерпания всех попыток при всё ещё
pending→(False, "CI still pending after <T>s")— явный провал с причиной, чтобы оператор видел тупик, а не молчаливый стол. 404→(False, "Branch ... not found or no status")— как раньше.- Транзиентная
httpx.HTTPErrorна отдельной попытке — не падаем сразу: логируем и пробуем ещё в рамках лимита попыток; если все попытки — сетевая ошибка →(False, "API error: <e>").
Параметры вынесены в src/config.py (pydantic-settings, env-prefix ORCH_, единый стиль с остальными настройками):
ci_poll_max_attempts(envORCH_CI_POLL_MAX_ATTEMPTS, дефолт 12)ci_poll_interval_s(envORCH_CI_POLL_INTERVAL_S, дефолт 10)
Итого по умолчанию гейт ждёт pending до ~2 минут (12 × 10s) перед тем как явно провалиться. Каждая не-финальная попытка логируется через существующий logger (check_ci_green: attempt i/N, state=..., retrying in Ns). timeout=10 на каждый отдельный запрос сохранён.
Сигнатура check_ci_green(repo, branch) -> tuple[bool, str] не менялась — её зовёт stage_engine и реестр гейтов QG_CHECKS.
Альтернативы
- Оставить single-shot, опрашивать гейт повторно снаружи (на уровне stage_engine/воркера). Отклонено: размазывает логику CI-ожидания по слоям, дублирует таймауты; гейт — естественное место знания о combined-status.
- Webhook от Gitea на завершение CI вместо поллинга. Отложено: требует надёжной доставки/дедупликации вебхуков именно по CI-статусу и переписывания триггера стадии; поллинг — минимальный, локализованный фикс race-а здесь и сейчас.
- Бесконечный ретрай до зелёного. Отклонено: задача могла бы висеть вечно при реально зависшем CI; ограниченный бюджет + явный
Falseс причиной даёт оператору сигнал.
Последствия
- CI-race ORCH-017 закрыт: транзиентный
pendingпереживается ретраем, гейт не промахивается. check_ci_greenтеперь блокирующий до ~max_attempts × intervalсекунд при затяжномpending(по умолчанию ~2 мин). Это осознанный trade-off; для красного CI и success — выход немедленный, без задержки.- Тупик больше не молчаливый: истечение попыток →
(False, "CI still pending after <T>s"), причина видна. - Бюджет/интервал настраиваемы через env без правки кода.
check_tests_passed/_parse_tests_verdict(ORCH-47) не затронуты.
Связи
ORCH-017 (инцидент-первоисточник: deadlock shared-gate из-за CI-race), реестр гейтов QG_CHECKS (check_ci_green), стадия development. Тесты: tests/test_qg.py::TestCheckCIGreen.