Merge remote-tracking branch 'origin/main' into feature/ORCH-036-orch-36-deploy-b
# Conflicts: # .env.example # CHANGELOG.md # docs/architecture/README.md # docs/operations/INFRA.md # src/config.py
This commit is contained in:
@@ -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.
|
||||
|
||||
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)
|
||||
|
||||
- **Статус:** 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`).
|
||||
Reference in New Issue
Block a user