129 lines
11 KiB
Markdown
129 lines
11 KiB
Markdown
# 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. Ключевые требования (бизнес-уровень)
|
||
|
||
1. **Источник истины — гейт/Plane, а не событие.** Sweeper дёргает ровно те же функции
|
||
продвижения, что и webhook. Параллельной логики продвижения быть не должно.
|
||
2. **Идемпотентность (критично).** Задержавшийся или дублированный webhook + sweeper
|
||
НЕ создают двойную задачу / двойной запуск / двойной advance. Тот же guard, что у
|
||
webhook: нет активного job + стадия совпадает + atomic claim как в `queue_worker`.
|
||
3. **Безопасность активной работы.** Sweeper НЕ трогает задачи с активными
|
||
(`queued`/`running`) job'ами — они легитимно в работе, не потеряны.
|
||
4. **Per-stage grace.** Разные стадии имеют разное нормальное время (analysis ~8–15 мин
|
||
vs deploy). Порог застревания настраивается, чтобы не дёргать гейт у задачи, где агент
|
||
законно работает.
|
||
5. **Restart-safe.** Sweeper — фоновый поток, стартует с приложением, переживает рестарт
|
||
(как `queue_worker`). Без потери состояния.
|
||
6. **Self-hosting safety.** Sweeper не должен ронять/рестартить прод-контейнер; kill-switch
|
||
в конфиге для поэтапного раската и аварийного отключения.
|
||
7. **Без шума.** Когда всё синхронно — никаких действий и нотификаций.
|
||
8. **Документация = 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
|
||
обязателен.
|