Files
avandClaude Opus 4.8 4cc4de4269 OpenSpec: архивация трёх параллельных changes + синк спек
Итог параллельной волны фиксов (worktree-изоляция, cherry-pick в master):
- ingest-dedup-integrity (F1, F6) → спека ingest
- retry-stall-basis (MAJOR-1, MAJOR-2) → спека state-reconciliation
- linking-transition-robustness (MAJOR-4, MINOR-7) → спеки file-layout
  и state-reconciliation

Дельты влиты в openspec/specs, changes перенесены в
openspec/changes/archive/2026-07-08-*. Беклог не трогаю (по решению).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:21:22 +03:00

79 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Why
Два связанных бага в семантике таймаутов зависания и ручного retry делают
повседневные сценарии сломанными:
- **MAJOR-1 — retry живого торрента мгновенно снова падает.** `Worker.Retry`
при живой раздаче (`alive=true`) не переиздаёт `Add`, а лишь возвращает
задачу в `downloading`. Базис отсчёта таймаута (`age = now added_on`) при
этом НЕ сбрасывается. Если торрент давно добавлен/давно простаивает,
ближайший тик снова видит `stalledDL && age > stuck_after` → задача опять
уходит в `stuck` (~секунды). Спека `state-reconciliation` «Ручной повтор»
требует сброса базиса, но код его не выполняет (комментарий «базис от
added_on» верен лишь для ветки повторного `Add`). Существующий тест
`TestRetryReattachesNoReadd` прячет баг, ставя `added_on` «минуту назад».
- **MAJOR-2 — `stuck_after` мерит ВОЗРАСТ, а не ПРОСТОЙ.** `checkTimeouts`
считает `stalledDL`-таймаут от `added_on` (возраст торрента). Торрент,
качавшийся 5 часов и на один тик зашедший в `stalledDL` (нормальный проход
между пирами), мгновенно получает `stuck` со лживым сообщением «stalled for
5h» и уведомление `EventFailed`. Результат — флап `stuck``downloading` и
до-часовые ложные пинги. Спека сама противоречива: «`stalledDL` дольше
`stuck_after`» (простой) против «возраст от `added_on`».
Дополнительно закрываются два NIT из того же ревью:
- **NIT-10** — фолбэк базиса возраста `added_on → created_at` (когда qBit не
отдал `added_on`) остаётся, но теперь явно документирован и покрыт.
- **NIT-12** — retry задачи в `qbit_error` мгновенно откатывается: перецепка к
сломанному (`error`/`missingFiles`) живому торренту бессмысленна — reconcile
тут же возвращает задачу в `failed`. Retry перестаёт перецепляться к
сломанному торренту и повторно отдаёт источник.
## What Changes
- **Мера простоя вместо возраста для `stuck_after`.** `stalledDL`-таймаут
считается от `last_activity` qBittorrent (момент последнего движения данных),
а не от возраста торрента. Долго качавшийся торрент со свежей активностью в
`stuck` не уходит (MAJOR-2). `magnet_timeout` по-прежнему мерит **возраст**
(`metaDL` без метаданных) от `added_on` — это семантически верно.
- **Сброс базиса таймаутов при ручном retry.** Новая колонка `download.retried_at`
(RFC 3339 UTC) фиксирует момент retry и приподнимает базис ОБОИХ таймаутов
(`max(базис, retried_at)`). После retry задача получает свежее окно и не
падает снова на ближайшем тике (MAJOR-1). Хранится в БД (не в памяти), чтобы
сброс пережил интервал поллинга и рестарт процесса.
- **Retry не перецепляется к сломанному торренту.** Если живой торрент в
состоянии ошибки qBittorrent (`error`/`missingFiles`), retry повторно отдаёт
источник вместо перецепки (NIT-12).
## Capabilities
### New Capabilities
Нет. Семантика таймаутов зависания относится к жизненному циклу загрузки,
который пока живёт в `docs/specs/workflow.md` (не мигрирован в OpenSpec).
Нормативная правка `stuck_after`/`magnet_timeout` вносится туда; в OpenSpec
затрагивается только `state-reconciliation` (восстановление и ручной retry).
### Modified Capabilities
- `state-reconciliation`: уточняется, что предотвращение `stuck` для
прогрессирующего торрента опирается на **простой от `last_activity`**, а не
на возраст; ручной retry сбрасывает базис таймаутов через `retried_at` и не
перецепляется к сломанному живому торренту.
## Impact
- **Спеки:** дельта `state-reconciliation` (2 MODIFIED requirements);
правка семантики таймаутов и retry в `docs/specs/workflow.md` (источник
истины по жизненному циклу до миграции).
- **Код:** `internal/worker/worker.go``checkTimeouts` (две разные меры),
`torrentAge`/новые `stallDuration`/`addedBasis`/`retriedFloor`, `Retry`
(сброс базиса + перецепка только к здоровому торренту); `internal/qbt`
(поле `last_activity`); `internal/store/download.go` (`RetriedAt`,
`RetriedTime`, `SetRetriedAt`).
- **Миграции БД:** `0010_retried_at.sql` — колонка `download.retried_at`;
обновление ER-схемы `docs/specs/database.md`.
- **qBittorrent-клиент:** читается новое поле `last_activity` из того же
ответа `/torrents/info` (без нового вызова API).