Retry/stall: сброс базиса таймаута + простой от last_activity (MAJOR-1, MAJOR-2)
Два связанных бага семантики таймаутов зависания и ручного retry. MAJOR-1: Retry живого торрента не сбрасывал базис отсчёта таймаута — задача мгновенно снова падала в stuck на ближайшем тике. Вводим колонку download.retried_at (миграция 0010): ручной retry фиксирует момент и приподнимает пол обоих таймаутов (max(базис, retried_at)). Хранится в БД, а не в памяти, чтобы сброс пережил тик поллинга и рестарт. MAJOR-2: stuck_after мерил ВОЗРАСТ торрента (от added_on), а не ПРОСТОЙ — долго качавшийся торрент, на миг зашедший в stalledDL, ложно уходил в stuck со «stalled for 5h». Теперь stuck_after мерит простой от qBit last_activity (новое поле qbt.Torrent из того же ответа /torrents/info); magnet_timeout по-прежнему мерит возраст (семантически верно). checkTimeouts разбит на torrentAge/stallDuration/addedBasis/retriedFloor. NIT-10: фолбэк базиса возраста added_on→created_at сохранён и покрыт. NIT-12: retry перестаёт перецепляться к сломанному живому торренту (error/missingFiles) — повторно отдаёт источник (перецепка к нему бессмысленна: reconcile тут же вернул бы в failed). Спека: дельта state-reconciliation (MODIFIED «Восстановление зависшей загрузки» и «Ручной повтор»), правка docs/specs/workflow.md (устранено противоречие «возраст vs простой»), ER-схема database.md. Тесты: TestRetryResetsTimeoutBasis (следующий тик после retry — прячется в TestRetryReattachesNoReadd), TestStallMeasuredFromLastActivity, TestSetRetriedAtOverwrites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Tasks: retry-stall-basis
|
||||
|
||||
## Схема и хранилище
|
||||
|
||||
- [x] Миграция `0010_retried_at.sql` — колонка `download.retried_at` (nullable TEXT).
|
||||
- [x] `store.Download.RetriedAt` (`db:"retried_at"`) + метод `RetriedTime()`.
|
||||
- [x] `store.SetRetriedAt(ctx, id, t)` — перезаписывающая запись базиса retry.
|
||||
- [x] Обновить ER-схему `docs/specs/database.md` (строка `retried_at`).
|
||||
|
||||
## qBittorrent-клиент
|
||||
|
||||
- [x] `qbt.Torrent.LastActivity` (`json:"last_activity"`).
|
||||
|
||||
## Логика воркера
|
||||
|
||||
- [x] `checkTimeouts` — две разные меры: `magnet_timeout` по возрасту,
|
||||
`stuck_after` по простою (`last_activity`).
|
||||
- [x] Разбить `torrentAge` на `addedBasis` (возраст, фолбэк `created_at`),
|
||||
`stallDuration` (простой от `last_activity`), `retriedFloor` (пол по
|
||||
`retried_at`).
|
||||
- [x] `Retry` — сброс базиса через `SetRetriedAt`; перецепка только к здоровому
|
||||
живому торренту, сломанный (`classErrored`) → повторный `Add` (NIT-12).
|
||||
- [x] `Store` interface воркера — метод `SetRetriedAt`.
|
||||
|
||||
## Спеки
|
||||
|
||||
- [x] Дельта `state-reconciliation` — MODIFIED «Восстановление зависшей
|
||||
загрузки» (простой от `last_activity`) и «Ручной повтор» (сброс базиса,
|
||||
NIT-12).
|
||||
- [x] `docs/specs/workflow.md` — устранить противоречие «возраст vs простой»,
|
||||
описать сброс базиса и перецепку только к здоровому торренту.
|
||||
|
||||
## Тесты
|
||||
|
||||
- [x] `TestRetryResetsTimeoutBasis` — следующий тик после retry (MAJOR-1).
|
||||
- [x] `TestStallMeasuredFromLastActivity` — простой vs возраст (MAJOR-2).
|
||||
- [x] `TestSetRetriedAtOverwrites` — store.
|
||||
- [x] Обновить фейки (`fakeStore`, `memStore`) методом `SetRetriedAt`.
|
||||
|
||||
## Проверки
|
||||
|
||||
- [x] `openspec validate --strict retry-stall-basis`.
|
||||
- [x] `task test`.
|
||||
- [x] `task lint`.
|
||||
Reference in New Issue
Block a user