download-tracking: требование о re-read source_type приведено к коду

- re-read `source_type` перечитывается под блокировкой после тик-снимка, а
  остаточное окно вывода имени названо известным ограничением с ценой и
  достоверным маршрутом восстановления (ручной шаг + `Retry`, не самоисцеление)
- в `ingest` снята парная ложная гарантия «воркер добавит раздачу файлом»,
  добавлены сценарии на оба окна апгрейда и на недоступные байты `.torrent`
- заведён ADR о том, что при разрыве спека↔код двигается тот, чья формулировка
  сильнее рационали
This commit is contained in:
av
2026-08-06 14:44:21 +03:00
parent f42db0a275
commit 30ee598547
11 changed files with 1179 additions and 11 deletions
+70 -5
View File
@@ -137,10 +137,42 @@ Conflict`), и загрузка зациклилась бы на ретраях.
magnet-хешу вместо файла система SHALL NOT.
`source_type` для выбора способа добавления worker SHALL перечитывать **под
блокировкой переходов** непосредственно перед добавлением (а не полагаться на
снимок, снятый ранее вне блокировки): иначе при точном оверлапе тика с апгрейдом
пойманной magnet-задачи до `.torrent` (см. `ingest`) воркер добавил бы magnet из
устаревшего снимка, хотя БД уже `torrent`.
блокировкой переходов после тик-снимка** — перед подготовкой запроса на
добавление, а не полагаясь на снимок списка `catched`, который к моменту
обработки загрузки уже устарел (блокировка между снятием списка и обработкой
отпускается). Иначе апгрейд пойманной magnet-задачи до `.torrent` (см.
`ingest`), легший между снятием списка и обработкой, был бы пропущен, и worker
добавил бы magnet из устаревшего снимка, хотя в БД уже `torrent`.
Если подготовка запроса не удалась (сохранённые байты `.torrent` недоступны),
worker `add` выполнять SHALL NOT и SHALL оставлять загрузку в `catched` для
повтора на следующем тике; отсечка — `catch_timeout`.
Между этим re-read и самим `add` остаётся окно: вывод отображаемого имени идёт
секунды вне блокировки (см. ниже), а запрос на добавление собран до него и по
свежей записи не пересобирается. Апгрейд magnet → `.torrent`, легший ровно в это
окно, worker не подхватывает и добавляет magnet-ссылку. Это **известное
ограничение с названной ценой**, а не гарантия: сегодня запрос собран до вывода
имени и по свежей записи не пересобирается. Закрывать ли окно кодом и каким
способом (пересборкой запроса или сравнением одного `source_type` под замком с
пропуском тика при расхождении) — открытый вопрос за границами этого
требования.
Ущерб окна ограничен, но не бесплатен, и спека называет его точно. На публичном
трекере (или при живом DHT) magnet доберёт метаданные и задача пойдёт штатно —
ущерба нет вовсе. На закрытом трекере, ради которого `.torrent` и подавался,
magnet без метаданных зависает в `metaDL`, и предохранитель `magnet_timeout`
уводит задачу в `failed` (см. «Таймауты-предохранители downloading»). Сама собой
задача из этого состояния не восстанавливается: фоновая сверка оживляет только
продвинувшийся источник, а `Retry` при живом и здоровом торренте (`metaDL`
считается живым) перецепляется к нему и повторный `add` по актуальному
`source_type` не делает — см. `state-reconciliation` «Ручной повтор
зависшей/упавшей загрузки из транспортов». Восстановление сегодня требует
ручного шага: убрать
зависшую magnet-раздачу из qBittorrent, после чего `Retry` добавит источник
заново по актуальному `source_type` — сохранёнными байтами, которые апгрейд уже
записал. Без этого шага повторный приём `.torrent` не помогает: новая загрузка
усыновит ту же зависшую раздачу (см. «усыновить» выше).
Неуспешный `add` (qBittorrent временно отверг/недоступен) SHALL оставлять
загрузку в `catched` для повторной попытки на следующем тике; переход в
@@ -167,7 +199,9 @@ re-read состояния → `[вне блокировки]` свежий ли
вывода имени) worker SHALL под блокировкой переходов перечитать запись и, если
она уже НЕ в `catched` (отменена), НЕ вызывать `add` и загрузку в этот тик
пропустить. Это сужает окно гонки до промежутка между re-read и записью
перехода.
перехода. Этот re-read проверяет **только состояние** и запрос на добавление не
пересобирает — за `source_type` отвечает более ранний re-read после
тик-снимка (см. выше).
- **Подтверждение отсутствия торрента перед `add`.** Непосредственно перед `add`
worker SHALL свежим листингом раздач qBittorrent подтвердить, что раздачи ни с
@@ -232,6 +266,37 @@ SHALL логировать на уровне `WARN` с корреляцией п
нашей категорией и `rename`, без обращения к magnet-хешу
- **AND** загрузка переходит в `downloading`
#### Scenario: Апгрейд magnet → .torrent до подготовки запроса — добавляется файлом
- **GIVEN** загрузка попала в снимок списка `catched` с `source_type = magnet`
- **WHEN** апгрейд до `.torrent` применяется после снятия снимка, но до re-read
записи под блокировкой, и worker обрабатывает эту загрузку
- **THEN** worker берёт `source_type = torrent` из перечитанной записи и
добавляет раздачу сохранёнными байтами файлом (`torrents`)
- **AND** magnet-ссылка из устаревшего снимка не используется
#### Scenario: Апгрейд magnet → .torrent в окне вывода имени — добавляется magnet
- **GIVEN** загрузка в `catched` с `source_type = magnet`, запрос на добавление
подготовлен, worker выводит отображаемое имя вне блокировки
- **WHEN** апгрейд до `.torrent` применяется именно в это окно
- **THEN** worker добавляет magnet-ссылку из подготовленного запроса — апгрейд в
этом окне не подхватывается (известное ограничение)
- **AND** на закрытом трекере раздача зависает в `metaDL` и по `magnet_timeout`
задача уходит в `failed`
- **AND** восстановление требует ручного шага: убрать зависшую раздачу из
qBittorrent, после чего `Retry` добавляет источник заново по актуальному
`source_type` — сохранёнными байтами
#### Scenario: Байты .torrent недоступны — повтор
- **GIVEN** загрузка в `catched` с `source_type = torrent`, торрента в
qBittorrent нет, но сохранённые байты `.torrent` прочитать не удалось
- **WHEN** worker готовит запрос на добавление
- **THEN** worker `add` НЕ вызывает
- **AND** загрузка остаётся в `catched`, попытка повторяется на следующем тике
(отсечка — `catch_timeout`)
#### Scenario: Торрент уже присутствует в qBittorrent — усыновление без add
- **GIVEN** загрузка в состоянии `catched`, торрент которой уже присутствует в
+22 -6
View File
@@ -511,11 +511,15 @@ v2-хеш (для v2/гибридного файла, BEP52) SHALL извлек
привязав их к этой загрузке, и сменить её `source_type` на `torrent`. Тем
самым воркер добавит раздачу файлом, а не magnet-хешем (иначе на закрытом
трекере без DHT метаданные не докачаются, а magnet застрянет в metaDL →
failed). Апгрейд SHALL применяться ТОЛЬКО пока загрузка в `catched` (воркер
источник ещё не добавил); для уже добавленной (`downloading` и далее)
загрузки смена `source_type` при дедупе выполняться SHALL NOT — её судьба
решается путями retry/сверки, а не приёмом. Апгрейд SHALL быть best-effort:
его неуспех приём не прерывает.
failed) — **при условии, что апгрейд лёг до того, как воркер подготовил запрос
на добавление**. Апгрейд, попавший в узкое окно вывода отображаемого имени,
воркер уже не подхватит и добавит magnet: это известное ограничение, названное в
`download-tracking` «Добавление пойманной загрузки в qBittorrent» вместе с
маршрутом восстановления. Апгрейд SHALL применяться ТОЛЬКО пока загрузка в
`catched` (воркер источник ещё не добавил); для уже добавленной (`downloading`
и далее) загрузки смена `source_type` при дедупе выполняться SHALL NOT — её
судьба решается путями retry/сверки, а не приёмом. Апгрейд SHALL быть
best-effort: его неуспех приём не прерывает.
Из полей `.torrent` система SHALL синтезировать контекст распознавания (имя
раздачи, суммарный размер, сигнал по дереву файлов, домен трекера, комментарий)
@@ -567,7 +571,19 @@ NOT.
- **THEN** новая загрузка не создаётся, возвращается существующая
- **AND** байты `.torrent` сохраняются привязанными к ней, а её `source_type`
становится `torrent` — в одной транзакции
- **AND** воркер добавит раздачу файлом (не по magnet)
- **AND** воркер добавит раздачу файлом (не по magnet), если апгрейд лёг до
подготовки запроса на добавление — см. `download-tracking` «Добавление
пойманной загрузки в qBittorrent»
#### Scenario: Апгрейд в окне вывода имени — воркер добавит magnet
- **GIVEN** активная загрузка в `catched` с `source_type = magnet`, для которой
воркер уже подготовил запрос на добавление и выводит отображаемое имя
- **WHEN** принимается `.torrent` с тем же инфохэшем
- **THEN** апгрейд применяется (байты сохранены, `source_type` стал `torrent`)
- **AND** гарантии, что воркер добавит раздачу файлом, приём в этом случае не
даёт: исход добавления определяет `download-tracking` «Добавление пойманной
загрузки в qBittorrent», где это окно и его цена описаны
#### Scenario: Magnet-задача уже добавлена — апгрейда нет