11 KiB
01-BRD — ORCH-086: reconciler шлёт ложное «ET-002 done разблокирована»
Work Item: ORCH-086
Тип: Багфикс (шум уведомлений / остаток livelock)
Приоритет: MEDIUM
Зона: src/reconciler.py
Связано: продолжение ORCH-068 (тот фикс задеплоен, но НЕ закрыл этот путь), наследует контракты ORCH-053 / ORCH-060 / ORCH-066.
1. Контекст / проблема
В Telegram периодически (а особенно сразу после рестарта оркестратора) прилетает уведомление:
🔧 reconciler: ET-002 done разблокирована (потерян webhook)
Это ложный шум: задача ET-002 (проект enduro-trails) давно завершена, реально ничего не разблокируется. Уведомление вводит наблюдателя в заблуждение (создаёт впечатление, что конвейер чинит застрявшую задачу, хотя ничего не происходит).
ORCH-068 уже починил аналогичный livelock на F-2 (plane-side): добавил per-issue терминал-исключение (_is_terminal_state, группа Plane completed/cancelled) и in-memory dedup-guard по issue_id→state_uuid. Однако эти две защиты не покрывают путь F-1 (gate-side).
2. Диагностика (код-аудит, golden source — текущий src/reconciler.py)
Уведомление отправляет Reconciler._note_unblock() (reconciler.py ~стр.444) через send_telegram() при settings.reconcile_notify_unblock=True.
Два механизма ORCH-068, которые ДОЛЖНЫ были его подавить, на пути F-1 не работают:
-
Dedup-guard не срабатывает. Guard ключуется по
state_uuidи активен только когдаstate_uuid is not None(_note_unblock, стр.459–463). Но вызов в F-1 (_reconcile_gate_task, стр.228):self._note_unblock(task.get("work_item_id") or str(task_id), stage)передаёт только 2 аргумента, БЕЗ
state_uuid→ ветка dedup пропускается → уведомление шлётся при каждом релевантном тике/старте. (В отличие от F-2, где все 4 вызова_note_unblockпередаютstate_uuid— стр.394/400/407/416.) -
Терминал-скип не ловит этот путь. Терминал-исключение ORCH-068 (
_is_terminal_state, стр.327–344) вызывается только в F-2 (_reconcile_plane_issue, стр.362). В F-1 единственный «терминал-фильтр» — этоget_active_tasks_for_reconcile()(db.pyстр.193:WHERE stage != 'done'), который смотрит только на стадию задачи в БД оркестратора и НЕ знает о статусе задачи в Plane (группаcompleted/cancelled). Поэтому задача, которая в БД оркестратора стоит на НЕ-doneстадии (дрейф), а в Plane ужеDone, проходит фильтр.
Почему advance_if_gate_passed считает ET-002 «продвинувшейся» (G1 — гипотеза, требует подтверждения в development)
Для enduro-trails (не self-hosting) условные гейты (check_staging_status, check_deploy_status, merge-gate, image-freshness, security-gate, merge-verify) — no-op (True, ...) (условность ORCH-35/43/58/71). Поэтому для enduro-задачи, чья стадия в БД оркестратора НЕ done, но застряла перед терминалом (например deploy), advance_if_gate_passed находит гейт зелёным (no-op) → вызывает advance_stage(..., finished_agent=None) → возвращает result.advanced=True (стр.227) → доходит до _note_unblock. Guard 2 (_is_blocked_or_needs_input, стр.230) задачу не спасает: его skip_set = {blocked, needs_input, extra_waits} и НЕ содержит done/cancelled → терминальная-в-Plane задача через него проходит. «Периодичность / при старте» объясняется отсутствием dedup (state_uuid не передан) + чистым in-memory состоянием нового процесса после рестарта (первый проход снова находит задачу).
Открытый вопрос для G1 (подтвердить в development по prod-БД/логам): точная стадия
ET-002в БД оркестратора в момент срабатывания (в quoted-сообщении фигурирует слово «done», ноget_active_tasks_for_reconcileисключаетstage='done'— значит стадия в БД иная либо аномальная). Фикс обязан быть робастным независимо от точной стадии: терминальность определяется по группе статуса Plane (как_is_terminal_state), а не по строковому совпадению стадии.
3. Бизнес-цели
- G1. Установить и задокументировать, почему F-1 (
advance_if_gate_passed) доводит терминальную в Plane задачу (ET-002) до_note_unblockна каждом релевантном тике/старте. - G2. Не слать unblock-уведомление для задач, УЖЕ терминальных (
done/cancelled) в Plane (по группе статуса) и/или в оркестраторе — распространить терминал-скип ORCH-068 на путь F-1 (стр.228), а не только на F-2. - G3. Передавать
state_uuidв_note_unblockна всех путях (включая F-1) → in-memory dedup-guard работает везде (страховка от повтора, даже если терминал-скип когда-то не сработает).
4. Объём (Scope)
В объёме:
- Точечная правка
src/reconciler.py: терминал-скип на пути F-1 + пробросstate_uuidв_note_unblockиз F-1. - Сохранение/корректное инкрементирование наблюдаемости ORCH-068 (
skipped_terminal_total,deduped_total,unblocked_total). - Unit-тесты, покрывающие AC-1…AC-5.
- Обновление документации (
docs/architecture/README.mdблок Reconciler,CHANGELOG.md).
Вне объёма (Не-цели):
- НЕ ломать легитимный replay реально застрявшей задачи (когда реконсиляция её ДЕЙСТВИТЕЛЬНО двигает — уведомление полезно).
- НЕ трогать пайплайн / статусы enduro-trails.
- НЕ отключать
reconcile_notify_unblockглобально (потеряем полезные алерты) — подавление точечное, только для терминальных. - НЕ менять
STAGE_TRANSITIONS, реестрQG_CHECKS, схему БД, контрактыadvance_stage/advance_if_gate_passed. - НЕ менять поведение F-2 (там ORCH-068 уже корректен) сверх необходимого переиспользования хелперов.
5. Заинтересованные лица
- Owner / Слава — наблюдатель Telegram-карточек и алертов; страдающая сторона (шум).
- enduro-trails — проект, чьи терминальные задачи генерируют ложные алерты; пайплайн не должен быть затронут.
- orchestrator (self-hosting) — терминал-детект должен корректно работать и для self (разные наборы Plane-статусов).
6. Риски и ограничения
- R1 (грабли мультипроектности). enduro-trails и orchestrator — разные проекты с разными наборами Plane-статусов. Терминал-детект ОБЯЗАН работать для обоих: первичный дискриминатор — группа статуса Plane (
completed/cancelled, project-independent), fallback — логические ключиdone/cancelled(как в существующем_is_terminal_state, стр.338–344). - R2 (наблюдаемость). Нельзя сломать счётчики ORCH-068. При скипе терминальной задачи в F-1 — инкрементировать
skipped_terminal_total(единая семантика с F-2).deduped_total/unblocked_total— без регрессии. - R3 (never-raise). Тик реконсилятора обязан оставаться never-raise (сеть Plane может быть недоступна). Сбой терминал-проверки → консервативное поведение (как Guard 2: при ошибке скорее НЕ слать, чем слать ложно; но НЕ ценой подавления легитимного unblock — см. AC-4).
- R4 (доп. сетевой вызов). F-1 для проброса
state_uuidи терминал-детекта должен знать текущий Plane-статус issue. Guard 2 (_is_blocked_or_needs_input) уже делаетfetch_issue_state. Желательно переиспользовать один fetch, не удваивая обращения к Plane API на тик (производительность горячего цикла). - R5 (ложно-отрицательный риск). Слишком агрессивное подавление может задушить полезный алерт о реально застрявшей задаче → обязателен регресс-тест AC-4.
7. Метрика успеха
- В Telegram больше нет периодического «ET-002 done разблокирована»;
skipped_terminal_totalрастёт (наблюдаемо вGET /queue). pytest tests/ -qзелёный; новые тесты AC-1…AC-5 проходят.