Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели (разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий. - Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице «источник × цель» → состояния target_missing/orphaned/deleted, переходы и самовосстановление (healing). - worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс пропажи источника (порог [worker].source_missing_threshold) и синхронный preflight перед действиями (relink/recognize/apply/undo) — не доверяем state в БД. - layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника), отказ всего батча без частичного отката (ErrLastCopy). - store: единый список terminalStates для IsTerminal и FindActiveByInfohash (иначе семантика «активности» разъезжается), столбец source_miss_count, миграция 0003. - httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне. - Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config. Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
> в том же change. Расхождение схемы с миграциями считаем багом
|
||||
> документации.
|
||||
>
|
||||
> Состояние на: миграции `0001_init`, `0002_recognition_plan`.
|
||||
> Состояние на: миграции `0001_init`, `0002_recognition_plan`,
|
||||
> `0003_source_miss_count`.
|
||||
|
||||
Назначение таблиц и почему так — [architecture.md](architecture.md) →
|
||||
«Хранилище». Значения `state` и переходы — [workflow.md](workflow.md).
|
||||
@@ -34,6 +35,7 @@ erDiagram
|
||||
TEXT state "NOT NULL; см. workflow.md"
|
||||
TEXT error_code "nullable"
|
||||
TEXT error_msg "nullable"
|
||||
INTEGER source_miss_count "NOT NULL DEFAULT 0; дебаунс пропажи источника (миграция 0003)"
|
||||
TEXT created_at "NOT NULL DEFAULT datetime('now')"
|
||||
TEXT updated_at "NOT NULL DEFAULT datetime('now')"
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ inode общий — диск не дублируется.
|
||||
поддержки жёстких ссылок), `layout` не падает, а копирует файл с
|
||||
предупреждением в лог — см. architecture.md → «Раскладка файлов».
|
||||
|
||||
## Безопасный undo (не снимать последнюю копию)
|
||||
|
||||
`Undo` снимает **лишний** хардлинк, а не единственный файл. Перед удалением
|
||||
батча `layout` проверяет каждую цель: если исходный файл уже не существует
|
||||
**или** у цели не осталось других жёстких ссылок (`nlink <= 1`), это —
|
||||
последняя копия данных, и весь `Undo` отклоняется целиком (ошибка
|
||||
`ErrLastCopy`), не сняв ни одной ссылки (частичный откат тоже стёр бы часть
|
||||
данных). Так нарушенный инвариант «источник неприкосновенен» (источник
|
||||
удалён вручную) не приводит к потере данных. Отсутствующую цель `Undo`
|
||||
пропускает как уже снятую (идемпотентность). Связь с состояниями
|
||||
рассинхрона — [workflow.md](workflow.md) → «Сверка с реальностью».
|
||||
|
||||
## Крайние случаи
|
||||
|
||||
- **Многофайловый фильм** (части) — стэкинг по точному токену Jellyfin
|
||||
|
||||
@@ -39,9 +39,19 @@ stateDiagram-v2
|
||||
stuck --> downloading: Retry
|
||||
failed --> downloading: Retry
|
||||
|
||||
done --> target_missing: сверка — цель удалена
|
||||
done --> orphaned: сверка — источник пропал
|
||||
target_missing --> recognizing: Привязать заново
|
||||
target_missing --> orphaned: источник тоже пропал
|
||||
target_missing --> deleted: источник тоже пропал
|
||||
orphaned --> deleted: цель тоже удалена
|
||||
target_missing --> done: healing (цель вернулась)
|
||||
orphaned --> done: healing (источник вернулся)
|
||||
|
||||
done --> [*]
|
||||
cancelled --> [*]
|
||||
reverted --> [*]
|
||||
deleted --> [*]
|
||||
|
||||
note right of cancelled
|
||||
«Отклонить» доступно из любого
|
||||
@@ -85,6 +95,30 @@ stateDiagram-v2
|
||||
(авто-раскладку не делаем) и требует, чтобы раздача всё ещё была в
|
||||
qBittorrent.
|
||||
|
||||
## Сверка с реальностью (рассинхрон)
|
||||
|
||||
Состояние в БД может разойтись с диском при **ручном** удалении: раздачу
|
||||
стирают из qBittorrent (источник) или файлы убирают из Jellyfin (целевые
|
||||
хардлинки). `worker` периодически сверяет уже разложенные задачи с фактом по
|
||||
двумерной матрице «источник × цель» (источник = раздача в qBittorrent,
|
||||
цель = разложенные хардлинки на ФС) и выводит состояние:
|
||||
|
||||
- **target_missing** — источник на месте, цель удалена. Доступна команда
|
||||
«Привязать заново» (`→ recognizing`); авто-действий нет.
|
||||
- **orphaned** — источник пропал, цель (последняя копия данных) на месте.
|
||||
Команд вперёд нет; `Undo` запрещён (снял бы единственную копию).
|
||||
- **deleted** — нет ни источника, ни цели; терминально.
|
||||
|
||||
Сверка трогает только `done`/`target_missing`/`orphaned`/`deleted` —
|
||||
активные и пользовательски-терминальные (`reverted`/`cancelled`/`failed`/
|
||||
`stuck`) состояния не задевает. Реальность «лечится» сама: при возврате
|
||||
источника/цели задача переходит обратно (вплоть до `done`). Пропажа
|
||||
**источника** дебаунсится (`[worker].source_missing_threshold` подряд идущих
|
||||
тиков), пропажа цели проверяется немедленно (локальная ФС надёжна). Команды,
|
||||
которым нужен источник (relink/распознать/применить/undo), проверяют его
|
||||
**синхронно перед действием** и не полагаются на фоновую сверку. Полные
|
||||
требования — `openspec/specs/state-reconciliation/`.
|
||||
|
||||
Все переходы и команды идут через `worker` под per-download блокировкой —
|
||||
два транспорта не гонятся за одно состояние. Состояние персистентно в
|
||||
SQLite; `worker` периодически сверяет qBittorrent с БД и **усыновляет**
|
||||
|
||||
Reference in New Issue
Block a user