architect(ET): auto-commit from architect run_id=317
All checks were successful
CI / test (push) Successful in 17s
All checks were successful
CI / test (push) Successful in 17s
This commit is contained in:
@@ -16,11 +16,12 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
|
||||
| adr-0008 | Провенанс staging-образа перед BUILD-ONCE retag | accepted | 2026-06-06 | ORCH-058 |
|
||||
| adr-0009 | Толерантность staging-вердикта к инфраструктурным FAIL | accepted | 2026-06-07 | ORCH-061 |
|
||||
| adr-0010 | Post-deploy мониторинг прода + реакция на деградацию | proposed | 2026-06-07 | ORCH-021 |
|
||||
| adr-0011 | Job-reaper + проактивный реклейм merge-lease | accepted | 2026-06-07 | ORCH-065 |
|
||||
|
||||
> ⚠️ Историческая коллизия: номер `0007` занят двумя файлами —
|
||||
> `adr-0007-reconciler.md` (ORCH-053) и `adr-0007-executable-self-deploy.md`
|
||||
> (ORCH-036). Оба accepted; для новых сквозных ADR использовать следующий
|
||||
> свободный номер (текущий максимум — `0010`).
|
||||
> свободный номер (текущий максимум — `0011`).
|
||||
|
||||
## Формат
|
||||
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.
|
||||
|
||||
77
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
77
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# adr-0011: Job-reaper + проактивный реклейм merge-lease
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Статус | accepted |
|
||||
| Дата | 2026-06-07 |
|
||||
| Источник | ORCH-065 (BUG P0, блокер ORCH-54) |
|
||||
| Детально | `docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md` |
|
||||
|
||||
## Контекст
|
||||
|
||||
Единый инстанс с общей БД и очередью (`jobs`, `max_concurrency=1` для
|
||||
self-hosting). Финализация статуса job (`done`/`queued`/`failed`) происходит
|
||||
ТОЛЬКО в `launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`. При
|
||||
`max_concurrency=1` одна такая зомби-строка блокирует claim всех job →
|
||||
**встаёт конвейер всех проектов**. Единственная защита — `requeue_running_jobs()`
|
||||
— работает ТОЛЬКО на старте процесса. Симметрично: merge-lease (ORCH-043,
|
||||
файл `.merge-lease-<repo>.json`) реклеймится лишь лениво по TTL при чужом
|
||||
`acquire`; liveness держателя по pid не проверяется → залипший lease блокирует
|
||||
чужие merge. Это последняя ручная точка автономного self-deploy (блокер ORCH-54);
|
||||
доказанные инциденты 07.06 — jobs 236/239/242/254.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **Job-reaper** — новый daemon-поток `src/job_reaper.py` (каркас `reconciler`:
|
||||
never-raise, `_stop`-Event, старт/стоп в `lifespan`, снимок в `/queue`,
|
||||
kill-switch). Работает **без рестарта** процесса. Liveness — трёхуровневая:
|
||||
Tier-1 мёртвый `jobs.pid` (новая колонка) после `reaper_dead_ticks` подряд
|
||||
тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3
|
||||
backstop по потолку `reaper_max_running_s`. Действие переиспользует контракты:
|
||||
exit0 → gate-driven idempotent advance (`_try_advance_stage`+`_finalize_job`,
|
||||
источник истины — QG, не «exit0»); exit≠0 / неизвестно → `attempts<max` →
|
||||
`queued`, иначе `failed`+Telegram. Атомарный reap-claim (`UPDATE ... WHERE
|
||||
id=? AND status='running'` + `rowcount`, как `claim_next_job`) исключает
|
||||
двойную обработку (совместимость со стартовым `requeue_running_jobs`).
|
||||
2. **Проактивный реклейм stale/dead lease** — функции в `merge_gate.py`
|
||||
(`pid_alive`, `reclaim_stale_lease`), вызываемые на старте (рядом с
|
||||
`requeue_running_jobs`) и периодически из тика reaper. Освобождение, если
|
||||
держатель **мёртв** (pid не жив) ИЛИ **просрочен** (TTL); живой держатель в
|
||||
пределах TTL — НЕ трогать. holder-aware, never-raise, условность как ORCH-43.
|
||||
3. **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не
|
||||
повторяются (`branch_is_behind_main==False`); добавлен детерминированный
|
||||
never-raise guard `pr_already_merged` (читает состояние PR), консультируемый
|
||||
перед повторным merge → уже слит = no-op.
|
||||
4. **Схема БД** — `jobs.pid INTEGER` через идемпотентный `_ensure_column`
|
||||
(паттерн live-safe миграции). Больше ничего не меняется.
|
||||
|
||||
Kill-switch'и (`ORCH_*`): `reaper_enabled`, `reaper_interval_s`,
|
||||
`reaper_dead_ticks`, `reaper_max_running_s`, `lease_reclaim_enabled`;
|
||||
переиспользуются `merge_lock_timeout_s`, `merge_gate_repos`. `false` → строго
|
||||
прежнее поведение.
|
||||
|
||||
## Альтернативы
|
||||
- Reaper внутри reconciler — отвергнуто (смешение stage- и jobs-уровней, общий
|
||||
kill-switch, хуже изоляция).
|
||||
- Только эвристика `agent_runs` без `jobs.pid` — отвергнуто как основной механизм
|
||||
(не ловит зомби, чей monitor умер до записи exit_code); оставлена как Tier-2/3.
|
||||
- БД-lock / внешний брокер очередей — вне объёма (single-node SQLite).
|
||||
- Форс `done` по факту exit0 — отвергнуто; выбран gate-driven advance.
|
||||
|
||||
## Последствия
|
||||
- (+) Зомби-job и залипший lease самовосстанавливаются без рестарта и без
|
||||
оператора; очередь общего инстанса не встаёт; снят технический блокер ORCH-54.
|
||||
- (+) Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); одна колонка через проверенный idempotent-паттерн.
|
||||
- (−) pid-liveness валиден в предположении одного pid-namespace (агент —
|
||||
дочерний процесс оркестратора); закрыто backstop'ом по времени и TTL.
|
||||
- (−) streak-счётчик in-memory (сброс на рестарте; рестарт покрыт
|
||||
`requeue_running_jobs`).
|
||||
|
||||
## Связи
|
||||
- Базируется: adr-0002 (очередь), adr-0006 (merge-gate), adr-0007 (reconciler /
|
||||
self-deploy).
|
||||
- Разблокирует: ORCH-54.
|
||||
Reference in New Issue
Block a user