Слияние: уборка торрента при cancel во время add (T4)
# Conflicts: # docs/backlog/README.md
This commit is contained in:
@@ -154,6 +154,66 @@ downloading` (см. «Переходы состояний сериализуют
|
||||
что загрузка всё ещё в `catched` (иначе переход отклоняется — например, при
|
||||
параллельной отмене).
|
||||
|
||||
Вывод имени (LLM) занимает секунды и идёт вне блокировки, поэтому загрузку могут
|
||||
отменить (`catched → cancelled`) в это окно. Чтобы отменённая задача не оставила
|
||||
неуправляемый торрент в qBittorrent, worker SHALL применять комбинированную
|
||||
защиту. Порядок шагов относительно блокировки переходов: `[под блокировкой]`
|
||||
re-read состояния → `[вне блокировки]` свежий листинг присутствия → `[вне
|
||||
блокировки]` `add` → `[под блокировкой]` запись перехода и (при неуспехе) re-read
|
||||
состояния для решения об уборке → `[вне блокировки]` удаление. Сетевые вызовы
|
||||
(листинг, `add`, удаление) под блокировкой держаться SHALL NOT.
|
||||
|
||||
- **Re-read состояния перед `add`.** Непосредственно перед `qbt.Add` (после
|
||||
вывода имени) worker SHALL под блокировкой переходов перечитать запись и, если
|
||||
она уже НЕ в `catched` (отменена), НЕ вызывать `add` и загрузку в этот тик
|
||||
пропустить. Это сужает окно гонки до промежутка между re-read и записью
|
||||
перехода.
|
||||
|
||||
- **Подтверждение отсутствия торрента перед `add`.** Непосредственно перед `add`
|
||||
worker SHALL свежим листингом раздач qBittorrent подтвердить, что раздачи ни с
|
||||
одним из infohash загрузки ещё НЕТ. Если этот листинг **не удался** (сетевой
|
||||
сбой), worker `add` выполнять SHALL NOT и загрузку в этот тик пропустить (повтор
|
||||
на следующем): без подтверждённого отсутствия признак «своё/чужое» неизвестен,
|
||||
и последующее удаление-с-данными было бы небезопасным. Если торрент уже
|
||||
присутствует (внешний клиент/пользователь добавил тот же infohash в окно
|
||||
гонки), worker `add` выполнять SHALL NOT и загрузку в этот тик пропустить — на
|
||||
следующем тике её усыновит ветка «уже присутствует». Подтверждённое отсутствие
|
||||
непосредственно-перед-`add` SHALL служить признаком того, что торрент,
|
||||
оказавшийся под этим infohash сразу после `add`, создан именно этим `add` (наш
|
||||
артефакт), а не пред-существовал.
|
||||
|
||||
- **Уборка добавленного торрента при отмене в окне после `add`.** Если `add`
|
||||
прошёл успешно, а последующая запись перехода `PromoteCatched` не применилась,
|
||||
worker SHALL принимать решение об уборке по **свежему re-read состояния под
|
||||
блокировкой**, а не по факту ошибки промоушена: неуспех промоушена бывает и
|
||||
из-за отмены (`state` уже не `catched`), и из-за транзиентного сбоя хранилища
|
||||
(`state` всё ещё `catched`, задача жива). Только при подтверждённом `state !=
|
||||
catched` worker SHALL удалить только что добавленный торрент из qBittorrent
|
||||
**вместе с его данными** (`deleteFiles = true`) по infohash загрузки. Если
|
||||
повторное чтение показало `state == catched` (транзиентный сбой) либо само не
|
||||
удалось, worker торрент удалять SHALL NOT — переход доводится на следующем тике
|
||||
усыновлением присутствующей (нашей) раздачи. Удаление SHALL идти через API
|
||||
qBittorrent (`torrents/delete`), не прямыми fs-операциями. Это легитимная уборка
|
||||
**собственного** артефакта, а не пользовательских данных: инвариант «источник
|
||||
неприкосновенен» защищает существующие раздачи/файлы пользователя под
|
||||
`paths.downloads`, а здесь удаляется торрент, который сам worker добавил
|
||||
секундами ранее — уже после намерения отмены. Состояние отменённой задачи
|
||||
(`cancelled`) уборка трогать SHALL NOT; неуспех удаления SHALL логироваться
|
||||
(торрент временно остаётся, повторная авто-уборка не требуется).
|
||||
|
||||
- **Негативный инвариант (удаляем только своё).** Удаление-с-данными допустимо
|
||||
ТОЛЬКО для торрента, который worker создал именно этим `add`. Торрент, который
|
||||
присутствовал в qBittorrent ДО нашего `add` (пользователь уже раздавал тот же
|
||||
infohash / внешний торрент с тем же хешем), удалять с данными worker SHALL NOT —
|
||||
иначе снёс бы чужие данные в нарушение инварианта. Гарантию обеспечивает
|
||||
подтверждение отсутствия перед `add`: путь уборки достижим только тогда, когда
|
||||
отсутствие infohash было подтверждено непосредственно перед `add`; при
|
||||
обнаруженном присутствии (или недоступном листинге) `add` не выполняется вовсе.
|
||||
|
||||
Записи об этом пути (торрент оставлен после отмены → удаляем; факт удаления) worker
|
||||
SHALL логировать на уровне `WARN` с корреляцией по `download_id`/`infohash` и без
|
||||
секретов; неуспех удаления — на `ERROR`.
|
||||
|
||||
#### Scenario: Пойманная magnet-загрузка добавляется в qBittorrent
|
||||
|
||||
- **GIVEN** загрузка в состоянии `catched` с `source_type = magnet`, торрента
|
||||
@@ -196,14 +256,54 @@ downloading` (см. «Переходы состояний сериализуют
|
||||
- **THEN** загрузка остаётся в `catched`
|
||||
- **AND** на следующем тике попытка добавления повторяется
|
||||
|
||||
#### Scenario: Отмена во время добавления
|
||||
#### Scenario: Свежий листинг перед add недоступен — повтор
|
||||
|
||||
- **GIVEN** загрузка в `catched`, worker выводит имя и добавляет её вне
|
||||
блокировки
|
||||
- **WHEN** параллельно приходит команда отмены (`catched → cancelled`), а затем
|
||||
worker берёт блокировку для записи перехода
|
||||
- **THEN** ре-валидация видит, что загрузка уже не в `catched`, и переход в
|
||||
`downloading` не применяется
|
||||
- **GIVEN** загрузка в `catched`, торрента в снимке тика нет, имя выведено
|
||||
- **WHEN** свежий листинг присутствия непосредственно перед `add` не удался
|
||||
(сетевой сбой)
|
||||
- **THEN** worker `add` НЕ вызывает (отсутствие infohash не подтверждено)
|
||||
- **AND** загрузка остаётся в `catched`, попытка повторяется на следующем тике
|
||||
|
||||
#### Scenario: Отмена до add — источник не добавляется
|
||||
|
||||
- **GIVEN** загрузка в `catched`, worker выводит отображаемое имя вне блокировки
|
||||
- **WHEN** параллельно приходит команда отмены (`catched → cancelled`) во время
|
||||
вывода имени, а затем worker перечитывает состояние перед `add`
|
||||
- **THEN** re-read видит, что загрузка уже не в `catched`, и `qbt.Add` НЕ
|
||||
вызывается
|
||||
- **AND** источник в qBittorrent не добавляется, задача остаётся `cancelled`
|
||||
|
||||
#### Scenario: Отмена в окне после add — добавленный торрент удаляется с данными
|
||||
|
||||
- **GIVEN** загрузка в `catched`, отсутствие её infohash в qBittorrent
|
||||
подтверждено перед `add`, и `add` прошёл успешно
|
||||
- **WHEN** отмена (`catched → cancelled`) приходит в окне между `add` и записью
|
||||
перехода, из-за чего запись перехода не применяется, а re-read состояния под
|
||||
блокировкой показывает `state != catched`
|
||||
- **THEN** worker удаляет только что добавленный торрент из qBittorrent вместе с
|
||||
его данными (`deleteFiles = true`) по infohash загрузки
|
||||
- **AND** пишет `WARN` о том, что торрент оставлен после отмены и удалён
|
||||
- **AND** состояние задачи остаётся `cancelled`
|
||||
|
||||
#### Scenario: Сбой записи перехода без отмены — торрент не удаляется
|
||||
|
||||
- **GIVEN** загрузка в `catched`, `add` прошёл успешно, но запись перехода
|
||||
`PromoteCatched` вернула ошибку из-за транзиентного сбоя хранилища
|
||||
- **WHEN** re-read состояния под блокировкой показывает, что загрузка всё ещё в
|
||||
`catched` (отмены не было)
|
||||
- **THEN** worker торрент из qBittorrent НЕ удаляет (это наш живой торрент)
|
||||
- **AND** переход доводится на следующем тике усыновлением присутствующей раздачи
|
||||
|
||||
#### Scenario: Пред-существующий торрент не удаляется с данными
|
||||
|
||||
- **GIVEN** загрузка в `catched`, чей infohash уже присутствует в qBittorrent к
|
||||
моменту проверки перед `add` (внешний торрент/раздача пользователя с тем же
|
||||
хешем)
|
||||
- **WHEN** worker обрабатывает тик и параллельно приходит отмена
|
||||
- **THEN** worker `add` НЕ выполняет и торрент с данными НЕ удаляет (чужие данные
|
||||
неприкосновенны)
|
||||
- **AND** загрузка пропускается в этот тик (усыновление присутствующей раздачи —
|
||||
на следующем тике, если задача ещё активна)
|
||||
|
||||
### Requirement: Предохранитель зависшего catched
|
||||
|
||||
|
||||
Reference in New Issue
Block a user