From f5aae505147f91206d5ed3ad3100b8522fedb3d6 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 20:15:09 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=194 --- docs/architecture/README.md | 24 +- docs/architecture/adr/README.md | 1 + docs/architecture/adr/adr-0007-reconciler.md | 69 ++++++ .../06-adr/ADR-001-stuck-task-reconciler.md | 221 ++++++++++++++++++ .../ORCH-053/07-infra-requirements.md | 45 ++++ .../ORCH-053/08-data-requirements.md | 38 +++ docs/work-items/ORCH-053/10-tech-risks.md | 27 +++ 7 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/adr/adr-0007-reconciler.md create mode 100644 docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md create mode 100644 docs/work-items/ORCH-053/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-053/08-data-requirements.md create mode 100644 docs/work-items/ORCH-053/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 27710b4..7c6063e 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,6 +11,7 @@ - **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`. - **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. - **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. +- **Reconciler** (`src/reconciler.py`, ORCH-053 — design, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). - **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту. - **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. @@ -52,6 +53,27 @@ created → analysis → architecture → development → review → testing → Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`. +### Reconciler: реконсиляция потерянных webhook (ORCH-053 — design) +Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде, +нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча +(инцидент ORCH-044). Фоновый поток `reconciler` периодически (`reconcile_interval_s`) +находит застрявшие задачи и доигрывает пропущенный переход **через те же штатные +гейты/обработчики**, что и webhook: +- **F-1 gate-side:** для задач со `stage∉{done}`, без активного job и + `age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка канонического QG; + зелёный → `stage_engine.advance_stage(..., finished_agent=None)`; красный → + тишина (спам нотификаций структурно невозможен). `analysis` не реконсилируется. +- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` / + `handle_verdict` из `webhooks/plane.py` (логика не дублируется). +- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback). + +Инварианты: источник истины — гейт/Plane, не событие; идемпотентность (active-job +guard + atomic-claim на создании под process-wide Lock + grace + `max_concurrency=1`); +never-raise на единицу работы; тишина при синхронности; restart-safe; kill-switch +`ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2). Схема БД +и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее: +[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`. + ## Откаты - Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`). - Tester `check_tests_passed` FAIL → откат на `development` + retry. @@ -109,4 +131,4 @@ created → analysis → architecture → development → review → testing → Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md). --- -*Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043.* +*Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043. ORCH-053: reconciler — design (см. adr-0007), реализация в ветке feature/ORCH-053.* diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 0a4c74a..69183b8 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -11,6 +11,7 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 | | adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 | | adr-0006 | Merge-gate (догон main + re-test + сериализация слияний) | proposed | 2026-06-06 | ORCH-043 | +| adr-0007 | Reconciler застрявших стадий (sweeper потерянных webhook) | proposed | 2026-06-06 | ORCH-053 | ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0007-reconciler.md b/docs/architecture/adr/adr-0007-reconciler.md new file mode 100644 index 0000000..171ffdf --- /dev/null +++ b/docs/architecture/adr/adr-0007-reconciler.md @@ -0,0 +1,69 @@ +# adr-0007: Reconciler застрявших стадий (sweeper потерянных webhook) + +- **Статус:** proposed +- **Дата:** 2026-06-06 +- **Задача:** ORCH-053 +- **Детальный ADR:** `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md` + +## Контекст +Конвейер продвигается **только** входящими webhook (Plane status / Gitea CI/PR). +Потерянное событие (502 на ребилде, отсутствие ретраев у Plane/Gitea, +неразрезолвленный `sha→branch`) → источник истины изменился, а стадия задачи — +нет; задача застревает молча (инцидент ORCH-044). Существующий resilience +(`requeue_running_jobs`, orphan-recovery, events de-dup ORCH-5, `ci_poll` +ORCH-045) работает на уровне jobs/agent_runs и **не реконсилирует** +рассинхрон «источник истины ≠ стадия задачи». + +## Решение +Фоновый daemon-поток `src/reconciler.py` (паттерн `queue_worker`, module-singleton, +`threading.Event`), стартует в `main.lifespan` после `worker.start()`, стоп в +`finally` перед `worker.stop()`. Две взаимодополняющие ветки на каждом тике +(`reconcile_interval_s`, дефолт 120с): + +- **F-1 gate-side** (локальная БД): для каждой `task` где `stage∉{done}`, **нет** + активного job, `age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка + канонического QG стадии; если зелёный → продвижение **штатным** + `stage_engine.advance_stage(..., finished_agent=None)` (тот же путь, что у Plane + Approved-webhook). Красный → **тишина** (нет advance, нет нотификаций — спам + структурно невозможен). `analysis` F-1 **не** реконсилирует (человеческий гейт → + отдан F-2). +- **F-2 plane-side** (опрос Plane API per-project через `list_issues_by_state`): + `In Progress`+нет задачи → `handle_status_start`; `Approved`+не сдвинута → + `handle_verdict(approved=True)`; `Rejected`+не откатана → + `handle_verdict(approved=False)`. Обработчики `webhooks/plane.py` + **переиспользуются** (async → `asyncio.run` из sync-потока), логика не дублируется. +- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по + `repo`+`stage='development'`, видимость на INFO) — defense-in-depth. + +**Инварианты:** источник истины — гейт/Plane, не событие; продвижение только через +`advance_stage`; идемпотентность (active-job guard + atomic-claim на создании + +grace + `max_concurrency=1`); never-raise на единицу работы; тишина при +синхронности; restart-safe; kill-switch. + +## Альтернативы +- **Флаг подавления нотификаций в `advance_stage`** — отклонён: меняет общий + критический путь. Вместо этого «не вызывать advance_stage на красном гейте». +- **UNIQUE-индекс `tasks.plane_id`** для анти-дубля — отклонён как primary: риск + падения миграции на проде; выбран process-wide `threading.Lock` (single-process + топология). Индекс — задокументированное будущее упрочнение для multi-process. +- **Отдельная стадия/QG реконсиляции** — вне объёма; нарушает «источник истины — + существующий гейт». +- **Реконсиляция analysis по локальным артефактам** — отклонена: автопродвижение + неодобренного человеком BRD. + +## Последствия +- Потерянный webhook ≠ молча застрявшая задача; ручной heartbeat-watchdog не нужен; + резервная сетка к ORCH-51 (буфер недоставленных) и ORCH-36 (deploy). +- Плата: фоновый поток + опрос Plane API (митигируется интервалом/фильтром/ + per-project); двойная оценка гейта на зелёной задаче; анти-дубль опирается на + single-process-допущение (как и очередь ORCH-1). +- Self-hosting: `reconcile_enabled` — обязательный kill-switch; поэтапный раскат + (`reconcile_plane_enabled` гасит только F-2); reconciler не рестартит/не роняет + прод-контейнер. БД-схема и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. + +## Связи +adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный +гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра +внутри `advance_stage`), adr-0001 (реестр проектов для F-2 per-project), ORCH-5 +(events de-dup — защита от дублей; reconciler — обратная защита от потерь), +ORCH-045 (`ci_poll`). diff --git a/docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md b/docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md new file mode 100644 index 0000000..6b50624 --- /dev/null +++ b/docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md @@ -0,0 +1,221 @@ +# ADR-001: Sweeper/reconciler потерянных webhook (реконсиляция застрявших стадий) + +- **Статус:** Proposed +- **Дата:** 2026-06-06 +- **Задача:** ORCH-053 +- **Сквозной ADR:** `docs/architecture/adr/adr-0007-reconciler.md` +- **Связи:** adr-0001 (реестр проектов), adr-0002 (очередь / `available_at`), + adr-0003 (условный staging-гейт — образец условности), adr-0006 (merge-gate как + под-гейт ребра), ORCH-5 (events de-dup), ORCH-045 (`ci_poll`). + +## Контекст + +Продвижение задач по конвейеру завязано **исключительно** на входящие webhook +(Plane status / Gitea CI/PR). Потерянное событие (502 на ребилдящемся инстансе, +Plane/Gitea не ретраят, `sha→branch` не разрезолвился) → источник истины (Plane / +зелёный CI) изменился, а задача в оркестраторе застряла молча (живой инцидент +ORCH-044). Ни один существующий механизм resilience (`requeue_running_jobs`, +orphan-recovery, events de-dup, `ci_poll`) не реконсилирует рассинхрон +**«источник истины ≠ стадия задачи»** — все они работают на уровне jobs/agent_runs, +а не stage-перехода. + +ТЗ (`02-trz.md`) фиксирует объём; данный ADR фиксирует архитектурные развилки, +явно отданные архитектору: (1) потокобезопасность и подавление спама нотификаций, +(2) способ вызова `async`-обработчиков `plane.py` из sync-потока, (3) atomic-claim +на создании задачи (анти-дубль), (4) критерий «потерян, а не задержан» (grace), +(5) отсутствие изменений схемы БД. + +## Решение + +### 1. Компонент: `src/reconciler.py` — фоновый daemon-поток + +Новый модуль по образцу `queue_worker.py`: класс `Reconciler` + module-singleton +`reconciler`. Plain `threading.Thread(daemon=True)` + `threading.Event` для +остановки. Стартует в `main.lifespan` **после** `worker.start()`, останавливается в +`finally` **перед** `worker.stop()`. Цикл: + +``` +while not stop: + try: + if settings.reconcile_enabled: + reconcile_gate_once() # F-1 + if settings.reconcile_plane_enabled: + reconcile_plane_once() # F-2 + except Exception: log.error(...) # outer never-raise + stop.wait(settings.reconcile_interval_s) +``` + +`start()` идемпотентен (как `QueueWorker.start`: если поток жив — no-op), что +покрывает AC-11 (повторный start не плодит второй поток). Никакого критичного +состояния в памяти — всё перечитывается из БД/Plane на каждом тике; метрики +наблюдаемости (`last_run_ts`, `unblocked_total`) — best-effort, теряются при +рестарте (AC-11 это явно допускает). + +### 2. Источник истины — гейт, не событие. Продвижение строго через `advance_stage` + +F-1 НЕ дублирует логику advance. Вводится тонкий хелпер в `stage_engine.py`: + +```python +def advance_if_gate_passed(task_id, stage, repo, work_item_id, branch) -> AdvanceResult | None +``` + +Алгоритм: +1. `stage == "analysis"` → немедленный возврат `None` (см. §6, AC-16). +2. `qg = get_qg_for_stage(stage)`; если `None` (created/done) → возврат `None`. +3. **Read-only пред-оценка гейта** тем же диспетчером, что использует webhook-путь: + `passed, reason = _run_qg(qg, repo, work_item_id, branch)`. +4. **passed** → вызвать `advance_stage(task_id, stage, repo, work_item_id, branch, + finished_agent=None)` — это **тот же** путь, которым продвигает Plane + Approved-webhook (`webhooks/plane._try_advance_stage`). Он повторно прогонит + гейт (гейты идемпотентны/read-only), продвинет стадию, отправит **штатные** + advance-нотификации и поставит следующего агента. +5. **not passed** → **тишина**: `logger.debug(...)`, возврат `None`. Никаких + `notify_qg_failure` / `plane_notify_qg`. + +Это даёт оба контракта одновременно: +- **AC-2 / TC-02:** в `reconciler.py` нет собственного `update_task_stage` + + `enqueue_job` для advance — продвижение исключительно через `advance_stage(..., + finished_agent=None)`. +- **AC-9 / TC-06:** на застрявшей-но-красной задаче `advance_stage` **не + вызывается вовсе**, поэтому ветка нотификации провала (`agent is None` → + `notify_qg_failure`+`plane_notify_qg`, `stage_engine.py:228-230`) не + срабатывает ни на одном тике. Спам структурно невозможен. + +**Подавление спама = «не вызывать advance_stage на красном гейте»**, а не флаг +внутри `advance_stage`. Это сохраняет унифицированный критический путь +(`advance_stage`) **без изменений** — минимальный blast-radius для self-hosting. + +> **Цена (осознанная):** на «зелёной» задаче гейт оценивается дважды (пред-оценка +> в хелпере + повтор внутри `advance_stage`). Гейты — чистые read-only проверки +> (`check_ci_green`, `check_*_status` из `12/13/14/15`), на реально-застрявшей-но- +> готовой задаче (целевой кейс ORCH-044) возвращаются быстро (CI уже зелёный → +> `ci_poll` отдаёт результат на первой итерации). Двойная оценка приемлема ради +> неизменности `advance_stage`. + +#### Отклонённая альтернатива: флаг `suppress_qg_failure_notify` в `advance_stage` +Однократная оценка гейта, но изменяет сигнатуру и поведение общего +критического пути (риск для self-hosting, обслуживающего все проекты). Отклонено +в пользу неизменности `advance_stage` (Option A выше). + +### 3. F-2: вызов `async`-обработчиков `plane.py` из sync-потока + +Reconciler — sync daemon-поток; `handle_status_start` / `handle_verdict` — +`async`. Решение: вызывать через **`asyncio.run(coro)`** на каждую единицу работы +внутри per-issue `try/except`. `asyncio.run` создаёт свежий event loop на вызов, +что необходимо, т.к. `handle_verdict → _try_advance_stage` использует +`asyncio.to_thread` (требует running loop). Логику **не дублировать** — +переиспользуются ровно `handle_status_start` / `handle_verdict` / +`list_issues_by_state`. + +`issue_data` собирается в форму, ожидаемую обработчиками (`{"id", "state":{"id":..}, +"project", "name", "description_stripped"}`); недостающие name/description +обработчики сами дотянут через `fetch_issue_fields` (как для status-only webhook). + +### 4. Идемпотентность создания (анти-дубль, AC-4) — atomic-claim в БД + +Гонка: F-2 видит `In Progress` + нет задачи; одновременно реальный webhook тоже +видит `In Progress` + нет задачи → оба проходят `get_task_by_plane_id() is None` +→ два `start_pipeline` → два task-row / branch / worktree / стартовых analyst-job +(events de-dup тут НЕ помогает: reconciler — не webhook-доставка). + +Решение: **atomic-claim создания, защищённый process-wide `threading.Lock`**. +Новый хелпер `db.create_task_atomic(plane_id, ...)` выполняет +`SELECT-exists → INSERT` под module-level `Lock`, возвращая `(row, created: bool)`: +только победитель (`created=True`) продолжает branch/docs/analyst; проигравший +видит существующую задачу и выходит. `start_pipeline` рефакторится так, чтобы +**первым** DB-действием был этот claim; reconciler идёт тем же путём через +`handle_status_start` → `start_pipeline`. + +**Обоснование выбора Lock, а не UNIQUE-индекса:** +- Прод — **один процесс** uvicorn на одну БД (staging/prod изолированы своими БД); + webhook исполняется в asyncio-треде uvicorn, reconciler — в своём треде того же + процесса → `threading.Lock` покрывает обе стороны гонки. +- **Без миграции схемы** (соответствует §6 ТЗ и образцу merge-gate ORCH-043). + `CREATE UNIQUE INDEX` на `tasks.plane_id` рискует упасть на проде, если там уже + существуют дубли `plane_id` (исторические) — а проверить это вживую нельзя. +- Дешёвый fast-path `get_task_by_plane_id` сохраняется до claim. + +> **Граница применимости:** гарантия верна для single-process деплоя (текущая +> топология). Многопроцессный запуск (`uvicorn --workers N`) потребовал бы +> DB-native UNIQUE-индекса — задокументировано как будущее упрочнение в +> `08-data-requirements.md`. Очередь (`queue_worker`) уже опирается на ту же +> single-process-singleton модель, так что допущение не новое. + +### 5. Анти-гонка с живым webhook (AC-3) — active-job guard + grace + +- **Active-job guard:** `has_active_job_for_task(task.id) == True` → задача + легитимно в работе или живой webhook только что поставил job → **skip** (ни + пред-оценки гейта, ни advance, ни нотификаций). И в F-1, и в F-2. +- **Самозатухание повторов:** `advance_stage → update_task_stage` обновляет + `tasks.updated_at` → следующий тик увидит свежий `updated_at` и не сработает + повторно (grace). +- `max_concurrency=1`: новый enqueued job встаёт в общую очередь — двойного + запуска нет (atomic `claim_next_job`). + +### 6. F-1 НЕ реконсилирует `analysis` (AC-16) + +Гейт `check_analysis_approved` — **человеческий**. В `advance_stage` при +`finished_agent=None` он трактуется как `approved-via-status` и продвинул бы +задачу. Но при потере именно **Approved**-webhka продвигать analysis допустимо +**только** если Plane реально в статусе Approved — этого локальная БД не знает. +Поэтому advance стадии `analysis` отдан **F-2** (сверяется с реальным статусом +Plane). `advance_if_gate_passed` для `stage == "analysis"` — ранний возврат +`None`. Защита от автопродвижения неодобренного человеком BRD. + +### 7. Grace: критерий «потерян, а не задержан» + +- **F-1:** кандидат, если `has_active_job_for_task == False` **и** + `age(tasks.updated_at) >= grace_for_stage(stage)`. + `grace_for_stage(stage)` = per-stage override из `reconcile_grace_overrides_json`, + иначе `reconcile_grace_default_s`. Невалидный JSON → дефолт (паттерн + `agent_timeout_overrides_json`, never-raise). +- **F-2:** источник «давности» — поле `updated_at` **issue из Plane** (когда статус + реально сменился). Реагировать только если `age(issue.updated_at) >= + reconcile_grace_default_s` — отсекает просто задержавшийся webhook. Для + существующей задачи дополнительно требуется отсутствие активного job. + +### 8. F-3: усиление `sha→branch` в `handle_ci_status` + +При неразрезолвленном branch (нет `branches`, `git branch -r --contains` пуст) — +fallback на БД: найти task'и repo со `stage='development'`; при **однозначности** +(ровно одна) использовать её branch; при неоднозначности — оставить +неразрезолвленным + `logger.info`. `logger.debug → logger.info` для видимости. +Success/failure-семантика гейта не меняется. Defense-in-depth: F-1 всё равно +подберёт такую задачу. + +### 9. БД и реестры — без изменений + +- Схема **не меняется** (§6 ТЗ). Стуковость — по `tasks.updated_at`/`tasks.stage` + + `has_active_job_for_task`. Анти-дребезг колонкой `last_reconcile_at` **не + нужен**: на красном гейте действий/нотификаций нет вовсе (§2), а после advance + `updated_at` обновляется → повтор невозможен. +- `STAGE_TRANSITIONS` и `QG_CHECKS` **не меняются** (AC / TC-23). Новых QG нет. + +### 10. Наблюдаемость (F-4) + +- При **разблокировке** (произошёл advance) — явная лог-строка + `reconciler: разблокирована (потерян webhook)`; при + `reconcile_notify_unblock=True` — `send_telegram(...)`. Только на изменении + состояния, не на каждый тик (AC-12, не конфликтует с AC-9/AC-10). +- `/queue` расширяется блоком `"reconcile": {enabled, plane_enabled, interval, + last_run_ts, unblocked_total, last_unblocked}` по образцу `worker.status()`. + +## Альтернативы (сводно) +- **Флаг подавления нотификаций в `advance_stage`** — отклонён (§2): изменяет общий + критический путь. +- **UNIQUE-индекс на `tasks.plane_id`** — отклонён как primary (§4): риск падения + миграции на проде; задокументирован как будущее упрочнение для multi-process. +- **Отдельная стадия/QG для реконсиляции** — вне объёма; нарушило бы «источник + истины — существующий гейт». +- **Реконсиляция analysis по локальным артефактам** — отклонена (§6): риск + автопродвижения неодобренного BRD. + +## Последствия +- Потерянный webhook больше не = молча застрявшая задача; ручной heartbeat-watchdog + Стрима не нужен; резервная сетка к ORCH-51/ORCH-36. +- Плата: фоновый поток + периодический опрос Plane API (нагрузка — митигируется + интервалом + фильтром по статусам + per-project); двойная оценка гейта на зелёной + задаче; анти-дубль на создании опирается на single-process-допущение. +- Self-hosting: kill-switch `reconcile_enabled` обязателен; reconciler не + рестартит/не роняет прод-контейнер; раскат поэтапный (флаги). +- Сквозной resilience-механизм → сопровождается global `adr-0007`. diff --git a/docs/work-items/ORCH-053/07-infra-requirements.md b/docs/work-items/ORCH-053/07-infra-requirements.md new file mode 100644 index 0000000..f7ea892 --- /dev/null +++ b/docs/work-items/ORCH-053/07-infra-requirements.md @@ -0,0 +1,45 @@ +# 07 — Требования к инфраструктуре — ORCH-053 + +Work Item ID: ORCH-053 + +## Топология +**Без изменений.** Новых контейнеров/портов/сервисов нет. Reconciler — фоновый +daemon-поток **внутри** существующего процесса orchestrator (как `queue_worker`). +Стартует/останавливается в `main.lifespan`. Деплой ORCH-053 — строго через +staging-гейт (8501) перед прод-деплоем (self-hosting, см. `docs/operations/INFRA.md`). + +## Новые переменные окружения (`.env` / `.env.staging` на хосте, префикс `ORCH_`) + +| Env | Поле `Settings` | Дефолт | Назначение | +|-----|-----------------|--------|-----------| +| `ORCH_RECONCILE_ENABLED` | `reconcile_enabled` | `true` | **Kill-switch** всего sweeper'а (self-hosting safety, поэтапный раскат, аварийное отключение). | +| `ORCH_RECONCILE_INTERVAL_S` | `reconcile_interval_s` | `120` | Период фонового прохода (сек). | +| `ORCH_RECONCILE_PLANE_ENABLED` | `reconcile_plane_enabled` | `true` | Отдельный флаг F-2 (опрос Plane API); `false` гасит только plane-ветку, F-1 работает. | +| `ORCH_RECONCILE_GRACE_DEFAULT_S` | `reconcile_grace_default_s` | `600` | Дефолтный порог «застревания» по `tasks.updated_at` / `issue.updated_at`. | +| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | `reconcile_grace_overrides_json` | `""` | Per-stage пороги, напр. `{"analysis":1800,"development":300,"deploy":900}`. Невалидный JSON → дефолт (never-raise). | +| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | `reconcile_notify_unblock` | `true` | Telegram при разблокировке (F-4). | + +Секреты не добавляются. `.env.example` (канон) обновляется в PR реализации. + +## Нагрузка / сеть +- **Plane API (F-2):** GET issues per-project каждые `reconcile_interval_s`, с + фильтром по статусам (In Progress / Approved / Rejected) и пагинацией. Митигация + нагрузки — интервал (120с), фильтр, per-project, never-raise (Plane outage → + `[]`, тик не падает). `get_project_states` уже кэширует state-uuid per-project. +- **Gitea API (F-1):** только косвенно — внутри переоценки гейтов (`check_ci_green` + и т.п.), которые и так вызываются webhook-путём. Дополнительных постоянных + вызовов reconciler не вносит сверх момента реальной разблокировки. +- **CPU/RAM:** один спящий daemon-поток; всплеск только при наличии застрявших + задач. + +## Self-hosting +- Reconciler **не** рестартит/не роняет прод-контейнер `orchestrator` (8500), + обслуживающий все проекты с общей БД. +- `docs/operations/INFRA.md` дополняется упоминанием kill-switch + `ORCH_RECONCILE_ENABLED` (выполняется в PR реализации, §9 ТЗ). +- Раскат: при первом деплое допустимо стартовать с `ORCH_RECONCILE_PLANE_ENABLED=false` + (только F-1, минимальный риск), затем включить F-2. + +## Конфиги/деплой +Дополнительных томов, портов, healthcheck'ов, изменений `docker-compose`/Dockerfile +**не требуется**. diff --git a/docs/work-items/ORCH-053/08-data-requirements.md b/docs/work-items/ORCH-053/08-data-requirements.md new file mode 100644 index 0000000..63f9962 --- /dev/null +++ b/docs/work-items/ORCH-053/08-data-requirements.md @@ -0,0 +1,38 @@ +# 08 — Требования к данным / схеме БД — ORCH-053 + +Work Item ID: ORCH-053 + +## Изменения схемы: НЕТ + +Реконсиляция строится исключительно на существующих структурах (по образцу +merge-gate ORCH-043 — «без новых колонок»): + +| Структура | Использование reconciler | +|-----------|--------------------------| +| `tasks.stage` | Кандидаты F-1: `stage NOT IN ('done')`; `created`/`analysis` отфильтровываются (нет QG / человеческий гейт). | +| `tasks.updated_at` | Критерий «застряла»: `age(updated_at) ≥ grace_for_stage(stage)`. `update_task_stage` уже штампует `updated_at` → самозатухание повторов. | +| `tasks.repo`, `tasks.branch`, `tasks.work_item_id`, `tasks.plane_id` | Аргументы `advance_stage` / резолв задачи. | +| `jobs` (`has_active_job_for_task`) | Active-job guard (AC-3): задача с `queued`/`running` job не трогается. | + +## Анти-дребезг (`last_reconcile_at`): НЕ вводится +На красном гейте reconciler не делает ни advance, ни нотификаций (см. ADR-001 §2), +поэтому спама нет структурно; после успешного advance обновляется `updated_at` → +повтор невозможен. Дополнительная колонка для дебаунса не нужна. + +## Идемпотентность создания (анти-дубль, AC-4) +Гонка reconciler↔webhook на создании задачи закрывается **process-wide +`threading.Lock`** вокруг `SELECT-exists → INSERT` (новый хелпер +`db.create_task_atomic`), **без** изменения схемы. Гарантия верна для текущей +**single-process** топологии (один uvicorn на одну БД; staging/prod изолированы) — +тот же допущение, что у очереди `queue_worker` (ORCH-1). + +### Будущее упрочнение (вне объёма ORCH-053) +Для multi-process деплоя (`uvicorn --workers N`) потребуется DB-native гарантия: +частичный UNIQUE-индекс `CREATE UNIQUE INDEX ... ON tasks(plane_id) WHERE plane_id +IS NOT NULL` (паттерн `idx_events_delivery`) + `INSERT OR IGNORE` claim. Не вводим +сейчас: миграция может упасть на проде при наличии исторических дублей `plane_id` +(проверить вживую нельзя); требует отдельной задачи с аудитом данных. + +## Миграции +Не требуются. Если в будущем понадобится колонка — только идемпотентный +`_ensure_column` (как все ALTER в `src/db.py`), безопасный на живой прод-БД. diff --git a/docs/work-items/ORCH-053/10-tech-risks.md b/docs/work-items/ORCH-053/10-tech-risks.md new file mode 100644 index 0000000..41c1b3c --- /dev/null +++ b/docs/work-items/ORCH-053/10-tech-risks.md @@ -0,0 +1,27 @@ +# 10 — Технические риски — ORCH-053 + +Work Item ID: ORCH-053 +Severity: 🔴 high / 🟡 medium / 🟢 low + +| # | Риск | Sev | Митигация (где зафиксировано) | +|---|------|-----|-------------------------------| +| R-1 | **Гонка reconciler↔живой webhook → двойная задача** (оба видят «нет задачи» на `In Progress`). | 🔴 | Atomic-claim `db.create_task_atomic` под process-wide `threading.Lock` (ADR-001 §4, 08-data). AC-4 / TC-15. | +| R-2 | **Двойной запуск агента** на стадии (reconciler дёргает гейт у задачи в работе). | 🔴 | `has_active_job_for_task` guard + `max_concurrency=1` + atomic `claim_next_job`; `update_task_stage` обновляет `updated_at` (ADR-001 §5). AC-3 / TC-03. | +| R-3 | **Спам нотификаций** на стабильно красном гейте каждый тик. | 🔴 | «Не вызывать `advance_stage` на красном» → ветка `notify_qg_failure` не достигается (ADR-001 §2). AC-9 / TC-06. | +| R-4 | **Автопродвижение неодобренного BRD** (F-1 продвинул `analysis` без Approved в Plane). | 🔴 | F-1 не реконсилирует `analysis`; advance стадии — только F-2 по реальному статусу Plane (ADR-001 §6). AC-16 / TC-08. | +| R-5 | **Дублирование логики advance/rollback** в reconciler (расхождение с webhook-путём со временем). | 🟡 | Продвижение строго через `advance_stage(..., finished_agent=None)`; F-2 — через `handle_*` из `plane.py`; своего `update_task_stage`/`enqueue_job` для advance нет (ADR-001 §2-3). AC-2 / TC-02. | +| R-6 | **Падение тика из-за одной битой задачи/issue** (битые данные, ошибка API). | 🟡 | Per-task / per-issue `try/except` + outer `try/except` в `_run` (паттерн `_drain_once`). AC-15 / TC-09. `list_issues_by_state` never-raise → `[]`. TC-16. | +| R-7 | **Нагрузка/недоступность Plane API** при опросе каждые N сек. | 🟡 | Интервал 120с + фильтр по статусам + per-project + кэш `get_project_states`; never-raise → мягкая деградация (ADR-001 §3, 07-infra). | +| R-8 | **`asyncio.run` из sync-потока** (event loop конфликты, зависание). | 🟡 | Свежий loop на единицу работы; внутри per-issue try/except; нет вложенного running loop (reconciler — не async). ADR-001 §3. | +| R-9 | **Self-hosting: reconciler меняет инструмент всех проектов** / нежелательное срабатывание на проде. | 🔴 | Kill-switch `reconcile_enabled`; раздельный `reconcile_plane_enabled`; деплой через staging-гейт; не рестартит прод. ADR-001 §1, 07-infra. AC-13 / TC-10. | +| R-10 | **Двойная оценка гейта** на зелёной задаче (пред-оценка + повтор в `advance_stage`); долгий `ci_poll` держит тик. | 🟢 | Гейты идемпотентны/read-only; на целевом кейсе (CI уже зелёный) возвращаются быстро; reconciler — отдельный daemon-поток. Осознанная цена за неизменность `advance_stage` (ADR-001 §2). | +| R-11 | **Ложный `sha→branch` матч** в F-3 при неоднозначности. | 🟡 | БД-fallback срабатывает только при ровно одной `development`-задаче repo; иначе — неразрезолвлено + INFO; success/failure-семантика гейта не тронута (ADR-001 §8). AC-14 / TC-18, TC-19. | +| R-12 | **Регресс реестров** (`STAGE_TRANSITIONS`/`QG_CHECKS`) или схемы. | 🟡 | Реестры/схема не меняются; snapshot-тест (ADR-001 §9). AC / TC-23. | +| R-13 | **Дубль на стадии deploy-staging↔merge-gate** (reconciler триггерит advance, конкурируя с merge-lease). | 🟢 | F-1 продвигает только через `advance_stage`, который штатно прогоняет merge-gate (defer/rollback владеет исходом); active-job guard + `updated_at` — без гонки на тике (ADR-001 §2). | +| R-14 | **Multi-process деплой ломает анти-дубль** (Lock — внутрипроцессный). | 🟢 | Текущая топология single-process (как очередь ORCH-1); ограничение задокументировано, DB UNIQUE-индекс — будущее упрочнение (08-data). | + +## Сводно +Самые острые (🔴) — анти-дубль на создании (R-1), двойной запуск (R-2), спам (R-3), +автопродвижение analysis (R-4), self-hosting (R-9) — закрыты явными механизмами с +покрытием в `04-test-plan.yaml`. Остаточные допущения: single-process топология +(R-14) и осознанная двойная оценка гейта (R-10).