Приём: дедуп по target_missing/orphaned + стоп-кран «Закрыть»
Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.
- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
привязывается к существующей записи (спящей, без обращения к qBittorrent),
а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
дедупе ведёт на страницу существующей записи.
Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ type fakeStore struct {
|
||||
upgradeUp bool // что вернуть из UpgradeCatchedMagnetToTorrent
|
||||
}
|
||||
|
||||
func (f *fakeStore) FindActiveByInfohash(_ context.Context, _ ...string) (*store.Download, error) {
|
||||
func (f *fakeStore) FindReingestBlockingByInfohash(_ context.Context, _ ...string) (*store.Download, error) {
|
||||
return f.active, nil
|
||||
}
|
||||
|
||||
@@ -159,6 +159,31 @@ func TestIngestIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Повторный приём привязывается к удерживающей источник desync-записи
|
||||
// (target_missing/orphaned) вместо создания близнеца: возвращается существующая
|
||||
// «спящей» (её состояние не меняется, к qBittorrent не ходим), Deduplicated=true.
|
||||
func TestIngestAttachesToDesyncRecord(t *testing.T) {
|
||||
for _, s := range []store.State{store.StateTargetMissing, store.StateOrphaned} {
|
||||
t.Run(string(s), func(t *testing.T) {
|
||||
existing := &store.Download{ID: "01hzzzexisting000000000000", State: s}
|
||||
fs := &fakeStore{active: existing}
|
||||
res, err := newService(fs).Ingest(context.Background(), Request{Source: sampleMagnet})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
if !res.Deduplicated || res.DownloadID != existing.ID {
|
||||
t.Errorf("ожидалось присоединение к desync-записи: %+v", res)
|
||||
}
|
||||
if res.State != s {
|
||||
t.Errorf("состояние существующей записи должно вернуться как есть (%s), got %s", s, res.State)
|
||||
}
|
||||
if len(fs.created) != 0 {
|
||||
t.Error("не должно создаваться новой задачи (близнеца)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Быстрый дедуп-путь доносит существующей задаче недостающие хеши
|
||||
// гибридного magnet (иначе последующий приём по второму хешу создал бы
|
||||
// вторую активную задачу).
|
||||
|
||||
Reference in New Issue
Block a user