Приём: усыновление присутствующего в qBittorrent торрента вместо дубль-Add (409)
processCatched перед Add проверяет присутствие торрента в qBittorrent (один листинг на тик): если раздача уже есть — усыновляем (promote catched→downloading без повторного Add и без LLM-namer, имя из раздачи), иначе добавляем как раньше. Это убирает бесконечный цикл дубль-Add → 409 → ретрай и лишние вызовы LLM. Инвариант приёма «одна активная на infohash» делает различие «наш/чужой» ненужным. source_type перечитывается под замком (сужение гонки апгрейда F6); при недоступности qBittorrent тик пропускается без вызова LLM. Дедуп на приёме (дубль на уже активную задачу) теперь отражается явным ответом бота «дубль уже активной #id — добавление отменено». Спека download-tracking обновлена (OpenSpec change заархивирован); закрыта задача беклога review-f2-promote-without-add. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
## Context
|
||||
|
||||
`processCatched` (`internal/worker/worker.go`) вызывается из `pollOnce` сразу
|
||||
после `Poll`. Сейчас он безусловно зовёт namer (LLM) и `qbt.Add`, а `409`/`Fails.`
|
||||
на дубле трактует как транзиентный сбой → вечный повтор с тратой LLM.
|
||||
|
||||
Приём (`ingest`) уже дедуплицирует по infohash на **активную** задачу
|
||||
(`FindActiveByInfohash`/`CreateDownloadIfNoActive`). Значит до `processCatched`
|
||||
доходит только загрузка, для которой в jellybit нет другой активной задачи.
|
||||
Отсюда упрощение: если торрент такой загрузки уже присутствует в qBittorrent,
|
||||
это не «конфликт с чужой задачей», а «раздачу уже кто-то (пользователь вручную,
|
||||
прошлый тик) добавил» — надо просто **усыновить** её и разложить.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Пойманная загрузка, чей торрент уже в qBittorrent, доводится до `downloading`
|
||||
без повторного `add` (без 409) и без LLM; дальше — обычная раскладка.
|
||||
- LLM-namer не вызывается ни при усыновлении, ни при недоступности qBittorrent.
|
||||
- Гонка апгрейда F6 сужена перечитыванием `source_type` под блокировкой.
|
||||
- Повторное добавление уже активной в jellybit загрузки транспорт отражает как
|
||||
дубль (сообщение + лог), без новой записи.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не заводим новых состояний загрузки. Дедуп на приёме записи не создаёт;
|
||||
усыновление — это `downloading`, а не отдельный статус.
|
||||
- Не различаем «наш/чужой» торрент по категории/тегу: инвариант приёма делает
|
||||
различие ненужным (до воркера доходит лишь загрузка без другой активной).
|
||||
- Не добавляем счётчик попыток `add` (предел — время `catch_timeout`).
|
||||
- Не проверяем присутствие на приёме (`ingest` остаётся быстрым, без qBittorrent).
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. Источник снимка присутствия: один листинг `qbt.Torrents("")` на входе в
|
||||
`processCatched`.** Строим `byHash` (по `Hash`/`InfohashV1`/`InfohashV2`,
|
||||
lowercase), переиспользуем для всех catched-задач тика (как это делает Poll для
|
||||
своего снимка). Провал листинга → qBittorrent недоступен → в этот тик пойманные
|
||||
не трогаем (namer не зовём), повтор на следующем; отсечка — `catch_timeout`.
|
||||
Поиск торрента задачи — существующий `torrentFor(d, byHash)`.
|
||||
|
||||
**2. Присутствует → усыновляем; ветвление до namer.** Если `torrentFor` нашёл
|
||||
раздачу — `PromoteCatched(id, t.Name)` (перевод `catched → downloading` + имя из
|
||||
`qbt.Torrent.Name`, без LLM), под коротким замком с ре-валидацией `state='catched'`.
|
||||
Иначе — обычный путь: re-read под замком → namer → `sourceAddParts` → `Add` →
|
||||
`PromoteCatched`. namer (LLM) на ветке усыновления и при недоступности qBit не
|
||||
зовётся.
|
||||
|
||||
**3. Никакого различия «наш/чужой» и никакого `duplicated`.** Инвариант приёма
|
||||
(«одна активная на infohash») гарантирует, что усыновляемая раздача не отберётся
|
||||
у другой активной задачи. `PromoteCatched` (гард `state='catched'`) корректен;
|
||||
`ActivateIfNoOtherActive` (как в `Retry` из терминального `failed`) здесь не
|
||||
нужен — `catched` нетерминален и уже единственный активный владелец infohash.
|
||||
Имя раздачи в `display_name` полезно и уведомлениям, и заголовку в UI (у `catched`
|
||||
оно пусто).
|
||||
|
||||
**4. Re-read `source_type` под замком перед добавлением (сужение гонки F6).**
|
||||
На absent-ветке перед сбором `addReq` берём короткий замок, перечитываем запись
|
||||
(`GetDownload`): ре-валидация `state='catched'` и актуальный `source_type`
|
||||
(апгрейд magnet→torrent мог случиться после снятия списка). Тяжёлые вызовы
|
||||
(`GetTorrentData`, namer, `Add`) — вне замка.
|
||||
|
||||
**5. Дедуп на приёме (case 1) — только сообщение.** `ingest` при попадании на
|
||||
активную задачу уже возвращает `Deduplicated=true` (запись не создаётся). Меняем
|
||||
лишь текст ответа транспорта: вместо «Уже в работе #id» — «♻️ дубль уже активной
|
||||
#id, добавление отменено». Лог дедупа (`download attached to active`) уже есть.
|
||||
Новых состояний/записей не заводим.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Остаточное окно F6]** → re-read `source_type` под замком + `Add` вне замка
|
||||
окно резко **сужают**, но не закрывают полностью. Полное закрытие требует
|
||||
держать замок через `Add`, что нарушает инвариант «тяжёлые вызовы вне замка».
|
||||
Оверлап крайне редок, цена промаха — один неудачный magnet-add, повтор на
|
||||
следующем тике уже увидит `torrent`. Принимаем суженное окно осознанно.
|
||||
- **[Снимок присутствия на тик «отстаёт»]** → каждая catched-задача
|
||||
обрабатывается раз за тик; если наш `add` прошёл, а запись перехода сорвалась,
|
||||
усыновление случится на следующем тике, где листинг уже видит раздачу.
|
||||
- **[Усыновление раздачи, добавленной вручную с иными savepath/категорией]** →
|
||||
инвариант источника не нарушается: файлы не наши, раскладка хардлинчит
|
||||
отдельно; infohash совпадает — контент тот же. Осознанное поведение (как
|
||||
`discover`).
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет.
|
||||
Reference in New Issue
Block a user