Files
orchestrator/docs/work-items/ORCH-053/01-brd.md

129 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~815 мин
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
обязателен.