Приём: гард дедуп-дозаписи хешей (F1) и апгрейд catched-magnet до torrent (F6)

Два дефекта дедуп-веток приёма (ревью Fable 2026-07-08), оба про инвариант
«≤1 активная загрузка на infohash» и сохранность источника.

F1: дедуп-ветка CreateDownloadIfNoActive дописывала все хеши входящего
источника в найденную активную задачу без пер-хеш гарда владения (в отличие
от AddInfohashes). Гибрид {v1,v2}, дедупнувшись на задачу B (владелец v2),
крал v1 у активной A → две активные владели v1. Теперь дозапись под тем же
гардом: хеш, которым владеет другая активная задача, не дописывается.

F6: при дедупе .torrent-байт на пойманную magnet-задачу (catched) байты
выбрасывались, source_type оставался magnet → worker добавлял по magnet-URL →
вечный metaDL → failed (magnet закрытого трекера без DHT метаданные не
докачает). Новый guarded-метод UpgradeCatchedMagnetToTorrent атомарно
сохраняет байты и меняет source_type magnet→torrent, но только пока задача в
catched (worker источник ещё не отдал). Ingest зовёт апгрейд на обоих
дедуп-путях. Это целевое исключение из правила спеки «при дедупе байты не
сохраняем» — оформлено MODIFIED-дельтой ingest.

Схема БД не меняется (download_torrent и source_type уже есть).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-08 17:18:28 +03:00
co-authored by Claude Opus 4.8
parent b1bca98738
commit 4475fbd548
11 changed files with 605 additions and 13 deletions
+45
View File
@@ -3,6 +3,7 @@ package store
import (
"context"
"errors"
"slices"
"strings"
"sync"
"testing"
@@ -167,6 +168,50 @@ func TestFindActiveByInfohash(t *testing.T) {
}
}
// F1: дедуп-ветка CreateDownloadIfNoActive доносит недостающие хеши под пер-хеш
// гардом владения — хеш, которым владеет ДРУГАЯ активная задача (split-identity
// гибрида), красть не должна, иначе две активные владели бы одним инфохэшем.
func TestDedupTopUpDoesNotStealForeignHash(t *testing.T) {
st := newTestStore(t)
ctx := context.Background()
v1 := hashN(1)
v2 := hashN(2)
// A владеет v1, B владеет v2 — две активные задачи одного гибрида (split).
idA := mustCreate(t, st, v1)
idB := mustCreate(t, st, v2)
// Приём гибрида {v1, v2} → дедуп на одну из активных; чужой хеш не крадём.
existing, err := st.CreateDownloadIfNoActive(ctx, newDownloading(), []string{v1, v2}, nil)
if err != nil {
t.Fatalf("create: %v", err)
}
if existing == nil {
t.Fatal("ожидался дедуп на активную задачу")
}
assertHashes(t, st, idA, v1) // A по-прежнему владеет только v1
assertHashes(t, st, idB, v2) // B по-прежнему владеет только v2
}
// assertHashes проверяет точный набор инфохэшей загрузки.
func assertHashes(t *testing.T, st *Store, id string, want ...string) {
t.Helper()
d, err := st.GetDownload(context.Background(), id)
if err != nil {
t.Fatalf("get %s: %v", id, err)
}
got := d.HashList()
if len(got) != len(want) {
t.Fatalf("%s hashes = %v, want %v", id, got, want)
}
for _, w := range want {
if !slices.Contains(got, w) {
t.Fatalf("%s hashes = %v, want содержит %s", id, got, w)
}
}
}
// Состояния рассинхрона (target_missing/orphaned/deleted) терминальны: задача
// в них не должна считаться «активной» (иначе relink/ingest-дедуп решат, что
// для infohash уже есть активная задача). Регрессия: FindActiveByInfohash и