4.5 KiB
🐞 Не держать общий замок воркера во время обращения к 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, и это тоже неверно.
Воспроизведение
- Заставить qBittorrent отвечать на
torrents/deleteмедленно (подставной клиент с задержкой; в жизни — снос раздачи с большим числом файлов на загруженном диске). - Во время вызова дёрнуть через тот же воркер любую команду по другой загрузке.
Видно вместо ожидаемого: команда ждёт весь чужой сетевой вызов. Замер ревью: при
задержке 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) — уже
устроена нужным образом и служит образцом, менять её не надо.