Обработка рассинхрона состояния с реальностью (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:
@@ -0,0 +1,87 @@
|
||||
## Why
|
||||
|
||||
Пришло время удалять просмотренные фильмы/сериалы, чтобы освобождать место
|
||||
под новые. Удаляют их **вручную** — из qBittorrent (источник) или из
|
||||
Jellyfin (целевые хардлинки). Сейчас jellybit этого не замечает: `worker`
|
||||
поллит только задачи в `downloading`, терминальные задачи (`done`) с
|
||||
реальностью не сверяет. Состояние в БД молча расходится с диском:
|
||||
|
||||
- **Источник пропал, цель осталась.** qBittorrent стирает скачанные файлы
|
||||
при удалении раздачи. Хардлинк в библиотеке становится **последней**
|
||||
ссылкой на inode, а обычный `Undo` (`unlink` цели) сотрёт единственную
|
||||
копию — прямая потеря данных. Инвариант «источник неприкосновенен»
|
||||
молчаливо перестаёт держаться: источника уже нет.
|
||||
- **Цель пропала, источник остался.** Файлы убрали из библиотеки, а
|
||||
jellybit по-прежнему числит загрузку `done` — состояние врёт.
|
||||
|
||||
Цель этого change — **path 1**: научить jellybit корректно **распознавать**
|
||||
ручное удаление и **отражать** его в состоянии, **не предпринимая
|
||||
автоматических действий**. Удаление средствами самого jellybit («единое
|
||||
окно», path 2) — отдельная будущая работа.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Фоновая сверка с реальностью.** `worker` расширяет периодический поллинг:
|
||||
помимо `downloading` сверяет терминальные/desync-задачи с фактом на ФС —
|
||||
присутствие раздачи в qBittorrent (источник) и существование разложенных
|
||||
хардлинков `file_link.dst_path` (цель).
|
||||
- **Новые состояния FSM** для рассинхрона (двумерная матрица «источник × цель»):
|
||||
- `target_missing` — источник на месте, цель удалена: доступен
|
||||
пользовательский флоу повторной привязки (relink); авто-действий нет.
|
||||
- `orphaned` — источник пропал, цель на месте: «осиротевшая» раздача,
|
||||
библиотечный хардлинк — единственная копия.
|
||||
- `deleted` — пропали и источник, и цель: терминально, действий больше нет.
|
||||
- Реальность «лечится» сама: если источник/цель снова появились, сверка
|
||||
возвращает задачу в согласованное состояние.
|
||||
- **Дебаунс пропажи источника.** Раздача считается удалённой только после
|
||||
`N` подряд тиков без неё (qBittorrent мог рестартовать / API мигнул);
|
||||
любое появление сбрасывает счётчик. Порог — в конфиге.
|
||||
- **Защита `Undo` от потери данных.** `Undo`/`layout.Undo` отказывается
|
||||
снимать хардлинк, если он **последняя копия** (`nlink == 1`) или исходный
|
||||
путь не существует — откат снимает лишний хардлинк, а не единственный
|
||||
файл; причина отказа сообщается явно.
|
||||
- **Уведомление** автору загрузки при переходе в `orphaned`/`target_missing`
|
||||
(через существующий `notifier`), чтобы рассинхрон не оставался незаметным.
|
||||
|
||||
Не входит в объём (Non-goals):
|
||||
|
||||
- Удаление раздач/файлов средствами самого jellybit (path 2, «единое окно»).
|
||||
- Автоматический повторный прогон распознавания/раскладки при рассинхроне —
|
||||
только пометка состояния; relink инициирует человек.
|
||||
- Сверка содержимого источника (manual delete файлов **внутри** живой
|
||||
раздачи → это `error`/`missingFiles` qBittorrent, отдельная тема).
|
||||
- Ретеншн/авточистка терминальных задач (отдельная задача в TODO).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `state-reconciliation`: периодическая сверка записанного состояния
|
||||
загрузки с фактом на ФС (раздача в qBittorrent, разложенные хардлинки),
|
||||
состояния рассинхрона (`target_missing`/`orphaned`/`deleted`), их переходы
|
||||
и дебаунс; а также инвариант безопасного `Undo` (не снимать последнюю
|
||||
копию).
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- Граф состояний и Undo живут в docs/specs/workflow.md и
|
||||
docs/specs/jellyfin-layout.md и ещё не перенесены в OpenSpec; меняем их
|
||||
напрямую как живые спеки (см. Impact). Capability-дельт для них нет. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Спеки:** новая `openspec/specs/state-reconciliation/`; правки живых
|
||||
`docs/specs/workflow.md` (граф состояний: +`target_missing`/`orphaned`/
|
||||
`deleted`, переходы) и `docs/specs/jellyfin-layout.md` (инвариант
|
||||
безопасного `Undo`).
|
||||
- **Код:** `internal/worker` (расширение `Poll`/`reconcile` на терминальные
|
||||
задачи, дебаунс, новые переходы, уведомления), `internal/store` (новые
|
||||
значения `state`, столбец счётчика промахов, миграция, запросы выборки
|
||||
desync-задач), `internal/layout` (`Undo` с проверкой `nlink`/наличия
|
||||
источника), `internal/qbt` (присутствие infohash — уже листаем все
|
||||
торренты), `internal/httpapi` + web-UI (отображение новых состояний,
|
||||
блокировка `Undo` для `orphaned`), `internal/config` (`[worker]` порог
|
||||
дебаунса).
|
||||
- **Конфиг:** новый ключ в `[worker]` (порог пропусков источника).
|
||||
- **Совместимость:** новые значения `state` — аддитивно; миграция goose для
|
||||
столбца счётчика.
|
||||
Reference in New Issue
Block a user