Files
avandClaude Opus 4.8 cc7e51b3a4 Обработка рассинхрона состояния с реальностью (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>
2026-06-29 11:07:09 +03:00

7.0 KiB
Raw Permalink Blame History

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

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 для столбца счётчика.