---
work_item: ORCH-113
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-15
model_used: claude-opus-4-8
---

# adr-0043: Reaper Tier-2 — in-memory ownership-маркер финализации `deploy-staging` (живой finalizer не реапится)

- **Статус:** proposed
- **Дата:** 2026-06-15
- **Задача:** ORCH-113 (bug → escalate full-cycle; кластер инцидента ORCH-111)
- **Детальный ADR:** `docs/work-items/ORCH-113/06-adr/ADR-001-reaper-finalizer-liveness-ownership.md`
- **Уточняет:** `adr-0011` (job-reaper/lease-reclaim ORCH-065), `adr-0040` (timeout-бюджеты ORCH-109),
  `adr-0042` (merge-gate re-test infra-tolerance + tree-kill ORCH-110), `adr-0041`
  (ORCH-111 `proc_blocking` — комплементарный наблюдатель того же инцидента)

## Контекст

На ребре `deploy-staging → deploy` живой монитор (`launcher._monitor_agent`) штампит
`agent_runs.finished_at`/`exit_code` **первым**, затем синхронно, в своём потоке, прогоняет тяжёлый
набор edge-под-гейтов через `_try_advance_stage → advance_stage` (`stage_engine.py:327–368`):
`security` → `merge-gate` (полный локальный re-test, `merge_retest_timeout_s=900`) → `coverage`
(`pytest --cov`) → `image-freshness` (docker-rebuild + пересоздание staging) — **минуты**, — и лишь
потом `_finalize_job`. Reaper Tier-2 (`job_reaper.py:197–209`) меряет `finished_age_s` от
`finished_at` = **начала** финализации и по `reaper_finalize_grace_s=300` считает живого, долго
финализирующего монитора мёртвым → независимо повторяет тот же тяжёлый advance. Атомарный
claim-before-act защищает лишь **флип строки** job, но не **side-effectful исполнение edge-гейтов**
(монитор не claim'ит строку перед `advance_stage`) → две `advance_stage` параллельно.

Инцидент ORCH-111 (job 1914): повторный re-test красный, ложный откат `deploy-staging → development`
(+ ложный developer-retry), **параллельно** исходный finalizer довёл deploy до SUCCESS и смержил
PR #130 — состояние раздвоилось. Реального сигнала «жив ли finalizer» нет (pid агента в Tier-2 мёртв в
обоих случаях). Per-stage grace, покрывающая Σ финализации (≈4160с), невозможна без нарушения сквозного
бюджета ORCH-065/109/110 `reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace (≈4460)`.

**Решающий факт (проверен):** монитор и reaper — daemon-**потоки одного** uvicorn-процесса (CMD без
`--workers`), общая SQLite-БД → живость finalizer'а определяется **in-memory**. Рестарт покрыт
существующим `requeue_running_jobs()` (running→queued), вызываемым в `main.lifespan` **до** старта reaper.

## Решение

1. **Leaf `src/finalizer_liveness.py`** — чистый процесс-локальный реестр владения финализацией
   (паттерн `serial_gate`/`coverage_gate`: never-raise, без сети/БД): `mark(job_id, run_id, stage)` /
   `clear(job_id)` / `is_active(job_id) -> bool` / `snapshot()`; `{job_id: {...}}` + `threading.Lock`;
   собственного TTL нет (ограничение по времени даёт Tier-3).
2. **Эмиссия владения** — `launcher._monitor_agent`: `mark(...)` сразу после штампа `exit_code`
   (самый ранний момент Tier-2), `clear(...)` в `try/finally` вокруг хвоста финализации → исключение
   в потоке монитора гарантированно снимает владение (reaper добивает). Гибель процесса → рестарт →
   `requeue_running_jobs` → реестр пуст (restart-safe без durable-хранения).
3. **Консультация reaper** — `_reap_job` Tier-2 (`exit_code` записан, `finished_age >= grace`): если
   `reaper_finalizer_liveness_enabled` **И** стадия `== "deploy-staging"` **И** `is_active(job_id)` →
   **defer** (лог + счётчик), не реапить через Tier-2, провалиться к Tier-3. Иначе — прежний путь.
   **Tier-3 (`age >= reaper_max_running_s`) маркер игнорирует** — добивает всегда в ограниченное время.
4. **Скоуп/флаг** — только глобальный kill-switch `reaper_finalizer_liveness_enabled`
   (env `ORCH_REAPER_FINALIZER_LIVENESS_ENABLED`, дефолт `True`); **без** per-repo разреза (баг общий
   для всех репо со стадией `deploy-staging`; per-repo оставил бы баг активным для части репо).
   `False` → reaper байт-в-байт прежний; стадии `!= deploy-staging` не консультируются.
5. **Наблюдаемость** — счётчик `finalizer_defers_total` + размер `snapshot()` в блоке `reaper`
   `GET /queue`; существующие ключи ответа не меняются; новых эндпоинтов нет.

**Инварианты:** `STAGE_TRANSITIONS` / `QG_CHECKS` / каждый `check_*` / machine-verdict ключи / схема
существующих таблиц — **байт-в-байт**; **нулевое** изменение схемы БД; reaper остаётся never-raise
наблюдателем; `reaper_finalize_grace_s` и `reaper_max_running_s` **не меняются** (сквозной бюджет цел);
фикс не рестартит прод и не пушит `main`.

## Альтернативы
- Per-stage grace, покрывающая Σ — отвергнуто (нарушает бюджет `5400 > Σ+grace`; таймер = источник бага).
- Durable-колонка (heartbeat/owner-токен) — отвергнуто (один процесс → in-memory авторитетно; рестарт
  покрыт requeue; блокирующий re-test не может бить heartbeat).
- Sub-state `finalizing` в `jobs.status` — отвергнуто (меняет семантику статуса для
  claim/requeue/reconciler/reaper — нарушение NFR-2).
- Lease-файл на `(job, stage)` — отвергнуто (тяжелее, дублирует merge-lease, TTL = таймер-проблема).
- Флип job из `running` до тяжёлых гейтов — отвергнуто (ломает `get_running_jobs`/метрики и
  restart-requeue).

## Последствия
- (+) Устранены повторный прогон edge-гейтов, ложный откат и расхождение состояния при живом долгом
  finalizer'е `deploy-staging`; идемпотентность исполнения edge-гейтов через владение.
- (+) Реально мёртвый/застрявший finalizer добивается (finally-clear → Tier-2; иначе Tier-3); функция
  reaper ORCH-065 сохранена.
- (+) Нулевое изменение схемы и контрактов; сквозной бюджет ORCH-065/109/110 не тронут; откат — один
  env-флаг.
- (−) Гарантия владения валидна при **одном процессе/одной БД** (проверено: один uvicorn-воркер); ввод
  `--workers>1` потребует durable-сигнала (риск в work-item 10-tech-risks).
- (−) Окно «штамп `finished_at` → `mark()`» (git push) маркером не покрыто — закрыто прежним grace=300.

## Связи
- Базируется/уточняет: `adr-0011`, `adr-0040`, `adr-0042`, `adr-0041`.
- Союзные задачи кластера инцидента ORCH-111: `ORCH-110` (инфра-толерантность merge-gate — отдельный
  объём, не дублировать), `ORCH-109` (бюджеты).
- Детально: `docs/work-items/ORCH-113/06-adr/ADR-001-reaper-finalizer-liveness-ownership.md`.
</content>
