7.0 KiB
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, нет нотификаций — спам структурно невозможен).analysisF-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-widethreading.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) не меняются.
Уточнения
-
ORCH-060 (
docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md): F-1 (_reconcile_gate_task) приобретает два пред-гарда ДО оценки гейта — пропускает escalated (developer_retry_count ≥ MAX_DEVELOPER_RETRIES, детерминированно) и Blocked/Needs-Input (Вариант A, Plane API, без миграции) задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise, тишина при пропуске). -
ORCH-068 (
docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md): фикс livelock F-2 (спам_note_unblockпо синхронизированной done-задаче после ORCH-066). F-2 исключает терминалы по группе состояния (completed/cancelled, fallback — ключиdone/cancelled) проектно-независимо;_note_unblock— только при подтверждённом state change (сравнение стадии до/после_dispatch) + in-memory дедуп;_STATES_CACHEполучает TTL (ORCH_PLANE_STATES_TTL_S, дефолт 300с,0=lifetime). Инварианты adr-0007 сохранены (источник истины — Plane; реестры/схема/handle_*/F-1/F-3 не меняются; never-raise; kill-switch'и).
Связи
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).