11 KiB
BRD — ORCH-053: Sweeper потерянных webhook (реконсиляция застрявших стадий)
Work Item ID: ORCH-053 Стадия: analysis → (architecture) Тип: надёжность конвейера (проектирование + реализация). Self-hosting (ORCH).
1. Проблема (бизнес-контекст)
Продвижение задач между стадиями конвейера завязано исключительно на входящие webhook-события:
- Plane (
work_item.updated→ статус In Progress / Approved / Rejected) — единственный триггер старта задачи, advance и rollback (src/webhooks/plane.py). - Gitea (CI-status
success/failure, push, PR reviewed/merged) — триггер development→review, architecture→development, review→testing, deploy→done (src/webhooks/gitea.py).
Если входящее событие потеряно (502 на падающем/ребилдящемся инстансе, Plane/Gitea не повторяют доставку, сетевой сбой, sha→branch не разрезолвился, вебхук был временно выключен) — статус в источнике истины (Plane / зелёный CI) уже изменился, а задача в оркестраторе не сдвинулась. Задача висит молча, без какого-либо механизма восстановления.
Живой инцидент (ORCH-044, 06.06): dev-агент отработал (exit 0, CI позеленел), но
Gitea webhook о CI-success не продвинул задачу (не дошёл / не сматчился sha→branch).
Задача висела бы на development молча навсегда — спасли только ручным дёрганьем гейта
check_ci_green. Это системная дыра, а не разовый сбой; сейчас её ловит ручной
heartbeat-watchdog Стрима (костыль).
Что уже есть и почему недостаточно
| Механизм | Что покрывает | Почему не закрывает дыру |
|---|---|---|
requeue_running_jobs() (startup) |
зависшие jobs при рестарте | про jobs, не про застрявший stage-переход |
orphan-recovery (main.py) |
agent_runs без finished_at |
job-уровень, не stage |
ORCH-5 events de-dup (delivery_id) |
защита от дублей webhook | обратной защиты от потери нет |
ORCH-045 ci_poll в check_ci_green |
поллит CI 12×10с | только если гейт уже вызван webhook'ом; не пришёл webhook → гейт не вызывается |
Общий принцип всех существующих механизмов — restart-safe resilience на уровне jobs. Нет ни одного механизма, реконсилирующего рассинхрон «источник истины ≠ стадия задачи».
2. Цель
Задача не должна застревать молча из-за потерянного входящего события. Ввести фоновый периодический sweeper / reconciler, который сам находит «зависшие» задачи и доигрывает пропущенный переход — через те же штатные гейты и обработчики, что и webhook (никакой параллельной логики продвижения). Убрать необходимость в ручном heartbeat-watchdog.
3. Заинтересованные стороны
- Owner / Стрим (Слава) — перестаёт ловить зависания вручную.
- Все проекты на инстансе (enduro-trails + orchestrator) — конвейер не встаёт молча.
- Self-hosting (ORCH) — особенно при ребилде прода (ORCH-51): вебхуки, прилетевшие на падающий инстанс, подбираются реконсиляцией после старта.
4. Объём (Scope)
В объёме — две взаимодополняющие ветки реконсиляции (обе обязательны):
F-1. Gate-side sweeper (реконсиляция застрявшей стадии по локальной БД)
Периодический проход по таблице tasks: найти задачи, у которых
(а) stage != done, (б) нет активных job'ов в очереди, (в) с момента updated_at
прошло больше per-stage порога → пере-проверить QG текущей стадии и, если passed —
продвинуть штатным путём (stage_engine.advance_stage(..., finished_agent=None),
тот же путь, что использует webhook). Закрывает потерю Gitea CI/PR-вебхуков (ORCH-044).
F-2. Plane-side reconciler (реконсиляция потерянного Plane status-webhook)
Периодический опрос Plane API по проектам реестра (projects.py): issues в статусах,
требующих действия (In Progress / Approved / Rejected). Сверить с локальной tasks и
доиграть через существующие обработчики webhooks/plane.py:
- In Progress + нет задачи в БД → создать+запустить (
handle_status_start/start_pipeline); - Approved + стадия не сдвинута → advance (
handle_verdict(approved=True)); - Rejected + не откатана → rollback (
handle_verdict(approved=False)).
F-3. Усиление sha→branch резолва в Gitea-вебхуке
В handle_ci_status добавить надёжный fallback (поиск task по БД), чтобы исходный
webhook реже терялся из-за неразрезолвленного branch. Sweeper работает от задачи
(repo+branch известны из БД) и обходит эту хрупкость по определению.
F-4. Наблюдаемость
Лог (и опц. Telegram) каждый раз, когда sweeper разблокировал застрявшую задачу —
чтобы видеть частоту срабатывания дыры (метрика потерянных webhook). Опц. вывод
счётчика в /queue или /reconcile. Не спамить, когда всё синхронно.
Вне объёма
- Буфер недоставленных webhook (это ORCH-51; sweeper — резервная сетка к нему).
- Изменение состава стадий/гейтов (
STAGE_TRANSITIONS,QG_CHECKS). - Изменение логики самих гейтов и обработчиков (только переиспользование).
- Новый исполняемый деплой (ORCH-36).
5. Ключевые требования (бизнес-уровень)
- Источник истины — гейт/Plane, а не событие. Sweeper дёргает ровно те же функции продвижения, что и webhook. Параллельной логики продвижения быть не должно.
- Идемпотентность (критично). Задержавшийся или дублированный webhook + sweeper
НЕ создают двойную задачу / двойной запуск / двойной advance. Тот же guard, что у
webhook: нет активного job + стадия совпадает + atomic claim как в
queue_worker. - Безопасность активной работы. Sweeper НЕ трогает задачи с активными
(
queued/running) job'ами — они легитимно в работе, не потеряны. - Per-stage grace. Разные стадии имеют разное нормальное время (analysis ~8–15 мин vs deploy). Порог застревания настраивается, чтобы не дёргать гейт у задачи, где агент законно работает.
- Restart-safe. Sweeper — фоновый поток, стартует с приложением, переживает рестарт
(как
queue_worker). Без потери состояния. - Self-hosting safety. Sweeper не должен ронять/рестартить прод-контейнер; kill-switch в конфиге для поэтапного раската и аварийного отключения.
- Без шума. Когда всё синхронно — никаких действий и нотификаций.
- Документация = golden source. README/architecture, ADR, CHANGELOG обновляются в том же PR.
6. Эффект
- Потерянный webhook больше не = молча застрявшая задача.
- Ручной heartbeat-watchdog Стрима больше не нужен для ловли зависаний (AC-5 в эпике).
- Резервная сетка к ORCH-51 при ребилде прода.
7. Связи
- Дополняет ORCH-51 (потеря webhook при рестарте — буфер; sweeper — реконсиляция).
- Дополняет ORCH-36 (если deploy-webhook потеряется — sweeper добьёт deploy→done).
- ORCH-1b — та же философия resilience: транзиентный сбой не убивает задачу.
- Эпик: звено ORCH-54 (автономное внедрение). Параллельна ORCH-36 (разные файлы),
но
max_concurrency=1→ встанет в очередь.
8. Риски (кратко; подробно — 10-tech-risks архитектора)
- Гонка sweeper ↔ живой webhook → двойной запуск. Митигируется atomic claim + active-job guard + grace-период (не конкурировать с задержавшимся webhook).
- Spam нотификаций при персистентно красном гейте на каждом тике. Митигируется: действие/нотификация только на изменении состояния (advance), не на каждый тик.
- Нагрузка на Plane API при опросе каждые N сек. Митигируется интервалом + фильтром по статусам + per-project.
- Self-hosting: sweeper правит инструмент, обслуживающий и другие проекты. Kill-switch обязателен.