# 🐞 Не держать общий замок воркера во время обращения к qBittorrent при удалении

- **Тип:** fix
- **Категория:** Инфраструктура
- **Зачем:** на время удаления встаёт весь фон: опрос, сверка и команды остальных транспортов ждут замок до 30 с на загрузку, пачкой — минутами; замерено на ревью bulk-delete-page

`Worker.Delete` берёт `w.mu` и держит его через сетевой вызов `qbt.Delete`, хотя
правило пакета обратное и записано рядом: медленные вызовы идут **вне** замка
(`internal/worker/worker.go`, `Poll` зовёт `qbt.Torrents` до `Lock`). Замок при
этом один на весь воркер, а не на задачу — комментарий пакета и
`docs/architecture.md` называют его per-download, и это тоже неверно.

## Воспроизведение

1. Заставить qBittorrent отвечать на `torrents/delete` медленно (подставной
   клиент с задержкой; в жизни — снос раздачи с большим числом файлов на
   загруженном диске).
2. Во время вызова дёрнуть через тот же воркер любую команду по **другой**
   загрузке.

Видно вместо ожидаемого: команда ждёт весь чужой сетевой вызов. Замер ревью: при
задержке `qbt.Delete` 300 мс конкурентный `Cancel` по неизвестному
идентификатору заблокирован на `280.450631ms`. Таймаут клиента qBittorrent —
зашитые 30 с (поля в `[qbittorrent]` нет), поэтому верхняя граница на одну
загрузку — 30 с, а на пачку группового удаления — минуты, в течение которых не
идут ни опрос, ни сверка, ни уведомления.

Отчёт ревью с оракулами —
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`,
находка 3.

## Затрагивает

- `Worker.Delete` (`internal/worker/review.go`) — границы критической секции;
- `w.mu` и комментарий пакета `internal/worker` — «per-download» неверно;
- `docs/architecture.md`, строка «Переходы состояний» в единых точках — тот же
  неверный факт;
- потолок времени на проход группового удаления (`httpapi.bulkBudget`) и
  требование о нём в спеке `web-ui` — при сужении замка они теряют основание;
- поле таймаута в секции `[qbittorrent]` конфига, которого сегодня нет.

## Критерии приёмки

- Команда по одной загрузке не ждёт сетевого вызова по другой (оракул: тест
  `internal/worker` с подставным клиентом, блокирующимся внутри `Delete`, —
  `w.mu.TryLock()` из теста возвращает `true`).
- Состояние после сетевого вызова перечитывается, и удаление не переходит в
  `deleted`, если задача успела уйти из допустимого состояния (оракул: тест на
  смену состояния между шагами).
- Факт про замок исправлен в комментарии пакета и в `docs/architecture.md`
  (оракул: `task gate`, шаг `canon`).
- Решено и записано, остаётся ли потолок времени на проход группового удаления
  (оракул: дельта-спека и `openspec validate --strict`).

## Рамки

Трогает операцию, которая сносит файлы: разбор идемпотентности при повторе
обязателен. Уборка собственного торрента после отмены (`processCatched`) — уже
устроена нужным образом и служит образцом, менять её не надо.
