Восстановление зависших загрузок и уведомления о падении (state-reconciliation)
Долгий metaDL больше не убивается агрессивным таймаутом: дефолт magnet_timeout 30m → 24h (страховочный предохранитель), базис отсчёта — added_on из qBittorrent, а не created_at (переживает retry/усыновление). Авто-восстановление: фоновая сверка возвращает в поток задачи, упавшие по нашей нетерпеливости (magnet_timeout/stalled), когда источник ожил и продвинулся за условие падения (downloading/completed по статусу торрента); qbit_error не воскрешается. Конфликт idempotency (infohash занят другой активной задачей) — оставляем в failed. Уведомления: любой переход в failed/stuck пингует автора (включая приёмный qbit_add через ingest), с дебаунсом против спама при флаппинге stalled. Ручной retry добавлен в веб-UI и Telegram; Retry перецепляется к живому торренту вместо слепого Add. Дельта state-reconciliation влита в живые спеки; обновлён workflow.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
## 1. Константы и базис таймаута
|
||||
|
||||
- [x] 1.1 Вынести `error_code`-строки (`magnet_timeout`, `stalled`,
|
||||
`qbit_error` — в `worker`; `qbit_add` — в `ingest`) в именованные константы;
|
||||
`error_code` уже читается из `store.Download.ErrorCode` (миграция не нужна)
|
||||
- [x] 1.2 В `checkTimeouts` считать возраст от `torrent.AddedOn` (UTC) через
|
||||
хелпер `torrentAge` (фолбэк на `created_at`, если `added_on` нет)
|
||||
- [x] 1.3 Поднять дефолт `[worker].magnet_timeout` до `24h` в
|
||||
`internal/config/config.go` и `config.example.toml` (с пометкой про страховку)
|
||||
|
||||
## 2. Уведомление о падении
|
||||
|
||||
- [x] 2.1 Добавить `EventFailed` в `NotifyEvent` (worker.go)
|
||||
- [x] 2.2 В `transition` слать `EventFailed` при переходе в `StateFailed` и
|
||||
`StateStuck` (неблокирующе, вне `w.mu`)
|
||||
- [x] 2.3 Обработать `EventFailed` в Telegram (`renderFailed` + `retryKeyboard`);
|
||||
httpapi `Notifier` не реализует — только tgbot
|
||||
|
||||
## 3. Авто-восстановление в сверке
|
||||
|
||||
- [x] 3.1 Восстановление `idempotency_key` отдельным методом стора НЕ нужно:
|
||||
`SetDownloadState` сам ставит ключ в `infohash` для нетерминального состояния
|
||||
- [x] 3.2 Реализовать `reconcileRecovery` (под `w.mu`, из `Poll`): задачи в
|
||||
`failed`/`stuck` с `error_code` `magnet_timeout`/`stalled`, поиск в `byHash`
|
||||
- [x] 3.3 Воскрешать только при прогрессе торрента (`torrentProgressed`):
|
||||
`magnet_timeout` — `!isMeta`; `stalled` — `!isStalledDL`; не `classErrored`;
|
||||
целевое состояние из `classify` (`recoveredState`: ready → `completed`,
|
||||
downloading → `downloading`)
|
||||
- [x] 3.4 Конфликт занятого `idempotency_key`: пре-проверка через
|
||||
`FindActiveByInfohash` — оставляем в `failed` + лог
|
||||
- [x] 3.5 Тесты (`recovery_test.go`): метаданные → `downloading`; готов →
|
||||
`completed`; всё ещё `metaDL` → `failed`; `qbit_error`/ошибка не воскрешаются;
|
||||
нет источника → `failed`; конфликт ключа → `failed`
|
||||
|
||||
## 4. Починка Retry и ручной retry в транспортах
|
||||
|
||||
- [x] 4.1 `Worker.Retry`: при живом торренте — без повторного `Add`, только
|
||||
`downloading`; `Add` лишь когда раздачи нет
|
||||
- [x] 4.2 Тест: retry при живом торренте не делает `Add` и не падает на
|
||||
ближайшем тике (базис `added_on`)
|
||||
- [x] 4.3 Кнопка retry в веб-UI (`internal/httpapi` + `index.html`) для
|
||||
`failed`/`stuck`; роут `/ui/downloads/{id}/retry`; тест `TestUIRetry`
|
||||
- [x] 4.4 Действие retry в Telegram-боте (колбэк `retry:` + кнопка); тесты
|
||||
`TestBot_CallbackRetry`, `TestBot_NotifyFailed`
|
||||
|
||||
## 5. Документация спек
|
||||
|
||||
- [x] 5.1 Обновить `docs/specs/workflow.md`: `magnet_timeout` как страховка,
|
||||
базис `added_on`, уведомление о `failed`/`stuck`, восстановление и retry
|
||||
- [x] 5.2 `openspec validate --strict download-failure-recovery` — без ошибок
|
||||
|
||||
## 6. Проверка
|
||||
|
||||
- [x] 6.1 `task lint` (0 issues) и `task test` (зелёные)
|
||||
- [x] 6.2 Ревью кода (чекпоинт до archive) — 8-угловой multi-agent проход
|
||||
|
||||
## 7. Фиксы по ревью кода
|
||||
|
||||
- [x] 7.1 Конфликт `idempotency_key`: проверка `FindActiveByInfohash`
|
||||
распространена на ветку `completed` (а не только `downloading`) —
|
||||
иначе constraint-ошибка и зависание в `failed` с логом каждый тик
|
||||
(тест `TestRecoverySkipsConflictOnCompleted`)
|
||||
- [x] 7.2 Recovery не пишет в `error_msg` здоровой задачи (передаём `""`,
|
||||
причину — в лог), иначе заметка светилась бы как ошибка в UI/REST
|
||||
- [x] 7.3 Эффективность: `reconcileRecovery` грузит кандидатов через
|
||||
`store.ListRecoverable` (SQL-фильтр по `error_code`), а не вычитывает все
|
||||
failed/stuck каждый тик
|
||||
- [x] 7.4 Дебаунс уведомлений о падении (`shouldNotifyFail`, окно 1h) —
|
||||
мерцающий stalled-торрент не спамит `EventFailed` (тест
|
||||
`TestFailNotifyDebounce`)
|
||||
- [x] 7.5 Уведомление о падении `qbit_add` в ingest через closure
|
||||
(`SetFailureNotifier`, wiring в serve.go), тест
|
||||
`TestIngestQbitErrorNotifies`
|
||||
- [x] 7.6 Конвенция логирования: убран неймспейс-префикс `recovery:` из `msg`
|
||||
- [x] 7.7 Мелочи: дедуп ветки `renderCard`, диагностика в `torrentAge`,
|
||||
`time.Unix(...).UTC()`, актуализирован комментарий `terminalStates`
|
||||
Reference in New Issue
Block a user