Приём: гард дедуп-дозаписи хешей (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:
@@ -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 и
|
||||
|
||||
Reference in New Issue
Block a user