Два связанных бага семантики таймаутов зависания и ручного 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>
2.1 KiB
2.1 KiB
Tasks: retry-stall-basis
Схема и хранилище
- Миграция
0010_retried_at.sql— колонкаdownload.retried_at(nullable TEXT). store.Download.RetriedAt(db:"retried_at") + методRetriedTime().store.SetRetriedAt(ctx, id, t)— перезаписывающая запись базиса retry.- Обновить ER-схему
docs/specs/database.md(строкаretried_at).
qBittorrent-клиент
qbt.Torrent.LastActivity(json:"last_activity").
Логика воркера
checkTimeouts— две разные меры:magnet_timeoutпо возрасту,stuck_afterпо простою (last_activity).- Разбить
torrentAgeнаaddedBasis(возраст, фолбэкcreated_at),stallDuration(простой отlast_activity),retriedFloor(пол поretried_at). Retry— сброс базиса черезSetRetriedAt; перецепка только к здоровому живому торренту, сломанный (classErrored) → повторныйAdd(NIT-12).Storeinterface воркера — методSetRetriedAt.
Спеки
- Дельта
state-reconciliation— MODIFIED «Восстановление зависшей загрузки» (простой отlast_activity) и «Ручной повтор» (сброс базиса, NIT-12). docs/specs/workflow.md— устранить противоречие «возраст vs простой», описать сброс базиса и перецепку только к здоровому торренту.
Тесты
TestRetryResetsTimeoutBasis— следующий тик после retry (MAJOR-1).TestStallMeasuredFromLastActivity— простой vs возраст (MAJOR-2).TestSetRetriedAtOverwrites— store.- Обновить фейки (
fakeStore,memStore) методомSetRetriedAt.
Проверки
openspec validate --strict retry-stall-basis.task test.task lint.