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:
Deivid Soto 2026-06-01 10:30:39 +02:00
parent 665ec0a34f
commit 132c88b3f0
8 changed files with 459 additions and 34 deletions

View file

@ -246,6 +246,55 @@ enabled = false
}
}
func TestLoadSeedingDefaultsOff(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "config.toml")
// No [downloads] seeding keys — seeding must stay off by default.
os.WriteFile(path, []byte(`[auth]
api_key = "tc_x"
`), 0o644)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.Download.SeedEnabled {
t.Error("SeedEnabled should default to false")
}
if cfg.Download.SeedRatio != 0 {
t.Errorf("SeedRatio = %v, want 0", cfg.Download.SeedRatio)
}
if cfg.Download.SeedTime != "" {
t.Errorf("SeedTime = %q, want empty", cfg.Download.SeedTime)
}
}
func TestLoadSeedingExplicit(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "config.toml")
os.WriteFile(path, []byte(`[downloads]
seed_enabled = true
seed_ratio = 2.0
seed_time = "24h"
`), 0o644)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if !cfg.Download.SeedEnabled {
t.Error("SeedEnabled = false, want true")
}
if cfg.Download.SeedRatio != 2.0 {
t.Errorf("SeedRatio = %v, want 2.0", cfg.Download.SeedRatio)
}
if cfg.Download.SeedTime != "24h" {
t.Errorf("SeedTime = %q, want 24h", cfg.Download.SeedTime)
}
}
func TestLoadInvalidTOML(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "config.toml")