feat(seeding): wire seed ratio/time lifecycle into the torrent daemon
SeedRatio/SeedTime were declared on TorrentConfig but never consumed, and SeedEnabled was hardcoded false in both constructors — the daemon never seeded, and if forced it seeded forever. - config: [downloads] seed_enabled/seed_ratio/seed_time (opt-in, off by default) - daemon: parse seed_time + wire all three; startup log per target shape - engine: seedTargetReached() (pure) + seedAndDrop() background monitor on a downloader-scoped seedCtx (not the task ctx, which dies when Download returns); drops the torrent on ratio (uploaded/size) OR time, whichever first; no target = seed until shutdown. Configurable check interval (tests lower it). - fix: cleanup() now always drops — previously leaked the handle on error paths when seeding was enabled. - refactor: dropTracked() helper shared by cleanup + post-seeding drop. Tests: TestSeedTargetReached (9 cases) + ctx/no-target branches + loopback swarm smoke (-tags smoke). Roadmap hueco closed.
This commit is contained in:
parent
665ec0a34f
commit
132c88b3f0
8 changed files with 459 additions and 34 deletions
|
|
@ -264,3 +264,114 @@ func TestTorrentDownloader_DownloadTimeout_MetadataCancel(t *testing.T) {
|
|||
func TestTorrentDownloader_ImplementsInterface(t *testing.T) {
|
||||
var _ Downloader = (*TorrentDownloader)(nil)
|
||||
}
|
||||
|
||||
// TestSeedTargetReached cubre la lógica pura de parada del seeding: ratio,
|
||||
// tiempo, ninguno, ambos (el primero que se cumple gana) y la guarda de tamaño
|
||||
// cero (no debe dividir por cero ni parar por ratio).
|
||||
func TestSeedTargetReached(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ratioTarget float64
|
||||
timeTarget time.Duration
|
||||
uploaded int64
|
||||
size int64
|
||||
elapsed time.Duration
|
||||
wantStop bool
|
||||
}{
|
||||
{"ratio reached", 2.0, 0, 200, 100, time.Minute, true},
|
||||
{"ratio not reached", 2.0, 0, 150, 100, time.Minute, false},
|
||||
{"ratio exactly met", 1.0, 0, 100, 100, time.Minute, true},
|
||||
{"time reached", 0, time.Hour, 10, 100, 2 * time.Hour, true},
|
||||
{"time not reached", 0, time.Hour, 10, 100, 30 * time.Minute, false},
|
||||
{"no targets never stops", 0, 0, 9999, 100, 99 * time.Hour, false},
|
||||
{"ratio wins when both set", 2.0, time.Hour, 200, 100, time.Second, true},
|
||||
{"time wins when ratio short", 5.0, time.Hour, 100, 100, 2 * time.Hour, true},
|
||||
{"zero size guards div", 2.0, 0, 200, 0, time.Minute, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reason := seedTargetReached(tc.ratioTarget, tc.timeTarget, tc.uploaded, tc.size, tc.elapsed)
|
||||
if got := reason != ""; got != tc.wantStop {
|
||||
t.Errorf("seedTargetReached(ratio=%.1f time=%s up=%d size=%d el=%s) stop=%v (reason %q), want %v",
|
||||
tc.ratioTarget, tc.timeTarget, tc.uploaded, tc.size, tc.elapsed, got, reason, tc.wantStop)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTorrentDownloader_SeedRatioTime verifica que SeedRatio y SeedTime se
|
||||
// propagan a la config del downloader.
|
||||
func TestTorrentDownloader_SeedRatioTime(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dl, err := NewTorrentDownloader(TorrentConfig{
|
||||
DataDir: dir,
|
||||
SeedEnabled: true,
|
||||
SeedRatio: 1.5,
|
||||
SeedTime: 2 * time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewTorrentDownloader: %v", err)
|
||||
}
|
||||
defer dl.Shutdown(context.Background())
|
||||
|
||||
if dl.cfg.SeedRatio != 1.5 {
|
||||
t.Errorf("SeedRatio = %v, want 1.5", dl.cfg.SeedRatio)
|
||||
}
|
||||
if dl.cfg.SeedTime != 2*time.Hour {
|
||||
t.Errorf("SeedTime = %v, want 2h", dl.cfg.SeedTime)
|
||||
}
|
||||
if dl.seedCtx == nil || dl.seedCancel == nil {
|
||||
t.Error("seedCtx/seedCancel must be initialised by the constructor")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedAndDrop_NoTargetReturnsImmediately verifica que sin ratio ni tiempo
|
||||
// objetivo, seedAndDrop retorna de inmediato (siembra indefinida) sin tocar el
|
||||
// handle — por eso es seguro pasar un torrent nil.
|
||||
func TestSeedAndDrop_NoTargetReturnsImmediately(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dl, err := NewTorrentDownloader(TorrentConfig{DataDir: dir, SeedEnabled: true}) // ratio 0, time 0
|
||||
if err != nil {
|
||||
t.Fatalf("NewTorrentDownloader: %v", err)
|
||||
}
|
||||
defer dl.Shutdown(context.Background())
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
dl.seedAndDrop("no-target-task-id", nil, 1000)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("seedAndDrop with no target should return immediately")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedAndDrop_StopsOnSeedCtxCancel verifica que seedAndDrop sale cuando se
|
||||
// cancela seedCtx (ruta de Shutdown), incluso con un objetivo de ratio alto y el
|
||||
// tick deshabilitado — el único camino de salida es seedCtx.Done().
|
||||
func TestSeedAndDrop_StopsOnSeedCtxCancel(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dl, err := NewTorrentDownloader(TorrentConfig{DataDir: dir, SeedEnabled: true, SeedRatio: 99})
|
||||
if err != nil {
|
||||
t.Fatalf("NewTorrentDownloader: %v", err)
|
||||
}
|
||||
defer dl.Shutdown(context.Background())
|
||||
|
||||
dl.seedCheckInterval = time.Hour // el ticker no disparará; solo seedCtx.Done() puede terminar
|
||||
dl.seedCancel() // cancela antes de arrancar el monitor
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
dl.seedAndDrop("ctx-cancel-task-id", nil, 1000)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("seedAndDrop should return when seedCtx is cancelled")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue