67 lines
5.1 KiB
Markdown
67 lines
5.1 KiB
Markdown
---
|
||
work_item: ORCH-114
|
||
stage: architecture
|
||
author_agent: architect
|
||
status: proposed
|
||
created_at: 2026-06-15
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 08 — Требования к данным: ORCH-114 — Durable transition-ownership lease + expected-stage CAS
|
||
|
||
Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: architecture
|
||
|
||
> When-applicable / информационный (гейтом не парсится). Сверено по `src/db.py`.
|
||
|
||
## Изменения схемы БД
|
||
|
||
**Ровно один новый объект — аддитивная таблица `transition_lease`** (`CREATE TABLE IF NOT EXISTS` в
|
||
`init_db()`, паттерн `repo_freeze`/`coverage_baseline`/`lessons`):
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS transition_lease (
|
||
task_id INTEGER PRIMARY KEY, -- одна задача = ≤1 активный владелец side-effectful перехода
|
||
owner TEXT NOT NULL, -- актор-держатель: monitor|reaper|reconciler|webhook|finalizer
|
||
owner_pid INTEGER, -- pid процесса-держателя (liveness, как merge-lease os.getpid())
|
||
owner_boot_id TEXT, -- нонс старта процесса; рестарт ⇒ смена ⇒ прежний lease мёртв
|
||
run_id INTEGER, -- agent_runs.id если применимо (контекст)
|
||
stage TEXT, -- from-стадия захвата (наблюдаемость/контекст)
|
||
acquired_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
```
|
||
|
||
Индекс не требуется (доступ по PK `task_id`); `snapshot()` для `GET /queue` — full-scan по малой таблице
|
||
(в любой момент строк ≈ числу активных side-effectful переходов, единицы).
|
||
|
||
**Изменений существующих таблиц НЕТ.** `tasks` / `jobs` / `agent_runs` / `events` / `job_deps` /
|
||
`repo_freeze` / `coverage_baseline` / `lessons` — схемы **байт-в-байт** (NFR-3, AC-11). **Колонка
|
||
`epoch/version` НЕ добавляется** (ADR-001 D2: для одно-процессной модели стадия *и есть* версия CAS;
|
||
epoch — форвард-расширение, не вводится сейчас).
|
||
|
||
## Новые/изменённые сущности
|
||
|
||
- **Таблица `transition_lease`** — durable-владение side-effectful переходом задачи. Инвариант:
|
||
активная строка для `task_id` ⇔ некий актор держит владение переходом этой задачи. **Живой** владелец ⇔
|
||
`owner_boot_id == <boot-id текущего процесса>` И `merge_gate.pid_alive(owner_pid)`; иначе **устарел** →
|
||
реклеймится. Захват — атомарный rowcount-guard (паттерн `claim_next_job`/`reap_running_job`): `INSERT … ON
|
||
CONFLICT(task_id)` берётся только при отсутствии живого владельца (иначе rowcount==0 → busy).
|
||
- **Функция `update_task_stage_cas(task_id, expected_stage, new_stage) -> bool`** (новая, в `db.py`):
|
||
`UPDATE tasks SET stage=?, updated_at=datetime('now') WHERE id=? AND stage=?`; возвращает `cur.rowcount==1`
|
||
(выиграл CAS) / `False` (проиграл — стадия уже не та, что читали → аборт без побочных эффектов).
|
||
Прежний `update_task_stage` **сохраняется без изменений** (путь kill-switch-off и записи вне
|
||
side-effectful области).
|
||
|
||
## Совместимость данных / миграции
|
||
|
||
- **Аддитивно/идемпотентно/restart-safe:** `CREATE TABLE IF NOT EXISTS` в `init_db()` — повторный старт
|
||
no-op; на живой общей прод-БД данные enduro-trails не затрагиваются (новая таблица изолирована).
|
||
- **Никакого backfill** существующих строк не требуется (таблица заполняется рантаймом при захвате владения).
|
||
- **Рестарт-семантика:** durable-строки lease переживают рестарт физически; новый процесс получает новый
|
||
`owner_boot_id` → ранее записанные строки трактуются как устаревшие и реклеймятся (ADR-001 D3/D7);
|
||
`recover_on_startup()` зачищает их наблюдаемо (после `requeue_running_jobs`).
|
||
- **Откат (NFR-8):** при `transition_lease_enabled=False` таблица не читается/не пишется и остаётся
|
||
инертной; удалять её при откате не требуется. Поведение БД-слоя — байт-в-байт до-ORCH-114.
|
||
- **enduro-trails:** при `transition_lease_repos=""` (self-hosting only) механизм для enduro не активируется
|
||
— нулевая регрессия.
|
||
</content>
|