104 lines
8.3 KiB
Markdown
104 lines
8.3 KiB
Markdown
# BRD — ORCH-065: zombie jobs + залипший merge-lease
|
||
|
||
Work Item ID: ORCH-065
|
||
Тип: BUG (P0)
|
||
Репозиторий: orchestrator (self-hosting)
|
||
Эпик: блокер ORCH-54 (полностью автономный self-deploy)
|
||
|
||
## 1. Контекст и проблема
|
||
|
||
Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
|
||
`max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная
|
||
автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся
|
||
к «процесс умер/завершился, а состояние осталось захваченным навсегда»:
|
||
|
||
### Проблема A — zombie jobs (строка `jobs` навсегда `running`)
|
||
Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM,
|
||
рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице
|
||
`jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только**
|
||
в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс
|
||
не доживает до финализации — job «зомбирован».
|
||
|
||
- Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`,
|
||
срабатывающая **исключительно на старте процесса**. Зомби, возникший **без**
|
||
рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется
|
||
никогда.
|
||
- При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех
|
||
последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит)
|
||
→ **встаёт конвейер всех проектов**.
|
||
|
||
### Проблема B — залипший merge-lease
|
||
Merge-gate (ORCH-043) берёт файловый lease `<repos_dir>/.merge-lease-<repo>.json`
|
||
ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс
|
||
умирает на финальном merge **с зажатым lease**:
|
||
|
||
- Реклейм lease реализован **лениво и только по возрасту** (`age >=
|
||
merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**.
|
||
Проактивного освобождения (на старте / периодически) нет; **liveness держателя по
|
||
pid не проверяется** (хотя `pid` в lease пишется).
|
||
- Пострадавшая задача сама re-drive не получает: merge не финализируется → задача
|
||
висит, lease мешает чужим merge до истечения TTL.
|
||
|
||
### Проблема C — неидемпотентная финализация merge
|
||
Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до
|
||
завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в
|
||
полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана.
|
||
|
||
## 2. Бизнес-последствия
|
||
- **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting
|
||
задача не доезжает до прода без оператора (cancel zombie + ручной merge PR +
|
||
ручной `--deploy`).
|
||
- Прямой блокер эпика ORCH-54.
|
||
- Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного
|
||
deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби,
|
||
прод-merge/deploy доводились вручную.
|
||
- Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер
|
||
enduro-trails и всех прочих проектов.
|
||
|
||
## 3. Цель
|
||
Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart
|
||
во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в
|
||
`running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при
|
||
этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать
|
||
`main`, fail-closed на реальных ошибках).
|
||
|
||
## 4. Объём (Scope)
|
||
|
||
### В объёме
|
||
1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`),
|
||
детектирующий «мёртвый» `running`-job и приводящий его строку в корректный
|
||
терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно,
|
||
без LLM. Restart-safe и работающий **без** рестарта процесса.
|
||
2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель
|
||
мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/
|
||
reconciler), а не только лениво при чужом `acquire`.
|
||
3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не
|
||
состоялся, операция повторяется/докатывается без потери уже сделанной работы.
|
||
|
||
### Вне объёма
|
||
- Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock.
|
||
- Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие
|
||
технического блокера.
|
||
- Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов.
|
||
|
||
## 5. Заинтересованные стороны
|
||
- Owner оркестратора (self-hosting автономность).
|
||
- Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки
|
||
общей очереди.
|
||
|
||
## 6. Допущения и ограничения
|
||
- `max_concurrency=1` для self-hosting сохраняется.
|
||
- Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках
|
||
задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать
|
||
легитимно работающий merge.
|
||
- Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби
|
||
(нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы).
|
||
- Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate).
|
||
- Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`).
|
||
|
||
## 7. Критерий успеха (бизнес-уровень)
|
||
После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/
|
||
self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно
|
||
доезжает до `done` сама, либо откатывается по штатному контракту — **без участия
|
||
оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный.
|