Files
orchestrator/docs/architecture/adr/adr-0007-reconciler.md

7.0 KiB
Raw Blame History

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) не меняются.

Уточнения

  • 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).