Долгий 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>
76 lines
5.9 KiB
Markdown
76 lines
5.9 KiB
Markdown
## Why
|
||
|
||
Загрузка magnet'ом ушла в терминальный `failed`/`magnet_timeout` по
|
||
wall-clock таймауту (возраст от `created_at`, ~1ч), хотя qBittorrent просто
|
||
долго тянул метаданные (медленные трекеры / мало пиров). Метаданные в итоге
|
||
пришли, торрент жив и качается, но задача застряла в терминальном состоянии
|
||
без выхода — восстановить её нельзя. Вдобавок падение происходит молча (нет
|
||
уведомления автору), а единственный путь возврата `Worker.Retry` баговый
|
||
(не сбрасывает базис времени → задача мгновенно снова падает) и доступен
|
||
только через REST, но не из веб-UI и Telegram.
|
||
|
||
## What Changes
|
||
|
||
- **Терпеливость к `metaDL`.** Перестаём убивать долгий magnet агрессивным
|
||
таймаутом. Дефолт `[worker].magnet_timeout` поднимается до `24h` — это
|
||
редкий страховочный предохранитель, а не рабочий механизм. Настоящие
|
||
провалы определяются по статусам ошибок qBittorrent (`error`/`missingFiles`
|
||
→ `qbit_error`), а не по wall-clock. *(У qBittorrent нет статуса «magnet
|
||
мёртв» — зависший magnet вечно висит в `metaDL`, поэтому единственный
|
||
сигнал на этот кейс — большой страховочный таймаут.)*
|
||
- **Базис таймаута — от факта, а не от `created_at`.** Возраст для
|
||
`magnet_timeout`/`stalled` считаем от времени добавления торрента в
|
||
qBittorrent (`added_on`), а не от создания записи. Это чинит неверный
|
||
отсчёт для усыновлённых раздач и устраняет мгновенное повторное падение
|
||
после возврата в `downloading`.
|
||
- **Уведомление о любом падении.** Переход в `failed` (любой `error_code`)
|
||
и `stuck` уведомляет автора загрузки через `notifier` (раньше уведомления
|
||
слались только для `review`/`done`/`orphaned`/`target_missing`).
|
||
- **Авто-восстановление из `failed`/`stuck`.** Фоновая сверка
|
||
(state-reconciliation) замечает, что у задачи в восстановимом
|
||
`failed`/`stuck` (наша нетерпеливость: `magnet_timeout`, `stalled`)
|
||
источник в qBittorrent жив и продвинулся, и возвращает задачу в поток
|
||
(`downloading` либо `completed` по статусу торрента). Пользовательские
|
||
и реальные провалы (`qbit_error`, `reverted`, `cancelled`) сверка не
|
||
воскрешает.
|
||
- **Ручной retry из UI и Telegram.** Кнопка повторной попытки добавляется в
|
||
веб-UI и Telegram-бот (раньше — только Cancel; retry был только в REST).
|
||
`Worker.Retry` чинится: перецепляется к уже живому торренту вместо слепого
|
||
повторного `Add`, базис таймаута сбрасывается.
|
||
|
||
## Capabilities
|
||
|
||
### New Capabilities
|
||
|
||
Нет. Уведомления о падении и семантика таймаута относятся к жизненному циклу
|
||
загрузки, который пока живёт в `docs/specs/workflow.md` (ещё не мигрирован в
|
||
OpenSpec); заводить отдельную capability `notifications` сейчас —
|
||
преждевременное дробление.
|
||
|
||
### Modified Capabilities
|
||
|
||
- `state-reconciliation`: восстановимые `failed`/`stuck` (`magnet_timeout`,
|
||
`stalled`) перестают быть «неприкосновенными» для сверки и подлежат
|
||
авто-восстановлению при живом продвинувшемся источнике; добавляется
|
||
требование о восстановлении и о доступности ручного retry. `qbit_error`,
|
||
`reverted`, `cancelled`, `deleted` остаются вне восстановления.
|
||
|
||
## Impact
|
||
|
||
- **Спеки:** дельта `state-reconciliation`; обновление графа переходов и
|
||
семантики таймаута/уведомлений в `docs/specs/workflow.md` (источник истины
|
||
по жизненному циклу до миграции).
|
||
- **Конфиг:** дефолт `[worker].magnet_timeout` → `24h`
|
||
(`internal/config/config.go`), `config.example.toml`.
|
||
- **Код:** `internal/worker/worker.go` — `transition` (уведомление о
|
||
failed/stuck), `checkTimeouts` (базис от `added_on`), `reconcile`/
|
||
`reconcileDesync` (воскрешение из failed/stuck), `Retry` (перецепление +
|
||
сброс базиса); новый `Event` падения и его обработка в `notifier`/
|
||
`tgbot`/`httpapi`; кнопка retry в `internal/httpapi` и `internal/tgbot`;
|
||
вынос строки `"magnet_timeout"` (и смежных `error_code`) в именованные
|
||
константы рядом с состояниями.
|
||
- **qBittorrent-клиент:** возможно потребуется поле `added_on` в
|
||
`qbt.Torrent` (если ещё не читается).
|
||
- **Миграции БД:** не ожидаются (восстановление опирается на состояние qBit и
|
||
существующие поля задачи).
|