feat(download): judge a queued slskd peer by its queue position
No bytes for ten minutes usually means a peer has queued us, and the
stall timer could not tell position 2 from position 400: the first was
abandoned while it was about to start, the second was waited on for ten
minutes for nothing.
While a requested file is "Queued, Remotely", the grab asks slskd for
its place (GET .../downloads/{user}/{id}/position, which asks the peer)
once a minute. A place that improved counts as progress and restarts
the stall clock. A place beyond 50 twice running gives the peer up at
once, and the manager moves to the next copy; two readings because
slskd documents the figure as possibly inaccurate. Waiting in a queue
has an overall ceiling of an hour without a byte, since a queue moving
one place an hour would otherwise hold the grab all day. As with a
stall, files that already arrived still go forward.
Closes #275
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HJiuc3ZZhxsPXz3ozTirT
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A peer that has queued us is judged by where we are in its queue, not
|
||||
// only by a timer (#275).
|
||||
|
||||
func queuedThen(polls int, final string) [][]slskdTransfer {
|
||||
queued := []slskdTransfer{
|
||||
{ID: "t0", Filename: `\s\Album\01 A.flac`, State: "Queued, Remotely"},
|
||||
{ID: "t1", Filename: `\s\Album\02 B.flac`, State: "Queued, Remotely"},
|
||||
}
|
||||
|
||||
out := make([][]slskdTransfer, 0, polls+1)
|
||||
for range polls {
|
||||
out = append(out, queued)
|
||||
}
|
||||
|
||||
return append(out, []slskdTransfer{
|
||||
{ID: "t0", Filename: `\s\Album\01 A.flac`, State: final, BytesTransferred: 500},
|
||||
{ID: "t1", Filename: `\s\Album\02 B.flac`, State: final, BytesTransferred: 500},
|
||||
})
|
||||
}
|
||||
|
||||
func descending(from int) []int {
|
||||
out := make([]int, 0, from)
|
||||
for p := from; p >= 1; p-- {
|
||||
out = append(out, p)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Two readings far back in the queue give the peer up at once, rather
|
||||
// than after the stall timer.
|
||||
func TestSlskdGivesUpOnALongQueue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stub := newSlskdStub(t)
|
||||
stub.transfers = queuedThen(100_000, "Completed, Succeeded")
|
||||
stub.positions = []int{400}
|
||||
|
||||
s, _ := newStubSlskd(t, stub)
|
||||
s.stallAfter = time.Hour
|
||||
|
||||
started := time.Now()
|
||||
|
||||
_, err := s.Grab(context.Background(), slskdAlbum(t, stub, "peer"), t.TempDir(), nil)
|
||||
if !errors.Is(err, ErrSlskdTimeout) || !strings.Contains(err.Error(), "position 400") {
|
||||
t.Fatalf("Grab = %v, want a queue-position give-up", err)
|
||||
}
|
||||
|
||||
if time.Since(started) > 5*time.Second {
|
||||
t.Error("the give-up waited on something other than the position")
|
||||
}
|
||||
|
||||
if len(stub.cancelledURIs()) == 0 {
|
||||
t.Error("the queued transfers were not cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// One far reading is not enough: slskd says the figure can be wildly
|
||||
// wrong.
|
||||
func TestSlskdOneBadPositionIsNotEnough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stub := newSlskdStub(t)
|
||||
stub.transfers = queuedThen(20, "Completed, Succeeded")
|
||||
stub.positions = []int{400, 3}
|
||||
|
||||
s, _ := newStubSlskd(t, stub)
|
||||
s.positionPoll = 0
|
||||
|
||||
if _, err := s.Grab(
|
||||
context.Background(),
|
||||
slskdAlbum(t, stub, "peer", "01 A.flac", "02 B.flac"),
|
||||
t.TempDir(), nil,
|
||||
); err != nil {
|
||||
t.Fatalf("Grab: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A queue that is moving is progress: the grab outlives the stall timer
|
||||
// while its position improves.
|
||||
func TestSlskdAMovingQueueIsProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stub := newSlskdStub(t)
|
||||
// Near enough to wait for, with more improving readings (45) than
|
||||
// there are queued polls (30), so the queue outlives the stall timer
|
||||
// (30 polls of at least 2 ms against 40 ms) while still improving,
|
||||
// however slowly the machine runs the loop.
|
||||
stub.transfers = queuedThen(30, "Completed, Succeeded")
|
||||
stub.positions = descending(slskdMaxQueuePosition - 5)
|
||||
|
||||
s, _ := newStubSlskd(t, stub)
|
||||
s.transferPoll = 2 * time.Millisecond
|
||||
s.positionPoll = 0
|
||||
s.stallAfter = 40 * time.Millisecond
|
||||
|
||||
if _, err := s.Grab(
|
||||
context.Background(),
|
||||
slskdAlbum(t, stub, "peer", "01 A.flac", "02 B.flac"),
|
||||
t.TempDir(), nil,
|
||||
); err != nil {
|
||||
t.Fatalf("Grab: %v; a moving queue was treated as a stall", err)
|
||||
}
|
||||
}
|
||||
|
||||
// However steadily the queue moves, waiting in it has a ceiling.
|
||||
func TestSlskdQueueHasACeiling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stub := newSlskdStub(t)
|
||||
stub.transfers = queuedThen(100_000, "Completed, Succeeded")
|
||||
stub.positions = descending(100_000)
|
||||
|
||||
s, _ := newStubSlskd(t, stub)
|
||||
s.stallAfter = time.Hour
|
||||
s.queueCeiling = 50 * time.Millisecond
|
||||
|
||||
_, err := s.Grab(context.Background(), slskdAlbum(t, stub, "peer"), t.TempDir(), nil)
|
||||
if !errors.Is(err, ErrSlskdTimeout) {
|
||||
t.Fatalf("Grab = %v, want the queue ceiling", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user