144 lines
16 KiB
Markdown
144 lines
16 KiB
Markdown
---
|
||
work_item: ORCH-114
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-15
|
||
model_used: claude-opus-4-8
|
||
escalate: full-cycle
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте
|
||
|
||
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **конкретные требования к реализации**, выведенные из BRD (`01-brd.md`) и фактического
|
||
> кода. **Выбор механизма** (durable lease / heartbeat / transition-epoch / форма хранения, набор
|
||
> покрываемых рёбер) и архитектурное обоснование — задача архитектора (`06-adr/`). Здесь — *что*
|
||
> должно быть истинно и *какие модули* затрагиваются, не *как* именно.
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Вводится **единый инвариант владения** side-effectful переходом стадии: запись стадии и исполнение
|
||
тяжёлых под-гейтов/финализации защищаются **durable-механизмом владения** (lease/epoch) + **CAS** на
|
||
запись стадии, так что в любой момент переход конкретной задачи доводит **ровно один** актор, а
|
||
конкурентный/после-рестартовый повторный вход (reaper / reconciler / webhook / startup-requeue)
|
||
**откладывается или становится no-op** вместо повторного применения необратимых эффектов
|
||
(merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя / противоречивый rollback↔done).
|
||
Аддитивно, под kill-switch, never-raise; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / вердикт-ключи
|
||
/ схемы существующих таблиц — не трогаются (обобщает и делает durable процесс-локальный
|
||
`finalizer_liveness` ORCH-113).
|
||
|
||
## 2. Задействованные модули / пути
|
||
|
||
| Путь | Действие | Назначение в ORCH-114 |
|
||
|------|----------|------------------------|
|
||
| `src/stage_engine.py` | изменить | `advance_stage`: захват владения на границе side-effectful перехода/финализации; CAS на запись стадии; release в `try/finally`; проигравший — чистый аборт. Покрыть `_handle_merge_verify`, под-гейты `deploy-staging→deploy`, `run_deploy_finalizer` (Phase C), `advance_if_gate_passed` (F-1). |
|
||
| `src/db.py` | изменить | CAS-вариант записи стадии (запись только при совпадении ожидаемой текущей стадии/эпохи; rowcount-результат). Durable-хелперы владения (acquire / heartbeat-touch / release / reclaim / snapshot) — форму хранилища задаёт архитектор. |
|
||
| `src/finalizer_liveness.py` | изменить/обобщить | Отправная точка: обобщить process-local реестр до **durable, кросс-путевого** владения (или надстроить durable-слой поверх). Сохранить контракт never-raise + kill-switch. |
|
||
| `src/job_reaper.py` | изменить | Tier-2/Tier-3 осведомлены о durable-владении на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом, реклейм мёртвого/устаревшего в ограниченное время (NFR-6). |
|
||
| `src/queue_worker.py` | изменить | `requeue_running_jobs` / стартовое восстановление сверяется с durable-владением: не пере-исполнять уже применённый необратимый шаг (умное восстановление). `claim_next_job` — не вносить сетевых зависимостей. |
|
||
| `src/reconciler.py` | изменить | F-1 (`advance_if_gate_passed`) — defer при активном lease перехода (по образцу skip-guard'ов escalated/Blocked/deps). |
|
||
| `src/webhooks/plane.py` | изменить | Пути продвижения (`_try_advance_stage`, Approved / Confirm Deploy) — defer при активном lease. |
|
||
| `src/webhooks/gitea.py` | изменить (учесть) | Прямые записи стадии в обход `advance_stage` (`handle_push`/`handle_ci_status`/`handle_pr`) должны попадать под тот же CAS-инвариант либо явно исключаться архитектором (граница в ADR). |
|
||
| `src/main.py` | изменить | Порядок старта демонов / точка восстановления; read-only блок в `GET /queue`; опц. operator-эндпоинт реклейма. |
|
||
| `src/config.py` | изменить | Kill-switch(и) + бюджеты/таймауты владения + (опц.) CSV-скоуп репо. |
|
||
| `tests/test_orch114_transition_ownership.py` | создать | Покрытие FR-1…FR-7 (см. `04-test-plan.yaml`). |
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-1 — Единое владение side-effectful переходом (BR-1, BR-6)
|
||
На границе, где начинается side-effectful финализация/переход (минимум: под-гейты
|
||
`deploy-staging→deploy`, `_handle_merge_verify` на `deploy→done`, Phase C `run_deploy_finalizer`),
|
||
актор **захватывает владение** задачей. Пока владение активно, другой актор не исполняет тот же
|
||
переход. Release — детерминированно в `try/finally` (в т.ч. на исключении/откате). Владение
|
||
**durable** (NFR-4): переживает рестарт и доступно для проверки другому актору/новому процессу.
|
||
|
||
### FR-2 — Compare-and-swap / epoch на запись стадии (BR-2)
|
||
Запись стадии для side-effectful переходов выполняется только если предусловие (ожидаемая текущая
|
||
стадия и/или эпоха перехода) не изменилось с момента чтения. Реализуется через CAS-вариант
|
||
`update_task_stage` (`UPDATE … SET stage=?[, epoch=epoch+1] WHERE id=? AND stage=?[ AND epoch=?]`,
|
||
решение по форме — архитектор). Проигравший гонку писатель получает «lost-race» результат, **не**
|
||
мутирует стадию и **не** выполняет ни одного побочного эффекта (merge_pr / ratchet / rebuild /
|
||
deploy-init / enqueue следующего агента). Инвариант распространяется и на пути, пишущие стадию в
|
||
обход `advance_stage` (gitea-webhook), — либо CAS, либо явное исключение (граница в ADR).
|
||
|
||
### FR-3 — Reaper, осведомлённый о владении на всех путях (BR-3, NFR-6)
|
||
Job-reaper перед реклеймом сверяется с durable-владением **не только** в Tier-2 для `deploy-staging`
|
||
(текущая область ORCH-113), а на всех релевантных тирах/рёбрах: **живой** владелец → **defer**
|
||
(лог + счётчик, без повторного advance); **мёртвый/устаревший** владелец → реклейм в ограниченное
|
||
время (Tier-3 backstop `reaper_max_running_s` добивает зависшего; маркер владения backstop не
|
||
обходит инвариант бюджета). Сохранить атомарный `reap_running_job` rowcount-guard.
|
||
|
||
### FR-4 — Умное восстановление при старте (BR-4, BR-6, NFR-7)
|
||
Стартовое восстановление (`requeue_running_jobs` + последующий цикл) использует durable-владение/эпоху,
|
||
чтобы **детерминированно** различить: (a) финализация не начиналась / безопасно перезапустить →
|
||
re-drive; (b) необратимый шаг уже применён (мерж в `main` / ratchet / прод-деплой инициирован) →
|
||
**сойтись к done/консистентному исходу без повторного применения**. Источник истины для «уже
|
||
применено» — авторитетные durable-факты (SHA-in-main ORCH-071/073, маркер `INITIATED` self-deploy,
|
||
durable-lease/эпоха), а не in-memory состояние.
|
||
|
||
### FR-5 — Skip/defer в reconciler и webhook (BR-5)
|
||
Reconciler F-1 (`advance_if_gate_passed`) и webhook-пути продвижения (`plane._try_advance_stage`,
|
||
Approved/Confirm Deploy) при **активном** lease перехода для задачи **откладывают** действие
|
||
(silent skip + наблюдаемость), по образцу существующих skip-guard'ов F-1 (escalated / Blocked /
|
||
task-deps). Fail-safe: неопределённость состояния lease → консервативный skip (не дублировать).
|
||
|
||
### FR-6 — Наблюдаемость (BR-7)
|
||
Аддитивный read-only блок в `GET /queue` (по образцу `serial_gate`/`merge_gate`/`reaper`):
|
||
держатели lease, возраст владения, defer-счётчики, форсированные/устаревшие реклеймы. Алерт
|
||
(`send_telegram`, кликабельный номер) на форсированный/устаревший реклейм. Опц. запись в
|
||
lessons-journal (ORCH-098, `source="auto"`). Опц. operator-эндпоинт ручного реклейма (по образцу
|
||
`POST /serial-gate/unfreeze`).
|
||
|
||
### FR-7 — Конфигурация, обратимость, never-raise (BR-9, NFR-1, NFR-2, NFR-8)
|
||
Все публичные функции владения — never-raise (ошибка → безопасный дефолт + WARNING). Kill-switch
|
||
возвращает поведение **байт-в-байт** к до-ORCH-114 (lease не пишется/не читается, CAS вырождается в
|
||
прежний безусловный `update_task_stage`). Hot-path — fail-open; prod-safety — fail-closed.
|
||
|
||
## 4. Изменения API
|
||
- **`GET /queue`** — аддитивный read-only блок владения переходом (имя ключа уточнит архитектор,
|
||
напр. `transition_ownership` / `transition_lease`). Существующие ключи `/queue` — байт-в-байт.
|
||
- **(Опционально, по решению архитектора)** `POST /transition-lease/release?work_item=<id>` —
|
||
операторский ручной реклейм застрявшего владения (паттерн `POST /serial-gate/unfreeze`).
|
||
- `GET /metrics` (ORCH-099) — при необходимости аддитивное поле без бампа `schema_version` (sidecar
|
||
обязан толерировать незнакомые ключи). Прочие эндпоинты — не трогаются.
|
||
|
||
## 5. Изменения схемы БД
|
||
**Требование (форму выбирает архитектор, `06-adr/` + `08-data-requirements.md`):** для durable-владения
|
||
(NFR-4) и CAS/epoch (FR-2) требуется **аддитивное, идемпотентное** durable-состояние. Кандидаты
|
||
(не предписание):
|
||
- доп. аддитивная таблица владения (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/
|
||
`coverage_baseline`/`lessons`) с `(task_id/job_id, owner, run_id, stage, acquired_at, heartbeat_at,
|
||
expires_at)`; **либо**
|
||
- аддитивные колонки на `tasks`/`jobs` (`_ensure_column`, паттерн `tasks.track`/`tasks.cancelled_at`),
|
||
включая возможную `epoch/version`-колонку для CAS.
|
||
|
||
**Жёсткие ограничения (NFR-3):** только аддитивно/идемпотентно; **схемы существующих таблиц
|
||
(`tasks`/`jobs`/`agent_runs` и пр.) — байт-в-байт**; никаких изменений существующих столбцов/индексов,
|
||
ломающих обратную совместимость; restart-safe инициализация в `init_db()`.
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
**Нет.** `QG_CHECKS` / `check_*` / `_parse_*` / машинные вердикт-ключи — **не трогаются**. Владение
|
||
переходом — свойство **движка переходов и фоновых акторов**, а **не** Quality Gate и **не** стадия
|
||
(аналогично тому, как merge-lease/serial-gate/finalizer-liveness — врезки/leaf'ы, а не QG). Никаких
|
||
новых стадий/рёбер в `STAGE_TRANSITIONS`.
|
||
|
||
## 7. Совместимость / регресс
|
||
- **Kill-switch** (новый флаг в `config.py`, env `ORCH_*`): `False` → CAS вырождается в прежний
|
||
безусловный `update_task_stage`, lease не пишется/не читается, reaper/reconciler/webhook ведут себя
|
||
как до ORCH-114 — **байт-в-байт** (включая зелёный существующий `pytest tests/`).
|
||
- **Область раската:** по образцу leaf-гейтов; durable-lease минимально применяется к self-hosting
|
||
рёбрам (`deploy-staging`/`deploy→done`, где живут необратимые эффекты); generic-CAS инертен при
|
||
отсутствии гонки (нулевая стоимость на не-затронутых переходах). Точную область фиксирует архитектор.
|
||
- **Обратимость:** механизм аддитивен и изолирован; откат = выключить kill-switch (durable-таблица/
|
||
колонки остаются инертными).
|
||
- **never-raise / fail-open / fail-closed:** hot-path claim/guard — fail-open (не клинить общую
|
||
очередь, AC-8 ORCH-088); prod-safety/необратимость — fail-closed; любой сбой механизма — WARNING +
|
||
безопасный дефолт.
|
||
- **Сквозной бюджет:** lease согласован с `reaper_max_running_s`/`reaper_finalize_grace_s` (ORCH-065/
|
||
109/110/113) — не удлиняет финализацию за backstop; устаревший владелец добивается в ограниченное время.
|
||
- **Маркеры трассировки (ORCH-078):** правки в блоках с маркерами ORCH-065/109/110/111/113 (reaper,
|
||
finalizer-liveness, merge-gate) сверяются с их ADR перед изменением; новый код помечается `ORCH-114`.
|
||
- **enduro-trails:** при флаге off / репо вне области — нулевая регрессия.
|