Приём: дедуп по target_missing/orphaned + стоп-кран «Закрыть»
Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.
- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
привязывается к существующей записи (спящей, без обращения к qBittorrent),
а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
дедупе ведёт на страницу существующей записи.
Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
## 1. Дедуп: reingest-blocking states (предотвращение дубля)
|
||||
|
||||
- [x] 1.1 В `internal/store/download.go` ввести множество reingest-blocking
|
||||
состояний (active ∪ `{target_missing, orphaned}`) и **отдельный** метод
|
||||
поиска по любому из хешей (напр. `FindReingestBlockingByInfohash`) с
|
||||
приоритетом активной записи над desync-записью. НЕ расширять общий
|
||||
`findActiveByInfohash` — на нём стоят `ActivateIfNoOtherActive`/
|
||||
`AddInfohashes`, где «активная» ОБЯЗАНА значить строго не-терминальную
|
||||
(иначе ломается инвариант «≤1 активной»). Desync-проверка — устойчивый
|
||||
пред-рид ДО create, активный гард (`CreateDownloadIfNoActive`) остаётся про
|
||||
строго активные.
|
||||
- [x] 1.2 В `internal/ingest/ingest.go` (`Ingest`/`attached`) при найденной
|
||||
desync-записи (`target_missing`/`orphaned`) вернуть её как `Deduplicated`
|
||||
«спящей»: без перевода в активное, без обращения к qBittorrent; добавить
|
||||
в результат приёма признак «требует relink/закрытия».
|
||||
- [x] 1.3 Убедиться, что донесение недостающих хешей и атомарный гард создания
|
||||
(`CreateDownloadIfNoActive`) не крадут хеш у другой активной загрузки и
|
||||
что гонка двух приёмов на свежую desync-запись не рождает близнеца.
|
||||
- [x] 1.4 Обновить ответы транспортов при attach на desync-запись: бот
|
||||
(`internal/tgbot`) и HTTP/веб — текст «существует как #id без цели —
|
||||
привяжите заново или закройте».
|
||||
|
||||
## 2. Команда «Закрыть» (dismiss) — универсальный стоп-кран
|
||||
|
||||
- [x] 2.1 В `internal/store/download.go` добавить рёбра `allowedTransitions` в
|
||||
`cancelled` из терминальных `done`/`failed`/`reverted`/`target_missing`/
|
||||
`orphaned` (у нетерминальных цель уже есть; `deleted` исключён). Обновить
|
||||
граф-тесты (`internal/store/transition_test.go`): (а) РАСЩЕПИТЬ
|
||||
`TestCancelDeferReachableFromEveryNonTerminal` — `cancelled` теперь цель из
|
||||
любого состояния кроме `deleted` (в т.ч. терминальных), `deferred` —
|
||||
по-прежнему только из не-терминальных (не расширять общий цикл наивно, иначе
|
||||
ложно потребует `deferred` из терминалов или оставит дырявый гард);
|
||||
(б) в `TestSetStateAllowsDeclaredEdges` добавить новые терминал→`cancelled`
|
||||
рёбра (переход обычным `SetDownloadState`, без `ActivateIfNoOtherActive`).
|
||||
- [x] 2.2 В `internal/worker/review.go` реализовать **отдельный** обработчик
|
||||
«Закрыть» (не ветка `Cancel`, который отклоняет терминальные): перевод
|
||||
`<любое, кроме deleted> → cancelled` с `error_code = "user_dismiss"` и
|
||||
причиной в `error_msg`/логе. Только setState: qBittorrent не вызывать,
|
||||
хардлинки не трогать (в т.ч. из `done`/`orphaned`), source-preflight не
|
||||
выполнять; из `deleted` — отказ; из `cancelled` — короткозамкнуть без
|
||||
`setState` (иначе идемпотентный no-op перезапишет `error_code`, подменив
|
||||
причину прежнего `Отклонить`).
|
||||
- [x] 2.3 Веб-UI: отдельная danger zone внизу страницы загрузки с кнопкой
|
||||
«Закрыть» (доступна из любого состояния, кроме `deleted`; htmx, деградация
|
||||
без JS, ошибка на htmx-пути = 200 + фрагмент). Рассмотреть подтверждение
|
||||
для `done`/терминальных.
|
||||
- [x] 2.4 Telegram (`internal/tgbot`): добавить действие «Закрыть» для загрузок.
|
||||
|
||||
## 3. Спеки, документация, проверка
|
||||
|
||||
- [x] 3.1 Обновить описание статусов/переходов в `docs/specs/database.md`
|
||||
(рёбра `<терминальные> → cancelled`, `error_code = "user_dismiss"`).
|
||||
- [x] 3.2 `task test` и `task lint` зелёные; добавить тесты: дедуп-attach на
|
||||
`target_missing`/`orphaned`, повторный приём после `cancelled`/`done`
|
||||
заводит новую, команда «Закрыть» переводит в `cancelled` из разных
|
||||
состояний и НЕ трогает qBittorrent/хардлинки (в т.ч. из `done`), отказ из
|
||||
`deleted`.
|
||||
- [x] 3.3 `openspec validate dedup-target-missing-and-dismiss --strict`.
|
||||
Reference in New Issue
Block a user