Merge remote-tracking branch 'origin/main' into feature/ORCH-036-orch-36-deploy-b
All checks were successful
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 14s

# Conflicts:
#	.env.example
#	CHANGELOG.md
#	docs/architecture/README.md
#	docs/operations/INFRA.md
#	src/config.py
This commit is contained in:
stream
2026-06-07 00:22:19 +03:00
31 changed files with 2735 additions and 25 deletions

View File

@@ -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) | accepted | 2026-06-06 | ORCH-053 |
## Формат
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.

View File

@@ -0,0 +1,69 @@
# adr-0007: Reconciler застрявших стадий (sweeper потерянных webhook)
- **Статус:** accepted (реализовано в `src/reconciler.py`)
- **Дата:** 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`).