Files
jellybit/openspec/specs/state-reconciliation/spec.md
T
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

13 KiB
Raw Blame History

state-reconciliation Specification

Purpose

Сверка записанного состояния загрузки с фактом на файловой системе и в qBittorrent. Capability описывает периодическую и принудительную проверку присутствия источника (раздача в qBittorrent) и цели (разложенные хардлинки), вывод состояний рассинхрона (target_missing/orphaned/ deleted) из матрицы «источник × цель», их переходы и самовосстановление, дебаунс пропажи источника, инвариант безопасного Undo (не снимать последнюю копию) и уведомления о рассинхроне.

Requirements

Requirement: Периодическая сверка состояния с реальностью

worker SHALL периодически (на тике поллинга) сверять задачи, для которых ожидаются разложенные файлы, с фактом на файловой системе и в qBittorrent, и выводить состояние задачи из двух независимых признаков: присутствия источника (раздача с download.infohash в выдаче qBittorrent) и присутствия цели (все file_link со status = linked существуют на ФС).

Сверке SHALL подвергаться только состояния done, target_missing, orphaned, deleted. Активные (downloading/recognizing/review/ deferred/linking) и пользовательски-терминальные (reverted/cancelled/ failed/stuck) состояния сверка трогать SHALL NOT.

Состояние SHALL переписываться только при его изменении (без записи и логов, когда выведенное состояние совпадает с текущим).

Scenario: Источник и цель на месте — состояние не меняется

  • WHEN для задачи в done раздача присутствует в qBittorrent и все её разложенные хардлинки существуют
  • THEN задача остаётся в done
  • AND запись состояния и лог перехода не выполняются

Scenario: Частичная пропажа цели считается отсутствием

  • WHEN часть разложенных хардлинков задачи удалена, а источник на месте
  • THEN цель считается отсутствующей и задача переходит в target_missing

Requirement: Принудительная проверка источника/цели перед действием

Команда workflow, требующая наличия источника или цели, SHALL синхронно проверять их присутствие непосредственно перед выполнением действия и SHALL NOT полагаться только на фоновую сверку worker (она отстаёт на интервал поллинга и дебаунс). Проверка перед действием выполняется как единичная немедленная проба без дебаунса: дебаунс применяется только к фоновому авто-маркированию.

Под это требование подпадают как минимум: повторная привязка/распознавание (relink, «Распознать заново», «Уточнить») и раскладка (Apply) — требуют источника; Undo — требует источника (чтобы не снять последнюю копию).

Если предусловие не выполнено, система SHALL NOT выполнять действие, SHALL привести состояние задачи в соответствие с реальностью (вывести состояние из матрицы «источник × цель», как при сверке) и SHALL сообщить причину пользователю.

  • WHEN пользователь даёт команду, требующую источника (например «Привязать заново»)
  • THEN система синхронно проверяет наличие раздачи в qBittorrent перед запуском распознавания
  • AND если источника нет — распознавание не запускается, задача приводится к orphaned либо deleted (по наличию цели), причина сообщается

Scenario: Проверка не ждёт фоновую сверку

  • WHEN источник уже удалён, а фоновая сверка ещё не отметила это (в БД состояние, например, done)
  • THEN команда, требующая источника, немедленно обнаруживает его отсутствие собственной проверкой и отказывает, не дожидаясь worker

Requirement: Состояние target_missing и доступность повторной привязки

Когда у задачи источник присутствует, а цель отсутствует, система SHALL переводить её в состояние target_missing и SHALL NOT предпринимать автоматических действий (не запускать повторное распознавание/раскладку самостоятельно).

Из target_missing система SHALL предоставлять пользовательскую команду повторной привязки (relink) с переходом target_missing → recognizing, как из reverted/cancelled; повторная привязка идёт через review с ручным подтверждением.

Scenario: Цель удалена, источник на месте

  • WHEN сверка обнаруживает, что разложенных хардлинков задачи done больше нет, но раздача в qBittorrent присутствует
  • THEN задача переходит в target_missing
  • AND система не запускает распознавание или раскладку автоматически

Scenario: Пользователь инициирует повторную привязку

  • WHEN для задачи в target_missing пользователь даёт команду «Привязать заново»
  • THEN задача переходит в recognizing и далее проходит через review с ручным подтверждением

Requirement: Состояние orphaned при пропаже источника

Система SHALL переводить задачу в состояние orphaned, когда источник отсутствует (с учётом дебаунса), а цель присутствует, отражая, что библиотечный хардлинк остался единственной копией данных.

Scenario: Источник удалён, цель на месте

  • WHEN сверка устойчиво (после дебаунса) не находит раздачу задачи в qBittorrent, а её разложенные хардлинки существуют
  • THEN задача переходит в orphaned

Requirement: Состояние deleted при пропаже источника и цели

Когда отсутствуют и источник (с учётом дебаунса), и цель, система SHALL переводить задачу в состояние deleted. В deleted действий над задачей больше нет.

Scenario: Источник и цель удалены

  • WHEN сверка устойчиво не находит раздачу в qBittorrent и разложенных хардлинков задачи на ФС больше нет
  • THEN задача переходит в deleted

Requirement: Дебаунс пропажи источника

Система SHALL дебаунсить только отсутствие источника, чтобы временная недоступность qBittorrent (рестарт демона, сбой API) не вызывала ложных пометок: источник считается удалённым лишь после N подряд тиков сверки без него, где N = [worker].source_missing_threshold. Любое обнаружение раздачи SHALL сбрасывать счётчик пропусков.

Отсутствие цели дебаунсу подвергаться SHALL NOT (локальная проверка ФС надёжна).

Scenario: Кратковременная пропажа источника не помечается

  • WHEN раздача отсутствует в qBittorrent меньше source_missing_threshold тиков подряд
  • THEN источник трактуется как присутствующий и состояние задачи не меняется

Scenario: Возврат источника сбрасывает счётчик

  • WHEN раздача снова обнаружена в qBittorrent
  • THEN счётчик пропусков источника сбрасывается в ноль

Requirement: Самовосстановление состояния при возврате реальности

Система SHALL возвращать задачу в согласованное состояние, когда реальность восстановилась (состояние выводится из текущей матрицы «источник × цель»): при возврате источника и/или цели задача SHALL переходить из orphaned/target_missing/deleted обратно (в т.ч. в done, когда присутствуют оба).

Scenario: Источник вернулся

  • WHEN для задачи в orphaned раздача снова появилась в qBittorrent, а цель по-прежнему на месте
  • THEN задача возвращается в done

Requirement: Безопасный Undo не снимает последнюю копию

Undo (снятие созданных хардлинков) SHALL отказываться удалять целевую ссылку, если она является последней копией данных: целевой файл существует и его счётчик ссылок nlink <= 1, либо исходный файл (src_path) не существует. В этом случае система SHALL NOT выполнять unlink такого файла и SHALL явно сообщать причину отказа; задача в reverted при отказе переходить SHALL NOT.

Отсутствующую целевую ссылку (файла уже нет) Undo SHALL пропускать как успешно снятую (идемпотентность). Команда Undo для задачи в orphaned SHALL отклоняться сразу с пояснением, что источник удалён.

Scenario: Отказ снять единственную копию

  • WHEN при Undo целевой хардлинк существует, но его nlink <= 1 (или исходный файл отсутствует)
  • THEN система не удаляет файл и сообщает, что это последняя копия
  • AND задача остаётся в текущем состоянии (не reverted)

Scenario: Undo снимает лишний хардлинк при живом источнике

  • WHEN при Undo целевой хардлинк существует, исходный файл на месте и nlink > 1
  • THEN система снимает целевой хардлинк, оставляя исходный файл нетронутым

Requirement: Уведомление о рассинхроне

При переходе задачи в orphaned или target_missing система SHALL уведомлять автора загрузки через настроенный механизм уведомлений (notifier), чтобы рассинхрон не оставался незамеченным.

Scenario: Уведомление при потере источника

  • WHEN задача переходит в orphaned
  • THEN система отправляет автору загрузки уведомление о потере источника