architect(ET): auto-commit from architect run_id=317
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
|
||||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
|
||||
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
|
||||
@@ -190,6 +191,53 @@ never-raise на единицу работы; тишина при синхрон
|
||||
и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее:
|
||||
[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`.
|
||||
|
||||
### Job-reaper + проактивный реклейм merge-lease (ORCH-065 — design)
|
||||
Финализация статуса 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 → встаёт конвейер
|
||||
ВСЕХ проектов (инциденты 07.06: jobs 236/239/242/254). `requeue_running_jobs()`
|
||||
спасал ТОЛЬКО на старте процесса. Симметрично залипал merge-lease (ORCH-043):
|
||||
реклейм был лениво-по-TTL и только при чужом `acquire`, liveness держателя по pid
|
||||
не проверялся. Это последняя ручная точка автономного self-deploy (блокер ORCH-54).
|
||||
ORCH-065 вводит фоновый watchdog, чтобы смерть процесса/потока на любой стадии НЕ
|
||||
оставляла навсегда захваченных ресурсов:
|
||||
- **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`,
|
||||
работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid`
|
||||
(новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность
|
||||
— живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job
|
||||
ещё `running` (monitor умер между записью exit_code и финализацией); Tier-3
|
||||
backstop по потолку `reaper_max_running_s` (> max agent_timeout+grace). Действие
|
||||
переиспользует контракты: 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'`) совместим со стартовым `requeue_running_jobs` (restart-safe,
|
||||
без двойной обработки).
|
||||
- **Проактивный реклейм stale/dead lease** (функции в `merge_gate.py`:
|
||||
`pid_alive`, `reclaim_stale_lease`) — на старте (рядом с `requeue_running_jobs`)
|
||||
и периодически из тика reaper: освобождает lease, чей держатель **мёртв** (pid
|
||||
не жив) ИЛИ **просрочен** (TTL `merge_lock_timeout_s`); живой держатель в
|
||||
пределах TTL — НЕ трогать (защита легитимного merge). holder-aware, never-raise,
|
||||
условность как ORCH-43 (`merge_gate_repos`/self-hosting).
|
||||
- **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не повторяются
|
||||
(`branch_is_behind_main==False`); добавлен never-raise guard `pr_already_merged`
|
||||
(читает состояние PR) — уже слит = no-op.
|
||||
- **Схема БД:** единственное изменение — `jobs.pid INTEGER` через идемпотентный
|
||||
`_ensure_column` (live-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука, файл-схема lease — без изменений.
|
||||
- **Наблюдаемость:** блок `reaper` в `GET /queue` (enabled, interval, last_run_ts,
|
||||
reaped_total, last_reaped, lease_reclaimed_total); каждый reap/lease-reclaim →
|
||||
`logger.warning`; reap→`failed` и lease-reclaim → Telegram.
|
||||
- **Kill-switch'и:** `ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`,
|
||||
`ORCH_REAPER_DEAD_TICKS`, `ORCH_REAPER_MAX_RUNNING_S`,
|
||||
`ORCH_LEASE_RECLAIM_ENABLED`; `false` → строго прежнее поведение.
|
||||
|
||||
Подробнее: [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md), детально —
|
||||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`.
|
||||
|
||||
## Откаты
|
||||
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
|
||||
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
|
||||
@@ -223,7 +271,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
- `events` — входящие вебхуки (дедуп)
|
||||
- `tasks` — задачи и их стадии
|
||||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||||
- `jobs` — очередь задач (ORCH-1)
|
||||
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||||
|
||||
## Изоляция (git worktree, ORCH-2)
|
||||
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
|
||||
@@ -233,7 +281,7 @@ never-raise на единицу работы; тишина при синхрон
|
||||
|--------|------|----------|
|
||||
| GET | `/health` | health check |
|
||||
| GET | `/status` | активные задачи (stage != done) |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + post_deploy (ORCH-021) + последние jobs |
|
||||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + последние jobs |
|
||||
| POST | `/webhook/plane` | Plane webhook |
|
||||
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
|
||||
|
||||
@@ -247,4 +295,4 @@ never-raise на единицу работы; тишина при синхрон
|
||||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||||
|
||||
---
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — design, ветка feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py; флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест).*
|
||||
|
||||
@@ -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.
|
||||
@@ -326,6 +326,7 @@ webhook (plane/gitea) background thread (queue_worker)
|
||||
| `status` | `queued` → `running` → `done` \| `failed` |
|
||||
| `attempts` / `max_attempts` | счётчик попыток (инкремент при claim) / лимит ретраев (default 2) |
|
||||
| `run_id` | FK на `agent_runs.id` после старта |
|
||||
| `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent) |
|
||||
| `task_content` | ТЗ, которое пишется в task-файл агента |
|
||||
| `error` | последняя ошибка |
|
||||
|
||||
@@ -343,6 +344,29 @@ status='queued'` и проверяет `rowcount`. При гонке двух т
|
||||
jobs со статусом `running` (воркер умёр на рестарте) → возвращаются в `queued`.
|
||||
Потом стартует воркер; на shutdown — `worker.stop()` (Event.set + join).
|
||||
|
||||
### Job-reaper (ORCH-065, рестарт НЕ требуется)
|
||||
|
||||
`requeue_running_jobs()` спасает ТОЛЬКО на старте процесса. Зомби-job, возникший
|
||||
**без** рестарта (умер monitor-поток/дочерний процесс, а сервис жив), оставался
|
||||
`running` навсегда и при `max_concurrency=1` блокировал всю очередь. Фоновый
|
||||
daemon-поток `src/job_reaper.py` (каркас `reconciler`) периодически
|
||||
(`reaper_interval_s`) сканирует `running`-jobs и реапит «мёртвые»:
|
||||
- **Tier-1** — `jobs.pid` мёртв (`os.kill(pid,0)`→`ProcessLookupError`) на
|
||||
протяжении `reaper_dead_ticks` подряд тиков (анти-ложноположительность);
|
||||
- **Tier-2** — у `agent_runs[run_id]` записан `exit_code`, а `jobs.status` ещё
|
||||
`running` (monitor умер между записью exit_code и `_finalize_job`);
|
||||
- **Tier-3** — backstop: job висит `running` дольше `reaper_max_running_s`.
|
||||
|
||||
Реап атомарен (`UPDATE jobs SET ... WHERE id=? AND status='running'` + `rowcount`,
|
||||
как `claim_next_job`) → совместим со стартовым `requeue_running_jobs` без двойной
|
||||
обработки. Действие переиспользует контракты: exit0 → gate-driven
|
||||
`_try_advance_stage`+`_finalize_job` (источник истины — QG); exit≠0/неизвестно →
|
||||
`attempts<max`→`queued`, иначе `failed`+Telegram. Тот же поток на старте и
|
||||
периодически делает проактивный реклейм stale/dead merge-lease (`merge_gate.py`:
|
||||
`pid_alive`/`reclaim_stale_lease`). never-raise; kill-switch `ORCH_REAPER_ENABLED`
|
||||
/ `ORCH_LEASE_RECLAIM_ENABLED`; снимок в `GET /queue` (блок `reaper`). Подробнее —
|
||||
adr-0011.
|
||||
|
||||
### Конфиг
|
||||
|
||||
- `ORCH_MAX_CONCURRENCY` (default 1) — лимит параллельных jobs.
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# ADR-001 (ORCH-065): Job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge
|
||||
|
||||
## Статус
|
||||
Accepted — 2026-06-07
|
||||
|
||||
Связь со сквозным ADR: [docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md](../../../architecture/adr/adr-0011-job-reaper-lease-reclaim.md).
|
||||
|
||||
## Контекст
|
||||
|
||||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||||
`max_concurrency=1` для self-hosting). BRD/ТЗ фиксируют три связанных класса
|
||||
отказов «процесс/поток умер, а состояние осталось захваченным навсегда»:
|
||||
|
||||
- **A — zombie jobs.** Статус job (`done`/`queued`/`failed`) выставляется ТОЛЬКО
|
||||
в `launcher._monitor_agent` → `_finalize_job` внутри того же процесса. Смерть
|
||||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||||
self-restart во время deploy) оставляет строку `jobs` навсегда `running`.
|
||||
Единственная защита — `requeue_running_jobs()`, срабатывает ТОЛЬКО на старте
|
||||
процесса. Зомби без рестарта не реанимируется никогда. При `max_concurrency=1`
|
||||
одна зомби-строка блокирует claim всех job (`count_running_jobs() >=
|
||||
max_concurrency`) → встаёт конвейер ВСЕХ проектов. Доказано: jobs 236/239/242/254
|
||||
(07.06).
|
||||
- **B — залипший merge-lease.** Файловый lease `.merge-lease-<repo>.json`
|
||||
(ORCH-043) реклеймится **лениво и только по возрасту** (`age >=
|
||||
merge_lock_timeout_s`) и **только** в момент `acquire_merge_lease` другой
|
||||
задачей. Liveness держателя по pid не проверяется, хотя pid в lease пишется.
|
||||
Смерть с зажатым lease блокирует чужие merge до истечения TTL.
|
||||
- **C — неидемпотентная финализация merge.** Если rebase+re-test зелёные, но
|
||||
процесс умер до фактического merge PR — повторного докатывания нет; дорогая
|
||||
работа (rebase+re-test) сделана, а задача висит.
|
||||
|
||||
Факты кода, на которых строится решение:
|
||||
- `_spawn` (launcher.py:401) создаёт `subprocess.Popen(["bash","-c",cmd])`;
|
||||
`proc.pid` — pid агентского процесса, дочернего к процессу оркестратора в ОДНОМ
|
||||
pid-namespace контейнера. Сейчас `jobs.pid` НЕ хранится.
|
||||
- `_monitor_agent` (launcher.py:541) порядок: `proc.wait()` → запись
|
||||
`agent_runs.finished_at/exit_code` → git commit/push (+PR) → БАГ-8 deployer
|
||||
rollback → usage-комменты → `_try_advance_stage` (exit0, gate-driven advance)
|
||||
→ `_finalize_job` (драйв статуса job по контракту attempts/transient).
|
||||
- `claim_next_job` (db.py:454) — атомарный claim через `UPDATE ... WHERE id=? AND
|
||||
status='queued'` + `rowcount` (образец атомарности).
|
||||
- `reconciler.py` — образец фонового daemon-потока (never-raise, `_stop`-Event,
|
||||
старт/стоп в `main.lifespan`, снимок в `/queue`, kill-switch).
|
||||
- `merge_gate.py`: lease пишет `pid=os.getpid()` (pid процесса оркестратора, НЕ
|
||||
агента), `acquired_at`; `release_merge_lease` уже holder-aware (по `branch`) и
|
||||
идемпотентен; `acquire_merge_lease` идемпотентен для «held by self» (по branch).
|
||||
- `is_self_hosting_repo` / `merge_gate_repos` — образец условности (ORCH-35/43).
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1. Job-reaper — отдельный daemon-поток `src/job_reaper.py`
|
||||
|
||||
Reaper — **новый модуль и отдельный daemon-поток** (НЕ расширение reconciler).
|
||||
Обоснование: reconciler работает на уровне **stage-transition** (источник истины —
|
||||
гейт/Plane); reaper работает на уровне **jobs/agent_runs** (источник истины —
|
||||
liveness процесса). Это разные never-raise-домены и разные kill-switch'и; слияние
|
||||
в один тик смешало бы ответственности. Reaper копирует проверенный каркас
|
||||
`Reconciler`: `threading.Thread(daemon=True)` + `threading.Event`, старт/стоп в
|
||||
`main.lifespan`, снимок в `/queue`, per-job изоляция исключений.
|
||||
|
||||
**Liveness — трёхуровневая (defense in depth):**
|
||||
|
||||
1. **Tier-1 (liveness, основной): мёртвый pid.** Добавляем колонку `jobs.pid`
|
||||
(см. Р-4). В `_spawn` рядом с `run_id`/`started_at` пишем `proc.pid`. Reaper:
|
||||
`pid_alive(pid)` = `os.kill(pid, 0)` с обработкой `ProcessLookupError` (мёртв)
|
||||
/ `PermissionError` (жив, чужой) — единственный сигнал, ловящий «monitor умер
|
||||
ДО записи `finished_at`».
|
||||
2. **Tier-2 (completion race): exit_code записан, job ещё `running`.** Если у
|
||||
`agent_runs[run_id]` есть `finished_at`/`exit_code`, а `jobs.status='running'`
|
||||
— monitor умер между записью exit_code и `_finalize_job`. Здесь исход **известен**.
|
||||
3. **Tier-3 (backstop по потолку):** job висит `running` дольше
|
||||
`reaper_max_running_s` (заведомо > max `agent_timeout`+grace). Реап даже когда
|
||||
liveness определить нельзя (pid переиспользован/неизвестен).
|
||||
|
||||
**Анти-ложноположительность (FR-1.3, AC-3):** по Tier-1 job реапится только после
|
||||
`reaper_dead_ticks` (≥2) ПОДРЯД тиков мёртвого pid — in-memory streak-счётчик по
|
||||
`job_id` (best-effort, сбрасывается на рестарте — но рестарт покрыт стартовым
|
||||
`requeue_running_jobs`). Tier-3 — одношаговый (порог времени, streak не нужен).
|
||||
Живой агент в пределах своего `agent_timeout` НЕ реапится никогда (pid жив + не
|
||||
превышен потолок).
|
||||
|
||||
**Действие при подтверждённой смерти (FR-1.2, AC-4) — переиспользование
|
||||
существующих контрактов, без дублирования:**
|
||||
|
||||
- **Атомарный reap-claim.** Перед любым действием с побочными эффектами reaper
|
||||
атомарно «застолбляет» строку тем же приёмом, что `claim_next_job`: терминальный
|
||||
flip несёт guard `WHERE id=? AND status='running'` и проверяет `rowcount`. При
|
||||
гонке (поздно доехавший monitor, стартовый `requeue_running_jobs`) проигравший
|
||||
видит `rowcount==0` и НЕ обрабатывает строку повторно (AC-5).
|
||||
- **Исход известен (Tier-2, exit_code в `agent_runs`):** маршрутизируем через
|
||||
существующий `launcher._finalize_job(job_id, agent, run_id, exit_code,
|
||||
output_path)`:
|
||||
- `exit==0`: **gate-driven idempotent advance.** Сначала проверяем, не
|
||||
продвинулась ли уже стадия (текущая `tasks.stage` ≠ исходная стадия агента
|
||||
или активного job нет и гейт уже пройден) → если да, просто `mark_job(done)`
|
||||
(идемпотентная уборка, без дубль-перехода). Если нет — `_try_advance_stage`
|
||||
(он сам гоняет канонический QG: артефакт/PR есть → зелёный гейт → advance;
|
||||
нет → красный гейт → НЕ advance), затем `_finalize_job`. **Источник истины —
|
||||
гейт, не «exit0»** — это исключает ложный `done` без реально выполненной
|
||||
работы (если monitor умер ДО git-push, артефакта нет → гейт красный →
|
||||
переходим к ветке «исход неуспешен» ниже).
|
||||
- `exit!=0`: ровно существующий контракт `_finalize_job` (классификация
|
||||
transient/permanent, `attempts<max` → `queued`, иначе `failed`+Telegram).
|
||||
- **Исход неизвестен (Tier-1 мёртвый pid без exit_code, или Tier-3 backstop):**
|
||||
не выдумываем `exit0`. Если `attempts < max_attempts` → `queued` (как
|
||||
`requeue_running_jobs`); если бюджет исчерпан → `failed` + Telegram-алерт.
|
||||
|
||||
**Restart-safe (FR-1.5, AC-5):** reaper стартует в `lifespan` ПОСЛЕ
|
||||
`requeue_running_jobs()`, поэтому при рестарте сначала отрабатывает стартовый
|
||||
requeue, а периодический reaper лишь добивает то, что возникнет позже; атомарный
|
||||
guard `status='running'` исключает двойную обработку.
|
||||
|
||||
### Р-2. Проактивный реклейм stale/dead merge-lease — функции в `merge_gate.py`
|
||||
|
||||
Логика lease живёт в одном месте (`merge_gate.py`). Добавляем:
|
||||
- `pid_alive(pid) -> bool` (never-raise; ошибка/`PermissionError` → считаем
|
||||
«жив», т.е. консервативно НЕ реклеймим — безопаснее не трогать).
|
||||
- `reclaim_stale_lease(repo) -> bool` — для репо из области (см. ниже): прочитать
|
||||
lease; освободить (`release_merge_lease(repo, branch=holder_branch)` —
|
||||
holder-aware), если держатель **мёртв** (`pid` из lease не жив) ИЛИ **просрочен**
|
||||
(`age >= merge_lock_timeout_s`). Живой держатель в пределах TTL — НЕ трогать
|
||||
(AC-8, защита легитимного merge). Каждый реклейм → `logger.warning` +
|
||||
Telegram.
|
||||
|
||||
**Точки вызова (FR-2.1):**
|
||||
- на старте — в `lifespan` рядом с `requeue_running_jobs()`;
|
||||
- периодически — из тика reaper (один общий фоновый поток на оба механизма A и B).
|
||||
|
||||
**Условность (FR-2.4, AC-9):** реально только для `merge_gate_repos`/self-hosting
|
||||
(тот же предикат, что merge-gate); прочие репо — no-op. Kill-switch
|
||||
`lease_reclaim_enabled` (=false → остаётся лишь прежний ленивый TTL-реклейм в
|
||||
`acquire_merge_lease`). Контракт **never-raise**: ошибка реклейма логируется и не
|
||||
валит поток.
|
||||
|
||||
**pid-семантика lease:** lease пишет pid процесса ОРКЕСТРАТОРА (`os.getpid()`).
|
||||
После рестарта контейнера старый pid мёртв → детектируется. Риск pid-reuse
|
||||
(контейнер мог переиспользовать номер pid) закрыт тем, что реклейм срабатывает по
|
||||
**ИЛИ** (pid мёртв **ИЛИ** TTL истёк): даже при ложном «жив» TTL добьёт lease
|
||||
(контракт ORCH-043 сохранён). См. 10-tech-risks.
|
||||
|
||||
### Р-3. Идемпотентная финализация merge (Проблема C) — re-drive + guard, без новой merge-логики
|
||||
|
||||
Per FR-3.3 — НЕ создаём дублирующую merge-логику. Восстановление обеспечивается
|
||||
**штатными путями**:
|
||||
- reaper доводит зомби-job до `queued` → стадия `deploy-staging`/`deploy`
|
||||
переисполняется и снова проходит `check_branch_mergeable` (merge-gate), ЛИБО
|
||||
reconciler доигрывает переход по зелёному гейту;
|
||||
- дорогие шаги не повторяются «вхолостую»: `branch_is_behind_main == False` → этап
|
||||
rebase+re-test пропускается (ветка уже догнана);
|
||||
- lease при повторе берётся заново — `acquire_merge_lease` уже идемпотентен для
|
||||
«held by self» по branch (FR-3.4).
|
||||
|
||||
**Идемпотентность у самого merge (FR-3.2, AC-11):** добавляем детерминированный
|
||||
never-raise guard `pr_already_merged(repo, branch) -> bool` (переиспользует
|
||||
существующий Gitea-клиент; запрос состояния PR). Путь слияния (deployer/merge)
|
||||
консультируется с этим guard ПЕРЕД повторным merge: PR уже слит → no-op (без
|
||||
второго merge и без ошибки). Это единственная новая «merge-related» функция — она
|
||||
не сливает, а лишь читает состояние, поэтому не нарушает «no duplicate merge
|
||||
logic».
|
||||
|
||||
### Р-4. Изменение схемы БД — `jobs.pid INTEGER` (lightweight migration)
|
||||
|
||||
Колонка добавляется идемпотентно через существующий `_ensure_column(conn, "jobs",
|
||||
"pid", "INTEGER")` в `init_db` (паттерн уже применяется к `jobs.transient_attempts`
|
||||
/ `jobs.available_at` — безопасно на live prod DB). `pid` проставляется в `_spawn`
|
||||
рядом с `run_id`/`started_at`. **Альтернатива без миграции отвергнута** (см.
|
||||
Альтернативы): только по `agent_runs.finished_at/exit_code` нельзя поймать
|
||||
зомби, у которого monitor умер ДО записи exit_code — а это и есть основной класс
|
||||
инцидента. `STAGE_TRANSITIONS`, `QG_CHECKS`, схема `agent_runs`, файл-схема lease —
|
||||
без изменений.
|
||||
|
||||
### Р-5. Конфигурация (`src/config.py`, env `ORCH_*`)
|
||||
|
||||
| Настройка | Назначение | Дефолт |
|
||||
|-----------|-----------|--------|
|
||||
| `reaper_enabled` | глобальный kill-switch job-reaper | `True` |
|
||||
| `reaper_interval_s` | период сканирования | `60` |
|
||||
| `reaper_dead_ticks` | подряд тиков мёртвого pid перед реапом (Tier-1) | `2` |
|
||||
| `reaper_max_running_s` | потолок `running` (Tier-3 backstop), > max agent_timeout+grace | `3600` |
|
||||
| `lease_reclaim_enabled` | kill-switch проактивного реклейма lease | `True` |
|
||||
| (reuse) `merge_lock_timeout_s` | TTL lease | `300` |
|
||||
| (reuse) `merge_gate_repos` | область применения lease-reclaim | как есть |
|
||||
|
||||
`false` → строго прежнее поведение (AC-14).
|
||||
|
||||
### Р-6. Наблюдаемость (`GET /queue`)
|
||||
|
||||
Блок `reaper` (образец `reconcile`/`post_deploy`): `enabled`, `interval`,
|
||||
`last_run_ts`, `reaped_total`, `last_reaped` (`{job_id, agent, outcome}`),
|
||||
`lease_reclaimed_total`. Каждый reap и lease-reclaim → `logger.warning` с
|
||||
идентификаторами (`job_id`, `run_id`, `pid`, `repo`, `branch`). reap→`failed` и
|
||||
lease-reclaim → Telegram (AC-16).
|
||||
|
||||
### Р-7. Lifespan (`src/main.py`)
|
||||
|
||||
Старт (после существующего `requeue_running_jobs()`):
|
||||
```
|
||||
init_db() # + _ensure_column(jobs, pid)
|
||||
... orphan-recovery (M-1) ...
|
||||
requeue_running_jobs()
|
||||
+ startup lease-reclaim # reclaim_stale_lease для merge_gate_repos
|
||||
worker.start()
|
||||
reconciler.start()
|
||||
+ reaper.start() # НОВЫЙ daemon-поток (A + периодический B)
|
||||
```
|
||||
Стоп (reverse): `reaper.stop()` → `reconciler.stop()` → `worker.stop()`.
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Reaper как часть reconciler** — отвергнуто: смешивает stage-уровень и
|
||||
jobs-уровень, два разных kill-switch'а в одном тике, хуже изоляция отказов.
|
||||
- **Без `jobs.pid`, только эвристика `agent_runs` + потолок** — отвергнуто как
|
||||
основной механизм: не ловит зомби, чей monitor умер ДО записи `exit_code`
|
||||
(главный класс инцидента). Эвристика оставлена как Tier-2/Tier-3 поверх pid.
|
||||
- **БД-lock вместо файлового lease / внешний брокер очередей** — вне объёма
|
||||
(BRD §4), несоразмерно для single-node SQLite.
|
||||
- **Реаниматор фабрикует `exit0` и форсит `done`** — отвергнуто: ложный `done`
|
||||
без реальной работы (если git-push не случился). Выбран gate-driven advance:
|
||||
источник истины — канонический QG, не предположение об успехе.
|
||||
- **Новый статус `reaping` в enum `jobs.status`** — отвергнуто: меняет контракт
|
||||
статусов; атомарного guard `WHERE status='running'` достаточно.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы:**
|
||||
- Зомби-job самовосстанавливается БЕЗ рестарта процесса → очередь не встаёт
|
||||
(групповой риск снят для всех проектов общего инстанса).
|
||||
- Залипший lease освобождается проактивно (старт + период), не дожидаясь TTL и
|
||||
чужого acquire.
|
||||
- Незавершённый merge докатывается штатным путём, идемпотентно; ручные шаги
|
||||
оператора устранены → снят технический блокер ORCH-54.
|
||||
- Контракты неизменны (`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||||
exit-коды хука); один новый столбец через проверенный idempotent-паттерн.
|
||||
|
||||
**Минусы / ограничения:**
|
||||
- pid-liveness валиден в предположении ОДНОГО pid-namespace (агент — дочерний
|
||||
процесс оркестратора в том же контейнере). Multi-container/namespaced pid →
|
||||
pid-liveness ненадёжен; закрыто backstop'ом по времени и TTL. См. 10-tech-risks.
|
||||
- streak-счётчик in-memory best-effort: после рестарта он сбрасывается, но
|
||||
стартовый `requeue_running_jobs` покрывает рестарт-сценарий.
|
||||
- Tier-3 backstop консервативен (потолок > max timeout); очень долгий легитимный
|
||||
агент (близко к потолку) теоретически может быть реапнут — потолок выбран с
|
||||
большим запасом, чтобы этого не случалось (AC-3).
|
||||
|
||||
## Инварианты (НЕ нарушать)
|
||||
- Reaper/lease-reclaim НЕ рестартят/не роняют прод-контейнер `orchestrator` и НЕ
|
||||
инициируют git-push в `main` (AC-12). Реклейм lease — только удаление
|
||||
файла-lease, без git-операций.
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, сигнатуры/поведение `check_*` (в т.ч.
|
||||
`check_branch_mergeable`), БАГ-8 откат, exit-коды deploy-хука — без изменений;
|
||||
новых QG checks/стадий нет (AC-13).
|
||||
- never-raise на единицу фоновой работы; идемпотентность (атомарный guard +
|
||||
gate-driven advance); restart-safe; тишина при отсутствии аномалий.
|
||||
- Анти-ложноположительность (FR-1.3): живой долгий агент не реапится.
|
||||
|
||||
## Связи
|
||||
- Базируется на: ORCH-1 (очередь, adr-0002), ORCH-043 (merge-gate, adr-0006),
|
||||
ORCH-053 (reconciler-паттерн, adr-0007), ORCH-36 (self-deploy, adr-0007).
|
||||
- Сквозной ADR: adr-0011.
|
||||
- Разблокирует: ORCH-54 (полностью автономный self-deploy).
|
||||
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
42
docs/work-items/ORCH-065/07-infra-requirements.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 07 — Требования к инфраструктуре (ORCH-065)
|
||||
|
||||
## Топология
|
||||
**Без изменений.** Новых контейнеров, портов, сетевых сервисов, внешних
|
||||
зависимостей нет. Job-reaper — ещё один фоновый daemon-поток ВНУТРИ существующего
|
||||
процесса оркестратора (как `queue_worker` и `reconciler`), стартует/останавливается
|
||||
в `main.lifespan`. Деплой/рестарт прод-контейнера в рамках задачи НЕ требуется и
|
||||
ЗАПРЕЩЁН (self-hosting safety) — выкатка через штатный `deploy-staging → deploy`.
|
||||
|
||||
## Допущение pid-namespace (важно для liveness-детекции)
|
||||
- Агент запускается как `subprocess.Popen(["bash","-c",cmd])` — **дочерний
|
||||
процесс оркестратора в ТОМ ЖЕ pid-namespace** (один контейнер). Значит
|
||||
`os.kill(jobs.pid, 0)` корректно отражает liveness агента, пока жив сам
|
||||
оркестратор. Это инвариант текущей упаковки (один контейнер на инстанс).
|
||||
- Lease пишет `pid = os.getpid()` — pid ПРОЦЕССА ОРКЕСТРАТОРА. После рестарта
|
||||
контейнера старый pid мёртв → детектируется. Риск переиспользования номера pid
|
||||
новым процессом закрыт условием «pid мёртв **ИЛИ** TTL истёк»: TTL добивает
|
||||
lease в любом случае (контракт ORCH-043 сохранён).
|
||||
- **Если в будущем агенты переедут в отдельные контейнеры/namespace** — Tier-1
|
||||
pid-liveness станет ненадёжной; тогда полагаемся на Tier-2 (exit_code) и Tier-3
|
||||
(потолок `reaper_max_running_s`). Зафиксировано в 10-tech-risks.
|
||||
|
||||
## Поведение при self-restart (ORCH-36 executable self-deploy)
|
||||
Self-restart прод-контейнера во время `deploy` — ровно тот сценарий, что плодит
|
||||
зомби: monitor-поток умирает вместе со старым контейнером. После рестарта:
|
||||
1. стартовый `requeue_running_jobs()` + стартовый `reclaim_stale_lease` чистят
|
||||
состояние, оставшееся от убитого процесса;
|
||||
2. периодический reaper добивает то, что возникнет позже без рестарта.
|
||||
Reaper/lease-reclaim сами НИКОГДА не рестартят и не роняют прод-контейнер и не
|
||||
делают git-push в `main` (AC-12).
|
||||
|
||||
## Эксплуатационные ручки (env, хост `.env`/`.env.staging`)
|
||||
`ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`, `ORCH_REAPER_DEAD_TICKS`,
|
||||
`ORCH_REAPER_MAX_RUNNING_S`, `ORCH_LEASE_RECLAIM_ENABLED`; переиспользуются
|
||||
`ORCH_MERGE_LOCK_TIMEOUT_S`, `ORCH_MERGE_GATE_REPOS`. Все флаги документируются в
|
||||
`.env.example` (developer-стадия). Полное отключение (`false`) → строго прежнее
|
||||
поведение.
|
||||
|
||||
## Документация эксплуатации
|
||||
`docs/operations/INFRA.md` — добавить (best-effort, developer/PR) короткое
|
||||
упоминание поведения reaper/lease-reclaim при self-restart. Топологическая карта
|
||||
INFRA.md не меняется.
|
||||
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
29
docs/work-items/ORCH-065/08-data-requirements.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 08 — Требования к данным (ORCH-065)
|
||||
|
||||
## Изменение схемы: `jobs.pid`
|
||||
|
||||
| Поле | Значение |
|
||||
|------|----------|
|
||||
| Таблица | `jobs` |
|
||||
| Колонка | `pid` |
|
||||
| Тип | `INTEGER` (nullable, без DEFAULT) |
|
||||
| Назначение | pid агентского процесса (`subprocess.Popen.pid` из `launcher._spawn`) для liveness-детекции зомби job-reaper'ом (Tier-1) |
|
||||
| Механизм миграции | `_ensure_column(conn, "jobs", "pid", "INTEGER")` в `db.init_db` — идемпотентно, no-op если колонка уже есть |
|
||||
| Безопасность на live prod DB | ДА. Тот же паттерn уже применён к `jobs.transient_attempts`, `jobs.available_at`, `events.delivery_id`, `agent_runs.*`. `ALTER TABLE ADD COLUMN` в SQLite — мгновенная метаданная-операция, не блокирует и не переписывает строки |
|
||||
| Заполнение | в `_spawn` рядом с существующим `UPDATE jobs SET run_id=?, started_at=datetime('now') WHERE id=?` добавить `pid=?` (`proc.pid`). Старые строки остаются `pid IS NULL` → для них Tier-1 неприменим, работают Tier-2/Tier-3 |
|
||||
|
||||
## Что НЕ меняется
|
||||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS` — без изменений (это контракты).
|
||||
- Схема `agent_runs` — без изменений (`finished_at`/`exit_code` уже есть — основа Tier-2).
|
||||
- Файл-схема merge-lease `.merge-lease-<repo>.json` — без изменений (`pid`,
|
||||
`acquired_at`, `branch`, `work_item_id`, `task_id` уже пишутся
|
||||
`acquire_merge_lease`).
|
||||
- `jobs.status` enum (`queued|running|done|failed`) — без изменений; новый статус
|
||||
`reaping` НЕ вводится (атомарного guard `WHERE status='running'` достаточно).
|
||||
|
||||
## Совместимость / откат
|
||||
- Откат миграции не требуется: лишняя nullable-колонка безвредна при
|
||||
`reaper_enabled=false`.
|
||||
- `pid IS NULL` (строки до миграции, или если запись pid не успела) → reaper не
|
||||
делает Tier-1, опирается на Tier-2 (exit_code) и Tier-3 (потолок). Поведение
|
||||
деградирует gracefully, ложноположительных реапов не возникает.
|
||||
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
22
docs/work-items/ORCH-065/10-tech-risks.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 10 — Технические риски (ORCH-065)
|
||||
|
||||
| # | Риск | Вероятн. | Влияние | Митигация |
|
||||
|---|------|----------|---------|-----------|
|
||||
| R-1 | **Ложноположительный реап живого долгого агента** (AC-3). Reaper помечает зомби работающий агент → потеря работы, дубль-запуск. | Сред. | Высокое | Tier-1 требует `reaper_dead_ticks`(≥2) подряд тиков мёртвого pid; живой pid = `os.kill(pid,0)` без `ProcessLookupError`. Tier-3 потолок `reaper_max_running_s` выбирается заведомо > max `agent_timeout`+grace. Юнит-тест TC-02/TC-03. |
|
||||
| R-2 | **Ложный `done` без выполненной работы.** Reaper при exit0-зомби помечает job done, хотя git-push/advance не случились (monitor умер до них). | Сред. | Высокое | Реап exit0 НЕ форсит done напрямую — идёт через **gate-driven** `_try_advance_stage`: канонический QG проверяет наличие артефакта/PR; нет артефакта → красный гейт → НЕ advance → ветка «исход неуспешен» (requeue). Источник истины — гейт, не «exit0». |
|
||||
| R-3 | **pid-reuse / namespace.** Номер pid переиспользован новым процессом → ложное «жив» (lease не реклеймится; зомби-job не реапится по Tier-1). | Низк. | Сред. | Lease: условие «pid мёртв **ИЛИ** TTL истёк» — TTL добивает в любом случае. Job-reaper: Tier-3 backstop по времени ловит то, что Tier-1 пропустил. Допущение «один pid-namespace» зафиксировано в 07-infra. |
|
||||
| R-4 | **Гонка reaper vs поздно доехавший monitor / стартовый `requeue_running_jobs`** → двойная обработка строки. | Сред. | Сред. | Атомарный reap-claim `UPDATE ... WHERE id=? AND status='running'` + проверка `rowcount` (образец `claim_next_job`). Reaper стартует ПОСЛЕ `requeue_running_jobs` в lifespan. Юнит-тест TC-06. |
|
||||
| R-5 | **Реклейм живого lease** → параллельный конфликтный merge, риск красного `main` self-hosting. | Низк. | Высокое | `reclaim_stale_lease` освобождает ТОЛЬКО при «держатель мёртв ИЛИ TTL истёк»; живой держатель в пределах TTL не трогается. holder-aware `release_merge_lease(repo, branch)`. Юнит-тест TC-12. |
|
||||
| R-6 | **Реклейм инициирует git-операцию / трогает прод-контейнер** (нарушение self-hosting safety, AC-12). | Низк. | Высокое | Реклейм = только удаление файла-lease (`os.remove`), без git. Reaper не вызывает деплой-хук/рестарт. Явный инвариант в ADR + тест/ревью. |
|
||||
| R-7 | **Идемпотентность merge не достигнута**: повторный проход стадии делает второй merge уже слитого PR. | Сред. | Сред. | never-raise guard `pr_already_merged(repo,branch)` (читает состояние PR) консультируется перед merge → уже слит = no-op. `branch_is_behind_main==False` пропускает rebase+re-test. Юнит-тест TC-16, интеграция TC-17. |
|
||||
| R-8 | **streak-счётчик in-memory теряется при рестарте** → задержка реапа или сброс прогресса. | Низк. | Низкое | Рестарт-сценарий покрыт стартовым `requeue_running_jobs` (мгновенно чистит running). Периодический reaper нужен лишь для зомби БЕЗ рестарта; сброс счётчика лишь переоткладывает реап на `reaper_dead_ticks` тиков. |
|
||||
| R-9 | **never-raise нарушен** — необработанное исключение валит daemon-поток reaper → защита тихо отключается. | Низк. | Сред. | Per-job изоляция `try/except` (образец `reconciler.reconcile_gate_once`) + внешний `try/except` в `_run`. Юнит-тест TC-08/TC-14. |
|
||||
| R-10 | **Регресс существующих тестов** merge_gate/queue/reconciler/deploy. | Низк. | Сред. | Контракты неизменны (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/exit-коды хука); только новая колонка + новый поток + флаги (дефолт сохраняет поведение). Полный прогон `pytest tests/ -q` (regression в 04-test-plan). |
|
||||
|
||||
## Открытые вопросы / follow-up
|
||||
- **Полная автоматизация merge-финализации.** Если деплой-merge (deployer/ORCH-36
|
||||
detached host-process) окажется не полностью идемпотентным к повторному проходу,
|
||||
может понадобиться доп. работа поверх `pr_already_merged`. Здесь закрываем
|
||||
технический блокер; полный авто-approve деплоя — ORCH-54.
|
||||
- Допущение «агенты — дочерние процессы в одном pid-namespace» (R-3) должно быть
|
||||
пересмотрено, если упаковка агентов изменится (отдельные контейнеры).
|
||||
Reference in New Issue
Block a user