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
78 lines
1.3 KiB
Go
78 lines
1.3 KiB
Go
package download
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
// keyedLock is a set of mutexes created on demand, one per key, that
|
|
// honour a context while waiting. An entry lives only while someone
|
|
// holds or waits on it, so a key per Soulseek peer or per folder name
|
|
// does not accumulate for the life of the process.
|
|
type keyedLock[K comparable] struct {
|
|
mu sync.Mutex
|
|
held map[K]*keyedEntry
|
|
}
|
|
|
|
type keyedEntry struct {
|
|
ch chan struct{}
|
|
|
|
// refs counts holders and waiters; the entry is dropped at zero.
|
|
refs int
|
|
}
|
|
|
|
// acquire blocks until k is free or ctx ends, and returns the function
|
|
// that frees it.
|
|
func (l *keyedLock[K]) acquire(ctx context.Context, k K) (func(), error) {
|
|
l.mu.Lock()
|
|
|
|
if l.held == nil {
|
|
l.held = map[K]*keyedEntry{}
|
|
}
|
|
|
|
e, ok := l.held[k]
|
|
if !ok {
|
|
e = &keyedEntry{ch: make(chan struct{}, 1)}
|
|
l.held[k] = e
|
|
}
|
|
|
|
e.refs++
|
|
|
|
l.mu.Unlock()
|
|
|
|
select {
|
|
case e.ch <- struct{}{}:
|
|
case <-ctx.Done():
|
|
l.drop(k, e)
|
|
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
var once sync.Once
|
|
|
|
return func() {
|
|
once.Do(func() {
|
|
<-e.ch
|
|
l.drop(k, e)
|
|
})
|
|
}, nil
|
|
}
|
|
|
|
func (l *keyedLock[K]) drop(k K, e *keyedEntry) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
e.refs--
|
|
if e.refs == 0 {
|
|
delete(l.held, k)
|
|
}
|
|
}
|
|
|
|
// size reports how many keys are held or awaited, for tests.
|
|
func (l *keyedLock[K]) size() int {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
return len(l.held)
|
|
}
|