Files
jellybit/openspec/changes/retry-stall-basis/tasks.md
T
avandClaude Opus 4.8 8261d5b55d 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>
2026-07-08 17:18:28 +03:00

45 lines
2.1 KiB
Markdown

# 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`.