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

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).
  • Store interface воркера — метод 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.