architect(ET): auto-commit from architect run_id=194
This commit is contained in:
@@ -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.*
|
||||
|
||||
@@ -11,6 +11,7 @@ Per-work-item решения живут в `docs/work-items/<id>/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.
|
||||
|
||||
69
docs/architecture/adr/adr-0007-reconciler.md
Normal file
69
docs/architecture/adr/adr-0007-reconciler.md
Normal file
@@ -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`).
|
||||
221
docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md
Normal file
221
docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md
Normal file
@@ -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: <work_item_id> <stage> разблокирована (потерян 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`.
|
||||
45
docs/work-items/ORCH-053/07-infra-requirements.md
Normal file
45
docs/work-items/ORCH-053/07-infra-requirements.md
Normal file
@@ -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
|
||||
**не требуется**.
|
||||
38
docs/work-items/ORCH-053/08-data-requirements.md
Normal file
38
docs/work-items/ORCH-053/08-data-requirements.md
Normal file
@@ -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`), безопасный на живой прод-БД.
|
||||
27
docs/work-items/ORCH-053/10-tech-risks.md
Normal file
27
docs/work-items/ORCH-053/10-tech-risks.md
Normal file
@@ -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).
|
||||
Reference in New Issue
Block a user