feat(download): one grab per Soulseek peer, several peers at once
slskd was capped at one transfer per daemon, on the grounds that Soulseek peers punish clients that ask for too much. That politeness is per peer: two different users do not compete for anyone's upload slot. So one slow peer serialised every other Soulseek download behind it. The manager now takes a per-(provider, peer) lock before any slot, so a grab waiting on a busy peer does not hold a provider slot another peer could use, and the slskd default rises to 3, which now counts peers. The help text says so. Running grabs at once exposed the folder collision: slskd names a download's directory after the remote leaf folder, so two peers' "Greatest Hits" (or any two rips' "CD1") share one directory, and collect finds files by name there. Grabs whose local folders overlap now take a package-level lock per folder, in sorted order, keyed on the full path because two clients can share one daemon. Closes #272 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJiuc3ZZhxsPXz3ozTirT
This commit is contained in:
1 parent
5e3ac8fb1b
commit
9710c11476
6 files changed
+420
-24
No files matched your search
@@ -79,9 +79,9 @@ func TestConcurrencyForPrefersOverrideThenKind(t *testing.T) {
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "slskd defaults to one",
|
||||
name: "slskd defaults to a few peers",
|
||||
cfg: Config{Kind: KindSlskd},
|
||||
want: 1,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "usenet defaults higher",
|
||||
@@ -92,9 +92,9 @@ func TestConcurrencyForPrefersOverrideThenKind(t *testing.T) {
|
||||
name: "explicit override wins",
|
||||
cfg: Config{
|
||||
Kind: KindSlskd,
|
||||
Settings: map[string]string{concurrencyKey: "3"},
|
||||
Settings: map[string]string{concurrencyKey: "1"},
|
||||
},
|
||||
want: 3,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "nonsense override falls back",
|
||||
@@ -102,7 +102,7 @@ func TestConcurrencyForPrefersOverrideThenKind(t *testing.T) {
|
||||
Kind: KindSlskd,
|
||||
Settings: map[string]string{concurrencyKey: "not a number"},
|
||||
},
|
||||
want: 1,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "zero override falls back",
|
||||
@@ -110,7 +110,7 @@ func TestConcurrencyForPrefersOverrideThenKind(t *testing.T) {
|
||||
Kind: KindSlskd,
|
||||
Settings: map[string]string{concurrencyKey: "0"},
|
||||
},
|
||||
want: 1,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "unknown kind falls back to the global default",
|
||||
@@ -126,9 +126,9 @@ func TestConcurrencyForPrefersOverrideThenKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The reason the per-provider cap exists: a Soulseek daemon capped at
|
||||
// one transfer must serialize, even when the global cap would allow
|
||||
// more and the user has queued several albums at once.
|
||||
// The reason the per-provider cap exists: a daemon capped at one
|
||||
// transfer must serialize, even when the global cap would allow more and
|
||||
// the user has queued several albums at once.
|
||||
func TestPerProviderCapSerializesTransfers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -142,6 +142,7 @@ func TestPerProviderCapSerializesTransfers(t *testing.T) {
|
||||
ID: 1,
|
||||
Kind: KindSlskd,
|
||||
Priority: 50,
|
||||
Settings: map[string]string{concurrencyKey: "1"},
|
||||
}, slow)
|
||||
|
||||
// Three requests against the same one-at-a-time provider.
|
||||
@@ -210,8 +211,8 @@ func TestSyncSemaphoresReplacesChangedLimits(t *testing.T) {
|
||||
f.manager.installProvider(Config{ID: 1, Kind: KindSlskd}, nil)
|
||||
|
||||
first := f.manager.semaphoreFor(1)
|
||||
if cap(first) != 1 {
|
||||
t.Fatalf("slskd semaphore cap = %d, want 1", cap(first))
|
||||
if want := kindConcurrency[KindSlskd]; cap(first) != want {
|
||||
t.Fatalf("slskd semaphore cap = %d, want %d", cap(first), want)
|
||||
}
|
||||
|
||||
// Same limit: the semaphore is kept, so in-flight accounting is not
|
||||
|
||||
Reference in new issue
Block a user