Files

11 KiB
Raw Permalink Blame History

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 не работают:

  1. Dedup-guard не срабатывает. Guard ключуется по state_uuid и активен только когда state_uuid is not None (_note_unblock, стр.459463). Но вызов в 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.)

  2. Терминал-скип не ловит этот путь. Терминал-исключение ORCH-068 (_is_terminal_state, стр.327344) вызывается только в 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, стр.338344).
  • 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 проходят.