integ: merge ORCH-068 reconciler livelock fix
# Conflicts: # docs/architecture/README.md # src/reconciler.py
This commit is contained in:
7
docs/work-items/ORCH-068/00-business-request.md
Normal file
7
docs/work-items/ORCH-068/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: reconciler livelock — спам unblock done-задачи (ET-002)
|
||||
|
||||
Work Item ID: ORCH-068
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
52
docs/work-items/ORCH-068/01-brd.md
Normal file
52
docs/work-items/ORCH-068/01-brd.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# BRD — ORCH-068: BUG reconciler livelock (спам unblock done-задачи)
|
||||
|
||||
## 1. Контекст и предыстория
|
||||
Reconciler (`src/reconciler.py`, ORCH-053) — фоновый поток, который доигрывает пропущенные webhook-переходы. Ветвь **F-2 (plane-side)** опрашивает Plane per-project и реплеит In Progress / Approved / Rejected через штатные `handle_status_start` / `handle_verdict`. При фактической разблокировке вызывается `_note_unblock` → лог + Telegram.
|
||||
|
||||
ORCH-066 (мердж `feature/ORCH-066-plane`, прод 2026-06-07 ~22:16 UTC) ввела новую статусную модель Plane (маппинг стадия↔статус, новые имена `Done`, `Monitoring after Deploy` и т.п.). Это спровоцировало регрессию в F-2.
|
||||
|
||||
## 2. Проблема (бизнес-симптом)
|
||||
С 22:17 UTC (рестарт прод-контейнера после деплоя ORCH-066) reconciler каждые ~120с шлёт в Telegram:
|
||||
```
|
||||
reconciler: ET-002 done разблокирована (потерян webhook)
|
||||
```
|
||||
для задачи **ET-002** (enduro-trails), которая в Done с 2026-05-21. На момент анализа — 191+ сообщений подряд, поток не прекращается (ночной спам, найден Славой 2026-06-08 01:22 UTC).
|
||||
|
||||
**Ключевой факт:** ET-002 полностью синхронизирована — БД `stage=done`, Plane `state=Done`. Reconciler обязан молчать (инвариант «silence when in sync», AC-9/AC-10 из ORCH-053), но шлёт уведомления вхолостую.
|
||||
|
||||
## 3. Диагностика (проведена, root cause найден)
|
||||
1. **Деньги/токены НЕ тратятся:** `jobs` / `agent_runs` за 4ч пусты; `advance_stage` для done = no-op; `handle_verdict` для done-задачи ничего не меняет. Это «дешёвый», но шумный и подрывающий доверие баг (livelock + ложный alert-fatigue).
|
||||
2. **Механизм:**
|
||||
- `_reconcile_plane_project` (`src/reconciler.py` ~241) тянет `list_issues_by_state(pid, [in_progress, approved, rejected])`.
|
||||
- На enduro-trails статусы «схлопнуты»: после ORCH-066 терминальный `Done` алиасится под UUID `approved` (см. ниже п.4) → ET-002 (Plane=Done) **попадает** в actionable-выборку.
|
||||
- В `_reconcile_plane_issue` (~295) срабатывает ветка `new_state == approved and task is not None` → `handle_verdict(approved)` (no-op, задача уже done) **+ безусловный `_note_unblock`**.
|
||||
- `_note_unblock` (~317) вызывается **сразу после `_dispatch`, не проверяя фактическое изменение состояния** — хотя его docstring обещает «fires only on an actual state change, never per idle tick». Инвариант нарушен.
|
||||
3. **Два независимых дефекта складываются:**
|
||||
- **D1 (выборка):** терминальные статусы (`Done`/`Cancelled`) не исключены из actionable-набора F-2; на «схлопывающих» проектах Done не отличается от approved по голому UUID.
|
||||
- **D2 (нотификация):** `_note_unblock` срабатывает безусловно после no-op dispatch, а не только при подтверждённом state change.
|
||||
4. **Почему `get_project_states` схлопывает:** функция строит маппинг по *именам* статусов из Plane API, затем недостающие ключи добивает из `_DEFAULT_STATES` (enduro-значения). После ORCH-066 набор статусов enduro изменился — голый UUID перестал однозначно различать `Done` (completed-группа) и `approved` (review). Группа состояния (`state.group`) при этом различает их корректно, но в коде не используется.
|
||||
|
||||
## 4. Связанный баг (BUG КЭША СТАТУСОВ, найден 2026-06-07 при деплое ORCH-066)
|
||||
`_STATES_CACHE` (`src/plane_sync.py` ~134) кэширует статусы Plane на **весь lifetime процесса**. После создания нового Plane-статуса (напр. `Confirm Deploy`) боевой процесс держит устаревший набор → webhook на новый статус даёт «no pipeline action» (Phase B не триггерится). Лечилось только рестартом орка. Примитив сброса уже есть — `reload_project_states()` — но он нигде не вызывается автоматически.
|
||||
|
||||
Оба бага — следствие хрупкости статусной модели после ORCH-066. **Решение:** вести их в одном work item (см. scope ниже), окончательное разделение — на усмотрение архитектора.
|
||||
|
||||
## 5. Цели (Goals)
|
||||
- G1. Reconciler НЕ шлёт «разблокирована» для синхронизированной done/cancelled задачи (восстановить инвариант silence-when-in-sync).
|
||||
- G2. `_note_unblock` срабатывает **только** при реальном state change (соблюдён AC-9/AC-10).
|
||||
- G3. Дедуп: нет повторного спама по той же задаче без изменения её состояния.
|
||||
- G4. Корректное различение терминальных (`Done`/`Cancelled`) и review-статусов (`approved`/`rejected`) даже на проектах, «схлопывающих» их по UUID — на всех проектах (enduro И orchestrator).
|
||||
- G5 (secondary). Устаревший `_STATES_CACHE` обновляется без рестарта процесса (TTL / flush-on-unknown / endpoint).
|
||||
|
||||
## 6. Не-цели (Out of scope)
|
||||
- N1. Менять source-of-truth: ориентир F-2 на Plane **остаётся** корректным по дизайну (таблица `tasks` без status-колонки; статусы двигает человек в Plane). Идею F-2 НЕ переписываем — баг в маппинге/нотификации, не в концепции.
|
||||
- N2. Менять реестры `STAGE_TRANSITIONS` / `QG_CHECKS`, схему БД, контракты гейтов.
|
||||
- N3. Менять поведение F-1 (gate-side) и F-3.
|
||||
- N4. Полный авто-approve деплоя (ORCH-54).
|
||||
|
||||
## 7. Затронутые стороны
|
||||
- **Все проекты на одном инстансе** (enduro-trails + orchestrator, общая БД/очередь) — баг проявился на ET-002, но фикс выборки терминалов обязан быть проектно-независимым.
|
||||
- **Self-hosting:** правка идёт в работающий прод-инструмент → обязательна страховка staging (8501), запрет на рестарт прод-контейнера в рамках задачи.
|
||||
|
||||
## 8. Критерий успеха (бизнес)
|
||||
Тик reconciler для синхронизированной done/cancelled задачи = **0 уведомлений, 0 jobs, 0 токенов**. Telegram-спам прекращён. Легитимная разблокировка (реально потерянный approved/in_progress webhook) по-прежнему работает (нет регресса F-2).
|
||||
68
docs/work-items/ORCH-068/02-trz.md
Normal file
68
docs/work-items/ORCH-068/02-trz.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ТЗ — ORCH-068: устранить livelock reconciler F-2 + спам unblock done-задачи
|
||||
|
||||
> Документ описывает ТРЕБОВАНИЯ к изменению поведения (что и где), а не выбор реализации. Конкретный механизм (state.group vs явный allowlist терминалов; TTL vs flush-on-unknown) — решение архитектора в ADR.
|
||||
|
||||
## 1. Затронутые модули `src/`
|
||||
| Модуль | Роль в баге | Требуемое изменение |
|
||||
|--------|-------------|---------------------|
|
||||
| `src/reconciler.py` | F-2 `_reconcile_plane_project` / `_reconcile_plane_issue` / `_note_unblock` | Исключить терминальные статусы из actionable-выборки; `_note_unblock` только при подтверждённом state change; дедуп. |
|
||||
| `src/plane_sync.py` | `get_project_states`, `list_issues_by_state`, `_STATES_CACHE` | Дать способ различать терминальные/review статусы (группа состояния); устранить вечно-устаревший кэш (TTL/flush). |
|
||||
| `src/config.py` | флаги | (если нужны) новые kill-switch/настройки TTL — с дефолтами, не меняющими прод-инварианты. |
|
||||
| `src/main.py` (`/queue`) | наблюдаемость | (опц.) отразить дедуп/skip-терминалов в снимке `reconcile`. |
|
||||
|
||||
**НЕ трогать:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), схему БД, контракты `handle_status_start` / `handle_verdict`, F-1 (`reconcile_gate_once`), F-3.
|
||||
|
||||
## 2. Требования к F-2 (src/reconciler.py)
|
||||
|
||||
### TR-1. Исключить терминальные статусы из actionable-выборки
|
||||
`_reconcile_plane_project` НЕ должен подавать задачи в терминальном Plane-статусе (`Done` и прочие completed-группы, `Cancelled`) ни в `list_issues_by_state`, ни в последующее сравнение веток.
|
||||
- Требование проектно-независимое: работает на enduro И orchestrator, независимо от того, «схлопывает» ли проект статусы по UUID.
|
||||
- Различение `Done`/`Cancelled` (completed) от `approved`/`rejected` (review) НЕ должно опираться только на голый UUID, если проект их алиасит. Допустимый ориентир — группа состояния Plane (`state.group`: `completed`/`started`/`unstarted`/`backlog`/`cancelled`) либо явный набор логических ключей терминалов. Выбор — за архитектором.
|
||||
|
||||
### TR-2. `_note_unblock` — только при реальном state change
|
||||
`_note_unblock` (лог + Telegram + инкремент `unblocked_total`) ВЫЗЫВАЕТСЯ ТОЛЬКО когда диспетчеризованный обработчик фактически изменил состояние задачи (advance / replayed transition, реально сдвинувший стадию). No-op dispatch (задача уже в целевом состоянии) → нотификация НЕ отправляется.
|
||||
- Сейчас `_dispatch` (`asyncio.run(coro_fn(...))`) отбрасывает результат, а `_note_unblock` зовётся безусловно. Требуется механизм подтверждения изменения (напр. сравнение стадии задачи до/после dispatch через `get_task_by_plane_id`, либо проброс сигнала из обработчика). Конкретику выбирает архитектор; контракт обработчиков `handle_*` менять НЕ обязательно (предпочтительно сравнение состояния до/после на стороне reconciler).
|
||||
- Восстановить соответствие docstring `_note_unblock`: «Fires only on an actual state change … never per idle tick».
|
||||
|
||||
### TR-3. Дедуп / идемпотентность нотификаций
|
||||
Reconciler НЕ должен слать повторное уведомление о той же задаче, если её состояние не менялось с прошлого тика. TR-1+TR-2 закрывают основной кейс (done более не входит в выборку и не нотифицируется); TR-3 — дополнительная страховка (best-effort), чтобы любой будущий no-op путь не дал повторного спама.
|
||||
|
||||
## 3. Требования к статус-кэшу (src/plane_sync.py) — secondary
|
||||
|
||||
### TR-4. Устаревший `_STATES_CACHE` обновляется без рестарта
|
||||
После появления нового Plane-статуса процесс не должен бесконечно держать устаревший набор. Допустимые подходы (выбор архитектора, можно комбинировать):
|
||||
- TTL на запись кэша (напр. `ORCH_PLANE_STATES_TTL_S`, дефолт разумный, 0/неуст. = прежнее поведение для совместимости);
|
||||
- flush-on-unknown: при детекте неизвестного статуса в вебхуке/реконсилере — вызвать существующий `reload_project_states(pid)` и перезапросить;
|
||||
- админ-эндпоинт/сигнал для ручного flush без рестарта.
|
||||
`reload_project_states()` уже существует — переиспользовать как примитив сброса, новую логику сброса не дублировать.
|
||||
|
||||
## 4. Изменения API
|
||||
- Новых обязательных endpoint'ов нет.
|
||||
- Опционально (TR-4, на усмотрение архитектора): admin-эндпоинт сброса кэша статусов (напр. `POST /admin/plane-states/reload`) — если выбран этот вариант flush. Должен быть защищён/идемпотентен; документировать в README таблице API.
|
||||
- Снимок `GET /queue` (блок `reconcile`) — без ломающих изменений; допустимо добавить поля наблюдаемости (skip-терминалов / dedup-счётчик).
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
**Нет.** Дедуп TR-3 реализуется in-memory (best-effort, как существующие счётчики `unblocked_total`/`last_unblocked`, AC-11 ORCH-053 допускает их сброс при рестарте) либо через сравнение живого состояния Plane/БД. Миграции не требуются.
|
||||
|
||||
## 6. Требования к новым QG checks
|
||||
Нет. Реестр `QG_CHECKS` не меняется.
|
||||
|
||||
## 7. Инварианты (обязаны сохраниться)
|
||||
- INV-1. Source of truth F-2 — Plane (НЕ меняем).
|
||||
- INV-2. never-raise на единицу работы (per-issue / per-project / per-tick) сохранён.
|
||||
- INV-3. Kill-switch `ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2) — работают.
|
||||
- INV-4. F-1 / F-3 поведение не изменено.
|
||||
- INV-5. 0 jobs / 0 токенов для синхронизированных задач (как сейчас) сохранено.
|
||||
- INV-6. Легитимная разблокировка реально-потерянного approved/in_progress webhook продолжает работать (нет регресса F-2).
|
||||
- INV-7. Self-hosting: тик reconciler НИКОГДА не рестартит/не роняет прод-контейнер.
|
||||
|
||||
## 8. Артефакты pipeline, которые надо обновить в ТОМ ЖЕ PR
|
||||
- `CLAUDE.md` — если меняется наблюдаемое поведение reconciler/кэша (раздел про reconciler/правила).
|
||||
- `docs/architecture/README.md` — секция «Reconciler … (ORCH-053)»: уточнить исключение терминалов + дедуп; при TR-4 — секция «Plane Sync».
|
||||
- `docs/architecture/adr/adr-0007-reconciler.md` (или новый per-WI ADR `docs/work-items/ORCH-068/06-adr/ADR-001-…`) — зафиксировать решение по терминалам/группе состояния и по кэшу.
|
||||
- `CHANGELOG.md` — запись о фиксе (`fix:`).
|
||||
- `.env.example` / `.env.staging` — если введён новый флаг (TTL/kill-switch).
|
||||
|
||||
## 9. Замечания по приёмке/тестированию
|
||||
- Регресс-тест ОБЯЗАН покрывать: задача в `Done` (синхронизирована) → тик F-2 = 0 нотификаций, на enduro И orchestrator проектах (terminal не зависит от алиасинга).
|
||||
- Тест НЕ должен делать реальных сетевых вызовов — мокать `list_issues_by_state` / `get_project_states` / `send_telegram` / `_dispatch` (handle_*).
|
||||
84
docs/work-items/ORCH-068/03-acceptance-criteria.md
Normal file
84
docs/work-items/ORCH-068/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Критерии приёмки — ORCH-068
|
||||
|
||||
Формат: каждый AC имеет чёткое условие PASS/FAIL. Задача принимается только при ВСЕХ PASS.
|
||||
|
||||
## Основное (P0) — livelock / спам
|
||||
|
||||
### AC-1. Синхронизированная done-задача → тишина
|
||||
**Дано:** задача в Plane `state=Done`, БД `stage=done`, активных job нет.
|
||||
**Когда:** выполняется один тик F-2 (`reconcile_plane_once` / `_reconcile_plane_project`).
|
||||
- PASS: `_note_unblock` НЕ вызван; `send_telegram` НЕ вызван; `unblocked_total` не изменился; создано 0 jobs.
|
||||
- FAIL: любое уведомление/лог «разблокирована»/инкремент счётчика для этой задачи.
|
||||
|
||||
### AC-2. Терминалы исключены из actionable-выборки
|
||||
**Дано:** проект, где `Done` (и/или `Cancelled`) по UUID совпадает/«схлопнут» с `approved`/`rejected`.
|
||||
**Когда:** `_reconcile_plane_project` формирует набор и обходит issues.
|
||||
- PASS: issue в терминальном статусе (completed-группа / `Cancelled`) НЕ попадает ни в одну из веток `in_progress/approved/rejected`; для неё F-2 — no-op silence.
|
||||
- FAIL: терминальная issue заходит в ветку approved/rejected/in_progress.
|
||||
|
||||
### AC-3. `_note_unblock` только при реальном state change
|
||||
**Дано:** dispatch обработчика (`handle_verdict`/`handle_status_start`) фактически НЕ изменил стадию задачи (no-op, задача уже в целевом состоянии).
|
||||
- PASS: `_note_unblock` НЕ вызван.
|
||||
- FAIL: `_note_unblock` вызван после no-op dispatch.
|
||||
|
||||
### AC-4. Дедуп по неизменному состоянию
|
||||
**Дано:** две последовательные итерации тика по одной и той же синхронизированной задаче, состояние между тиками не менялось.
|
||||
- PASS: суммарно 0 повторных уведомлений по этой задаче.
|
||||
- FAIL: повторное уведомление на втором тике без изменения состояния.
|
||||
|
||||
### AC-5. Нет регресса легитимной разблокировки F-2
|
||||
**Дано:** задача, у которой Plane=`Approved`, а локальная стадия НЕ продвинулась (реально потерянный verdict-webhook), grace выдержан, активных job нет.
|
||||
**Когда:** тик F-2.
|
||||
- PASS: `handle_verdict(approved)` доигран; задача продвинута; `_note_unblock` вызван РОВНО один раз (реальный state change).
|
||||
- FAIL: задача не продвинута ИЛИ нотификация не отправлена ИЛИ отправлена многократно.
|
||||
|
||||
### AC-6. Аналогично для in_progress-старта и rejected-отката
|
||||
- PASS: потерянный `In Progress` (task is None) → старт пайплайна + 1 unblock; потерянный `Rejected` → откат + 1 unblock — оба только при реальном изменении.
|
||||
- FAIL: ложный/повторный unblock или отсутствие легитимного.
|
||||
|
||||
## Инварианты (P0)
|
||||
|
||||
### AC-7. Деньги/ресурсы не тратятся на синхронизированные задачи
|
||||
- PASS: 0 jobs, 0 agent_runs, 0 токенов для done/cancelled задач (как до бага).
|
||||
- FAIL: любой созданный job/agent_run.
|
||||
|
||||
### AC-8. never-raise сохранён
|
||||
**Дано:** `list_issues_by_state` / `get_project_states` / `_dispatch` / `send_telegram` бросают исключение.
|
||||
- PASS: тик не падает; ошибка изолирована (per-issue / per-project), логируется; остальные задачи обрабатываются.
|
||||
- FAIL: непойманное исключение роняет тик/поток.
|
||||
|
||||
### AC-9. Kill-switch'и работают
|
||||
- PASS: `ORCH_RECONCILE_ENABLED=false` → F-1 и F-2 не выполняются; `ORCH_RECONCILE_PLANE_ENABLED=false` → F-2 не выполняется, F-1 работает.
|
||||
- FAIL: любой свитч не гасит соответствующую ветку.
|
||||
|
||||
### AC-10. F-1 / F-3 / реестры / схема БД не изменены
|
||||
- PASS: `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, контракты `handle_*`, поведение F-1/F-3 — без изменений (diff не затрагивает).
|
||||
- FAIL: любое изменение перечисленного.
|
||||
|
||||
### AC-11. Self-hosting безопасность
|
||||
- PASS: ни один путь reconciler не рестартит/не роняет прод-контейнер `orchestrator`.
|
||||
- FAIL: обратное.
|
||||
|
||||
## Secondary (P1) — кэш статусов
|
||||
|
||||
### AC-12. Устаревший `_STATES_CACHE` обновляется без рестарта
|
||||
**Дано:** после старта процесса в Plane появился новый статус (его UUID отсутствует в кэше).
|
||||
**Когда:** срабатывает выбранный механизм (TTL истёк / flush-on-unknown / ручной flush).
|
||||
- PASS: следующий `get_project_states` возвращает свежий набор, включающий новый статус; webhook на новый статус даёт корректное pipeline-действие БЕЗ рестарта.
|
||||
- FAIL: процесс продолжает отдавать устаревший набор → «no pipeline action».
|
||||
|
||||
### AC-13. Совместимость кэша по умолчанию
|
||||
- PASS: при дефолтных настройках (TTL не задан / flush не сработал) поведение `get_project_states` не регрессирует; enduro по-прежнему получает свои UUID, fallback на `_DEFAULT_STATES` при недоступности API сохранён.
|
||||
- FAIL: регресс резолва статусов или потеря fallback.
|
||||
|
||||
## Документация (P0 по правилам проекта)
|
||||
|
||||
### AC-14. Документация обновлена в том же PR
|
||||
- PASS: обновлены применимые из {`docs/architecture/README.md` (Reconciler/Plane Sync), ADR, `CHANGELOG.md`, `CLAUDE.md`, `.env.example`}; reviewer подтверждает.
|
||||
- FAIL: поведение изменено, доки/ADR/CHANGELOG не обновлены.
|
||||
|
||||
## Тесты (P0)
|
||||
|
||||
### AC-15. `pytest tests/ -q` зелёный
|
||||
- PASS: весь набор тестов проходит; добавлены регресс-тесты из `04-test-plan.yaml`, включая done→silence на enduro и orchestrator.
|
||||
- FAIL: любой красный тест или отсутствие регресс-теста на основной баг.
|
||||
122
docs/work-items/ORCH-068/04-test-plan.yaml
Normal file
122
docs/work-items/ORCH-068/04-test-plan.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
work_item: ORCH-068
|
||||
description: >
|
||||
Регрессионные и модульные тесты на устранение livelock reconciler F-2
|
||||
(спам _note_unblock для синхронизированной done-задачи) и связанного бага
|
||||
кэша статусов. Все тесты офлайн: Plane API / Telegram / dispatch мокаются.
|
||||
Целевые модули: src/reconciler.py, src/plane_sync.py.
|
||||
|
||||
tests:
|
||||
# ---------- P0: основной баг (livelock / спам) ----------
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: >
|
||||
Синхронизированная done-задача (Plane=Done, БД=done, нет активных job):
|
||||
один тик F-2 -> _note_unblock НЕ вызван, send_telegram НЕ вызван,
|
||||
unblocked_total не изменился, 0 jobs. (AC-1, AC-7)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: >
|
||||
Терминал «схлопнут» с approved по UUID: issue в Done с тем же UUID, что и
|
||||
approved-набор, НЕ заходит ни в одну ветку in_progress/approved/rejected
|
||||
(silence). Проверка проектно-независимого исключения терминалов. (AC-2)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: >
|
||||
Cancelled терминал также исключён из actionable-выборки -> тик = silence,
|
||||
0 нотификаций. (AC-2)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: >
|
||||
_note_unblock не вызывается после no-op dispatch: handle_verdict не сдвинул
|
||||
стадию (задача уже в целевом состоянии) -> 0 нотификаций. (AC-3)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: >
|
||||
Дедуп: два последовательных тика по одной синхронизированной задаче без
|
||||
изменения состояния -> суммарно 0 повторных уведомлений. (AC-4)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: >
|
||||
Нет регресса: Plane=Approved, локальная стадия не продвинута, grace выдержан,
|
||||
нет активных job -> handle_verdict доигран, задача продвинута, _note_unblock
|
||||
вызван РОВНО один раз. (AC-5)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: >
|
||||
Нет регресса для in_progress (task is None -> старт пайплайна, 1 unblock) и
|
||||
rejected (task существует -> откат, 1 unblock), оба только при реальном
|
||||
изменении состояния. (AC-6)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: >
|
||||
never-raise: list_issues_by_state / get_project_states / _dispatch /
|
||||
send_telegram бросают исключение -> тик не падает, ошибка изолирована и
|
||||
залогирована, прочие issues обработаны. (AC-8)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: >
|
||||
Kill-switch: reconcile_enabled=False -> F-2 не выполняется;
|
||||
reconcile_plane_enabled=False -> F-2 не выполняется, F-1 не затронут. (AC-9)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: >
|
||||
End-to-end F-2 на двух проектах (enduro И orchestrator): задача в Done на
|
||||
каждом -> тик reconcile_plane_once = 0 нотификаций / 0 jobs на обоих,
|
||||
независимо от алиасинга статусов проекта. Главный регресс-тест бага. (AC-1, AC-2)
|
||||
module: tests/test_reconciler_plane.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- P1: связанный баг кэша статусов ----------
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: >
|
||||
Устаревший _STATES_CACHE обновляется без рестарта: после появления нового
|
||||
статуса срабатывает выбранный механизм (TTL/flush) -> следующий
|
||||
get_project_states содержит новый статус. (AC-12)
|
||||
module: tests/test_plane_states_cache.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: >
|
||||
Совместимость по умолчанию: при дефолтных настройках get_project_states не
|
||||
регрессирует — enduro отдаёт свои UUID, fallback на _DEFAULT_STATES при
|
||||
недоступности API сохранён. (AC-13)
|
||||
module: tests/test_plane_states_cache.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- P0: общий прогон ----------
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: >
|
||||
Полный набор pytest tests/ -q зелёный (нет регресса в reconciler/plane/qg/
|
||||
stage_engine). (AC-15)
|
||||
module: tests/
|
||||
expected: PASS
|
||||
@@ -0,0 +1,162 @@
|
||||
# ADR-001 (ORCH-068): Исключение терминалов из F-2 по группе состояния + подтверждённый unblock + TTL кэша статусов
|
||||
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-08
|
||||
- **Задача:** ORCH-068 (BUG: reconciler livelock — спам «разблокирована» по синхронизированной done-задаче)
|
||||
- **Сквозной ADR:** уточняет [adr-0007-reconciler.md](../../../architecture/adr/adr-0007-reconciler.md) (F-2) — реестры/схема НЕ меняются
|
||||
- **Связанные:** ORCH-053 (reconciler F-2), ORCH-066 (новая статусная модель Plane — триггер регрессии), ORCH-060 (F-1 пред-гарды), ORCH-10 (`get_project_states`)
|
||||
|
||||
## Контекст
|
||||
|
||||
Reconciler F-2 (`src/reconciler.py`, `_reconcile_plane_project` / `_reconcile_plane_issue`)
|
||||
опрашивает Plane per-project и доигрывает потерянные webhook-переходы через штатные
|
||||
`handle_status_start` / `handle_verdict`. После мерджа ORCH-066 (новая статусная модель
|
||||
Plane) на проде с 22:17 UTC reconciler каждые ~120с слал в Telegram
|
||||
`reconciler: ET-002 done разблокирована (потерян webhook)` для задачи ET-002, которая
|
||||
полностью синхронизирована (БД `stage=done`, Plane `state=Done` с 2026-05-21). 191+
|
||||
сообщений за ночь — livelock без advance/jobs/токенов, но подрывающий доверие alert-fatigue.
|
||||
|
||||
Диагностика (BRD §3) выявила **два независимых, складывающихся дефекта**:
|
||||
|
||||
- **D1 (выборка):** F-2 различает actionable-статусы по **голому UUID**
|
||||
(`in_progress`/`approved`/`rejected`). `get_project_states` строит маппинг по *именам*
|
||||
статусов, недостающие ключи добивает из `_DEFAULT_STATES` (enduro-значения). После
|
||||
ORCH-066 набор имён enduro изменился → терминальный `Done` перестал однозначно
|
||||
отличаться от `approved` по UUID, и ET-002 (Plane=Done) **попала** в actionable-набор
|
||||
ветки `approved`. Терминальные статусы (`Done`/`Cancelled`) нигде не исключаются из F-2.
|
||||
- **D2 (нотификация):** `_note_unblock` вызывается **безусловно сразу после `_dispatch`**,
|
||||
не проверяя, изменил ли обработчик реально состояние задачи. `handle_verdict(approved)`
|
||||
для уже-`done` задачи — no-op, но нотификация всё равно уходит. Это прямо нарушает
|
||||
собственный docstring `_note_unblock` («fires only on an actual state change, never per
|
||||
idle tick») и инвариант silence-when-in-sync (AC-9/AC-10 ORCH-053).
|
||||
|
||||
Связанный secondary-баг (BRD §4): `_STATES_CACHE` (`src/plane_sync.py`) кэширует статусы
|
||||
на **весь lifetime процесса**. После появления нового Plane-статуса боевой процесс держит
|
||||
устаревший набор → webhook на новый статус даёт «no pipeline action», лечилось только
|
||||
рестартом орка. Примитив сброса `reload_project_states()` уже есть, но автоматически не
|
||||
вызывается.
|
||||
|
||||
Ограничения (из ТЗ, обязаны сохраниться): источник истины F-2 — Plane (не переписываем);
|
||||
НЕ трогать `STAGE_TRANSITIONS` / `QG_CHECKS` / схему БД / контракты `handle_*` / F-1 / F-3;
|
||||
never-raise per unit of work; kill-switch'и; 0 jobs/0 токенов для синхронизированных задач;
|
||||
self-hosting — reconciler НИКОГДА не рестартит прод-контейнер.
|
||||
|
||||
## Решение
|
||||
|
||||
Чиним **оба** дефекта независимыми слоями (defense in depth, как принято в проекте —
|
||||
ORCH-058) плюс TTL для кэша. Все правки локальны в `src/reconciler.py` и
|
||||
`src/plane_sync.py`; реестры, схема БД и контракты обработчиков не меняются.
|
||||
|
||||
### Слой D1 — исключение терминалов по ГРУППЕ состояния (TR-1, AC-2)
|
||||
|
||||
Различаем терминальные (`completed`/`cancelled`) и review/work-статусы по **группе
|
||||
состояния Plane** (`state.group ∈ {backlog, unstarted, started, completed, cancelled}`),
|
||||
а НЕ по голому UUID. Группа — авторитетный, проектно-независимый дискриминатор: она
|
||||
корректно различает `Done` (completed) и `approved` (started/review) даже когда проект
|
||||
«схлопывает» их по UUID после переименований.
|
||||
|
||||
Механика (single API fetch, без новых сетевых вызовов):
|
||||
- `/states/`-ответ Plane содержит для каждого статуса поле `group`. Расширяем кэш-запись
|
||||
`_STATES_CACHE` так, чтобы из ОДНОГО запроса хранить и текущий `{logical_key → uuid}`,
|
||||
и `{uuid → group}`. `get_project_states` сохраняет **прежнюю сигнатуру и форму возврата**
|
||||
(`{logical_key: uuid}`) — обратная совместимость (AC-13). Добавляется sibling-аксессор
|
||||
`get_project_state_groups(project_id) -> dict[uuid, group]` (или эквивалент), читающий ту
|
||||
же кэш-запись.
|
||||
- В `_reconcile_plane_issue` ДО выбора ветки: если группа `new_state` ∈
|
||||
{`completed`, `cancelled`} → **тишина** (return, no-op). Fallback, когда группа
|
||||
недоступна (API не отдал `group` / fallback на `_DEFAULT_STATES`): исключать по логическим
|
||||
ключам терминалов `{states.get("done"), states.get("cancelled")}`.
|
||||
|
||||
Терминал-исключение применяется **per-issue** (а не сужением `wanted`-набора
|
||||
`list_issues_by_state`), потому что при UUID-алиасинге терминал может физически совпадать с
|
||||
actionable-UUID в `wanted` — фильтрация по UUID его не отсечёт, а проверка группы отсечёт.
|
||||
|
||||
### Слой D2 — `_note_unblock` только при подтверждённом state change (TR-2, AC-3)
|
||||
|
||||
`_note_unblock` (лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО когда диспетчеризованный
|
||||
обработчик **фактически изменил состояние задачи**. Реализация — сравнение состояния
|
||||
**до/после на стороне reconciler** (предпочтение ТЗ; контракты `handle_*` НЕ меняются):
|
||||
- `approved`/`rejected` (task существует): захватить `stage_before` (из уже прочитанного
|
||||
`task`), после `_dispatch` перечитать `get_task_by_plane_id(issue_id)` → `stage_after`;
|
||||
`_note_unblock` только если `stage_after != stage_before`.
|
||||
- `in_progress` + `task is None` (старт пайплайна): подтверждение = задача **появилась**
|
||||
после dispatch (`get_task_by_plane_id` теперь не None).
|
||||
|
||||
No-op dispatch (задача уже в целевом состоянии) → 0 нотификаций. Восстанавливает соответствие
|
||||
docstring и инвариант silence-when-in-sync.
|
||||
|
||||
### Слой TR-3 — дедуп нотификаций (страховка, AC-4)
|
||||
|
||||
In-memory best-effort guard: `{issue_id → last_unblocked_state_uuid}`. `_note_unblock` для
|
||||
issue+state, уже отмеченного, подавляется. Сбрасывается при рестарте (допустимо — AC-11
|
||||
ORCH-053, как `unblocked_total`/`last_unblocked`). D1+D2 закрывают основной кейс; TR-3 —
|
||||
дополнительная сетка против любого будущего no-op-пути.
|
||||
|
||||
### Слой TR-4 — TTL кэша статусов (secondary, AC-12/AC-13)
|
||||
|
||||
Кэш-запись `_STATES_CACHE` хранит timestamp; `get_project_states` перезапрашивает API при
|
||||
истечении `ORCH_PLANE_STATES_TTL_S`. Примитив инвалидации — существующий
|
||||
`reload_project_states()` (не дублируем логику сброса). Новый флаг
|
||||
`plane_states_ttl_s` (env `ORCH_PLANE_STATES_TTL_S`):
|
||||
- дефолт **300** (5 мин) — устаревший набор самозалечивается без рестарта (G5);
|
||||
- `0` — отключает TTL → строго прежний lifetime-кэш (escape hatch / strict back-compat).
|
||||
|
||||
Fallback на `_DEFAULT_STATES` при недоступности API сохранён без изменений; TTL-перезапрос
|
||||
возвращает тот же корректный набор → не регресс (AC-13).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Явный allowlist логических ключей терминалов (`done`/`cancelled`) без группы** —
|
||||
отклонён как primary: хрупок к будущим переименованиям/добавлению completed-статусов
|
||||
(`Monitoring after Deploy` и т.п.) и к UUID-алиасингу. Оставлен как **fallback**, когда
|
||||
`group` недоступен.
|
||||
- **Сужение `wanted`-набора в `list_issues_by_state`** — недостаточно: при UUID-алиасинге
|
||||
терминал совпадает с actionable-UUID и не отсекается фильтром по UUID. Нужна проверка
|
||||
группы per-issue.
|
||||
- **Проброс «changed»-сигнала из `handle_*`** — отклонён: меняет контракт обработчиков
|
||||
(запрещено ТЗ N2). Выбрано сравнение до/после на стороне reconciler.
|
||||
- **Флаг подавления нотификаций в `advance_stage`** — отклонён (как и в adr-0007):
|
||||
трогает общий критический путь.
|
||||
- **flush-on-unknown как primary для кэша** — допустимо ТЗ и дешевле, но недетерминирован
|
||||
для юнит-теста (TC-11) и не лечит «тихий устаревший набор» без триггера-вебхука. Выбран
|
||||
TTL (детерминированный, самозалечивающий); flush-on-unknown может быть добавлен позже как
|
||||
комплемент, переиспользуя `reload_project_states`.
|
||||
- **Admin-эндпоинт `POST /admin/plane-states/reload`** — отклонён в объёме (требует
|
||||
ручного действия, не лечит автоматически); TTL покрывает G5 без нового API.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Плюсы:** livelock устранён двумя независимыми слоями; терминал-исключение
|
||||
проектно-независимо (enduro И orchestrator), устойчиво к будущим переименованиям статусов;
|
||||
`_note_unblock` снова соответствует своему контракту; устаревший кэш самозалечивается без
|
||||
рестарта прода. Реестры/схема/контракты/F-1/F-3 не тронуты.
|
||||
- **Минусы / плата:** один доп. accessor группы (тот же API-запрос, без новой сетевой
|
||||
стоимости); TTL добавляет редкий перезапрос `/states/` (раз в 5 мин/проект); дедуп-словарь
|
||||
— небольшая in-memory структура, неперсистентная (приемлемо).
|
||||
- **Совместимость:** `get_project_states` форма возврата неизменна; `plane_states_ttl_s=0`
|
||||
→ строго прежнее поведение кэша; `_DEFAULT_STATES`-fallback сохранён.
|
||||
- **Self-hosting:** ни один путь не рестартит прод-контейнер (AC-11); правка
|
||||
обязательно проходит staging-гейт (8501) перед прод-деплоем орка.
|
||||
- **Наблюдаемость (опц.):** допустимо добавить в блок `reconcile` снимка `GET /queue`
|
||||
счётчики `skipped_terminal` / `deduped` без ломающих изменений.
|
||||
|
||||
## Инварианты (подтверждение)
|
||||
|
||||
INV-1 источник истины F-2 = Plane — сохранён (правим маппинг/нотификацию, не концепцию).
|
||||
INV-2 never-raise per-issue/-project/-tick — сохранён (новый guard в том же try-периметре).
|
||||
INV-3 kill-switch'и `ORCH_RECONCILE_ENABLED` / `ORCH_RECONCILE_PLANE_ENABLED` — без изменений.
|
||||
INV-4 F-1/F-3 — не тронуты. INV-5 0 jobs/0 токенов для done/cancelled — восстановлен.
|
||||
INV-6 легитимная разблокировка реально-потерянного approved/in_progress — работает (D2
|
||||
подтверждает реальный change, не подавляет его). INV-7 self-hosting — тик не рестартит прод.
|
||||
|
||||
## Объём изменений (для разработчика)
|
||||
|
||||
- `src/reconciler.py`: терминал-гард по группе + fallback в `_reconcile_plane_issue`;
|
||||
before/after-сравнение стадии вокруг `_dispatch`; in-memory дедуп-словарь в `Reconciler`.
|
||||
- `src/plane_sync.py`: кэш-запись с timestamp + `{uuid→group}`; `get_project_state_groups`;
|
||||
TTL-логика в `get_project_states` (переиспользуя `reload_project_states`).
|
||||
- `src/config.py`: флаг `plane_states_ttl_s` (env `ORCH_PLANE_STATES_TTL_S`, дефолт 300).
|
||||
- `.env.example` / `.env.staging`: задокументировать `ORCH_PLANE_STATES_TTL_S`.
|
||||
- Доки в ТОМ ЖЕ PR: `docs/architecture/README.md` (Reconciler/Plane Sync), `CHANGELOG.md`
|
||||
(`fix:`), `CLAUDE.md` (при изменении наблюдаемого поведения), этот ADR.
|
||||
- Тесты: `04-test-plan.yaml` (TC-01…TC-13), офлайн (мок Plane/Telegram/`_dispatch`).
|
||||
17
docs/work-items/ORCH-068/10-tech-risks.md
Normal file
17
docs/work-items/ORCH-068/10-tech-risks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Технические риски — ORCH-068
|
||||
|
||||
| ID | Риск | Вероятность | Влияние | Митигация |
|
||||
|----|------|-------------|---------|-----------|
|
||||
| R-1 | Plane `/states/` не отдаёт поле `group` (старая версия API / урезанный ответ) → терминал-исключение по группе не срабатывает | Низкая | Высокое (рецидив livelock) | Fallback на логические ключи терминалов `{done, cancelled}` при отсутствии `group`; never-raise → консервативная тишина при сбое резолва |
|
||||
| R-2 | Over-exclusion: легитимная задача в started/review-группе ошибочно классифицирована как терминал → пропущена легитимная разблокировка (регресс INV-6) | Низкая | Среднее | Исключаем ТОЛЬКО группы `completed`/`cancelled`; `approved`/`rejected` относятся к started/unstarted → не задеты; регресс-тесты TC-06/TC-07 |
|
||||
| R-3 | Гонка before/after: между `stage_before` и `stage_after` живой webhook двигает стадию → ложный `_note_unblock` | Очень низкая | Низкое | active-job guard + `max_concurrency=1` уже сериализуют; дедуп TR-3 подавляет повтор; ложный unblock безвреден (0 jobs/токенов) |
|
||||
| R-4 | TTL `300s` провоцирует частые `/states/`-перезапросы при многих проектах | Низкая | Низкое | 1 запрос/проект/5 мин — пренебрежимо; `ORCH_PLANE_STATES_TTL_S=0` отключает TTL |
|
||||
| R-5 | TTL-перезапрос в момент недоступности Plane → временный fallback на `_DEFAULT_STATES` (enduro) для не-enduro проекта | Низкая | Среднее | Поведение идентично текущему cold-cache fallback; самозалечивается следующим успешным запросом; не хуже статус-кво |
|
||||
| R-6 | Дедуп-словарь растёт неограниченно (по issue_id) | Очень низкая | Низкое | Ключи — только реально разблокированные issue (редки); сбрасывается при рестарте; при необходимости — ограничить размер/LRU |
|
||||
| R-7 | Изменение в `get_project_states` (кэш-запись) ломает прочих потребителей формы возврата | Низкая | Высокое | Внешняя сигнатура и форма `{logical_key: uuid}` сохранены; группа — отдельный accessor; покрыто TC-12 (совместимость по умолчанию) |
|
||||
| R-8 | Self-hosting: правка в работающем прод-инструменте | — | Высокое | Обязательный staging-гейт (8501); запрет рестарта прод-контейнера в рамках задачи; INV-7 |
|
||||
|
||||
## Замечания
|
||||
- Все правки локальны (`reconciler.py`, `plane_sync.py`, `config.py`); схема БД, реестры
|
||||
`STAGE_TRANSITIONS`/`QG_CHECKS`, контракты `handle_*`, F-1/F-3 — не затронуты (AC-10).
|
||||
- Тесты офлайн (мок Plane API / Telegram / `_dispatch`) — сетевых вызовов в CI нет.
|
||||
47
docs/work-items/ORCH-068/12-review.md
Normal file
47
docs/work-items/ORCH-068/12-review.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-068
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-068
|
||||
|
||||
## Summary
|
||||
Фикс livelock reconciler F-2 (спам `_note_unblock` по синхронизированной done-задаче после ORCH-066) реализован чисто и полностью по ТЗ/ADR. Два независимых слоя (D1 терминал-исключение по группе состояния + D2 подтверждённый state change) плюс TR-3 дедуп и TR-4 TTL кэша. Правки строго локальны в `src/reconciler.py` (F-2), `src/plane_sync.py`, `src/config.py`. Запрещённые ТЗ артефакты (`STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, контракты `handle_*`, F-1, F-3) не тронуты — diff не выходит за 3 файла `src/`. `pytest tests/ -q` — **764 passed**.
|
||||
|
||||
## Соответствие ТЗ
|
||||
- **TR-1** (исключить терминалы) ✅ — `_is_terminal_state` по `state.group ∈ {completed, cancelled}` с fallback на логические ключи `done`/`cancelled`; проверка per-issue (а не сужением `wanted`), что корректно для UUID-алиасинга.
|
||||
- **TR-2** (`_note_unblock` только при реальном change) ✅ — `_stage_changed` (сравнение стадии до/после `_dispatch`), для in_progress-старта подтверждение = задача появилась; контракты `handle_*` не менялись.
|
||||
- **TR-3** (дедуп) ✅ — in-memory guard `{issue_id → state_uuid}`, best-effort, сброс при рестарте (как `unblocked_total`).
|
||||
- **TR-4** (TTL кэша) ✅ — `plane_states_ttl_s` (дефолт 300, `0`=lifetime), переиспользует `reload_project_states`; форма возврата `get_project_states` неизменна; при сбое перезапроса отдаётся stale-but-correct набор.
|
||||
|
||||
## Соответствие ADR
|
||||
ADR-001 (terminal-exclusion-and-cache-ttl) реализован 1:1: группа как primary-дискриминатор, allowlist-fallback, before/after-сравнение на стороне reconciler, TTL с инвалидацией через существующий примитив. Сквозной adr-0007 дополнен корректной ссылкой. Все инварианты INV-1…INV-7 сохранены (источник истины Plane, never-raise per-issue, kill-switch'и, F-1/F-3 нетронуты, self-hosting не рестартит прод).
|
||||
|
||||
## Критерии приёмки
|
||||
AC-1…AC-15 — все PASS. Покрытие тестами адресное: TC-01 (synced Done silence), TC-02 (aliased terminal по группе — ядро D1), TC-03 (Cancelled), TC-04 (no-op silence), TC-05 (дедуп), TC-06/TC-07 (легитимный unblock ×1), TC-08 (never-raise изоляция), TC-09 (kill-switch), TC-10 (enduro И orchestrator — headline-регресс), TC-11/TC-12 (TTL self-heal + back-compat + stale-on-failure).
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- нет
|
||||
|
||||
### P1 — Must fix
|
||||
- нет
|
||||
|
||||
### P2 — Should fix
|
||||
- нет
|
||||
|
||||
### P3 — Nice-to-have
|
||||
- [ ] Дедуп-guard ключуется по `issue_id → state_uuid` без сброса при смене состояния. Теоретический edge-case: задача legitимно проходит `approved`(uuid_X)→…→снова `approved`(тот же uuid_X) — повторное (но легитимное) уведомление будет подавлено. Функционального ущерба нет (advance выполняется в `_dispatch` независимо от нотификации), это потеря только уведомления, и D2 — основной гард. Best-effort по контракту ТЗ. Можно при желании чистить запись при детекте смены состояния away-and-back. Не блокирует.
|
||||
|
||||
## Документация
|
||||
Обновлена полно и в том же PR (AC-14 PASS):
|
||||
- `docs/architecture/README.md` — компонент **Plane Sync** (TTL + `{uuid→group}`) и секция **Reconciler/F-2/F-4** (терминал-исключение, дедуп, счётчики `skipped_terminal_total`/`deduped_total`); футер «обновлять при изменении» расширен записью ORCH-068.
|
||||
- `docs/architecture/adr/adr-0007-reconciler.md` — добавлена кросс-ссылка на per-WI ADR.
|
||||
- `docs/work-items/ORCH-068/06-adr/ADR-001-…` — детальный ADR (Accepted).
|
||||
- `CHANGELOG.md` — запись `### Fixed` (D1/D2/TR-3/TR-4, инварианты, тесты).
|
||||
- `.env.example` — `ORCH_PLANE_STATES_TTL_S=300` с комментарием.
|
||||
|
||||
Изменение `src/` сопровождено соответствующим обновлением документации — требование golden-source выполнено.
|
||||
64
docs/work-items/ORCH-068/13-test-report.md
Normal file
64
docs/work-items/ORCH-068/13-test-report.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-068
|
||||
result: PASS
|
||||
---
|
||||
|
||||
# Test Report — ORCH-068
|
||||
|
||||
Фикс livelock reconciler F-2 (спам `_note_unblock` по синхронизированной
|
||||
done-задаче) + связанный баг устаревшего `_STATES_CACHE`.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (plugins: anyio-4.13.0, asyncio-0.23.8)
|
||||
- Среда исполнения: worktree `feature/ORCH-068-bug-reconciler-livelock-unbloc`
|
||||
- Prod health (8500): `{"status":"ok","service":"orchestrator"}` — OK (read-only smoke)
|
||||
- Дата: 2026-06-08T05:13:59Z
|
||||
|
||||
## Smoke test API (read-only, прод 8500 не трогался деструктивно)
|
||||
| Endpoint | Результат |
|
||||
|----------|-----------|
|
||||
| `GET /health` | `{"status":"ok",...}` — OK |
|
||||
| `GET /status` | 200, активные задачи отданы — OK |
|
||||
| `GET /queue` | 200, counts + блок `reconcile` (enabled/plane_enabled/unblocked_total) — OK |
|
||||
|
||||
## Результаты
|
||||
|
||||
| TC ID | Описание | Тест | Результат |
|
||||
|-------|----------|------|-----------|
|
||||
| TC-01 | Синхронизированная done → тишина (AC-1, AC-7) | `test_tc01_synced_done_is_silent` | PASS |
|
||||
| TC-02 | Терминал «схлопнут» с approved по UUID исключён (AC-2) | `test_tc02_terminal_aliased_to_approved_excluded` | PASS |
|
||||
| TC-03 | Cancelled терминал исключён (AC-2) | `test_tc03_cancelled_excluded` | PASS |
|
||||
| TC-04 | `_note_unblock` не вызван после no-op dispatch (AC-3) | `test_tc04_noop_dispatch_no_unblock` | PASS |
|
||||
| TC-05 | Дедуп: 0 повторных уведомлений (AC-4) | `test_tc05_dedup_no_repeat_notification` | PASS |
|
||||
| TC-06 | Легитимный approved → unblock ровно один раз (AC-5) | `test_tc06_legit_approved_unblock_once` | PASS |
|
||||
| TC-07 | in_progress-старт и rejected-откат, каждый 1 unblock (AC-6) | `test_tc07_in_progress_start_and_rejected_each_one_unblock` | PASS |
|
||||
| TC-08 | never-raise: изоляция ошибок (AC-8) | `test_tc08_never_raise_isolation` | PASS |
|
||||
| TC-09 | Kill-switch'и F-2 (AC-9) | `test_tc09_kill_switches` | PASS |
|
||||
| TC-10 | done→silence на enduro И orchestrator (headline-регресс, AC-1/AC-2) | `test_tc10_done_silent_on_all_projects` | PASS |
|
||||
| TC-11 | Устаревший `_STATES_CACHE` self-heal по TTL (AC-12) | `test_tc11_stale_cache_refreshes_after_ttl` + accessor/zero-ttl тесты | PASS |
|
||||
| TC-12 | Совместимость кэша по умолчанию + fallback (AC-13) | `test_tc12_enduro_uuids_unchanged`, `test_tc12_api_error_falls_back_to_defaults`, `test_tc12_stale_served_when_refresh_fails` | PASS |
|
||||
| TC-13 | Полный прогон `pytest tests/` зелёный (AC-15) | весь набор | PASS |
|
||||
|
||||
## Соответствие критериям приёмки
|
||||
AC-1…AC-15 — все PASS. Целевые регресс-тесты (TC-01..TC-12) проходят;
|
||||
полный набор без регрессий в reconciler / plane / qg / stage_engine / webhooks.
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
### Целевые файлы (tests/test_reconciler_plane.py + tests/test_plane_states_cache.py)
|
||||
```
|
||||
collected 26 items
|
||||
... (все 26 PASSED, включая tc01..tc17 reconciler + tc11/tc12 cache)
|
||||
======================== 26 passed, 1 warning in 0.82s =========================
|
||||
```
|
||||
|
||||
### Полный набор
|
||||
```
|
||||
======================= 764 passed, 1 warning in 13.66s ========================
|
||||
```
|
||||
(1 warning — PydanticDeprecatedSince20 в src/config.py, не относится к ORCH-068, предсуществующий.)
|
||||
|
||||
## Итог
|
||||
PASS
|
||||
12
docs/work-items/ORCH-068/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-068/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-068
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
14
docs/work-items/ORCH-068/16-post-deploy-log.md
Normal file
14
docs/work-items/ORCH-068/16-post-deploy-log.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
post_deploy_status: HEALTHY
|
||||
action_taken: NONE
|
||||
work_item: ORCH-068
|
||||
window_s: 900
|
||||
checks_total: 30
|
||||
checks_failed: 0
|
||||
---
|
||||
|
||||
# Post-deploy log — ORCH-021 post-deploy monitor
|
||||
|
||||
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
|
||||
|
||||
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.
|
||||
Reference in New Issue
Block a user