Files
jellybit/tasks/items/delete-holds-worker-lock.md
T

60 lines
4.5 KiB
Markdown

# 🐞 Не держать общий замок воркера во время обращения к 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`) — уже
устроена нужным образом и служит образцом, менять её не надо.